Use slate.js as a rich text editor for chat messages

This commit is contained in:
Gabe Kangas 2022-05-05 13:52:10 -07:00
parent f96bde4f71
commit 66a55401a8
No known key found for this signature in database
GPG key ID: 9A56337728BC81EA
3 changed files with 280 additions and 31 deletions

View file

@ -1,27 +1,101 @@
import { SmileOutlined } from '@ant-design/icons';
import { Button, Input } from 'antd';
import React, { useRef, useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import ContentEditable from 'react-contenteditable';
import { Transforms, createEditor, Node, BaseEditor, Text } from 'slate';
import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
import WebsocketService from '../../../services/websocket-service';
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
import { getCaretPosition, convertToText, convertOnPaste } from '../chat';
import { MessageType } from '../../../interfaces/socket-events';
import s from './ChatTextField.module.scss';
type CustomElement = { type: 'paragraph'; children: CustomText[] };
type CustomText = { text: string };
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
interface Props {
value?: string;
}
const Image = ({ element }) => (
<img
src={element.url}
alt="emoji"
style={{ display: 'inline', position: 'relative', width: '30px', bottom: '10px' }}
/>
);
const insertImage = (editor, url) => {
const text = { text: '' };
const image: ImageElement = { type: 'image', url, children: [text] };
Transforms.insertNodes(editor, image);
};
const withImages = editor => {
const { isVoid } = editor;
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
editor.isInline = element => element.type === 'image';
return editor;
};
export type EmptyText = {
text: string;
};
type ImageElement = {
type: 'image';
url: string;
children: EmptyText[];
};
const Element = props => {
const { attributes, children, element } = props;
switch (element.type) {
case 'image':
return <Image {...props} />;
default:
return <p {...attributes}>{children}</p>;
}
};
const serialize = node => {
if (Text.isText(node)) {
let string = node.text; // escapeHtml(node.text);
if (node.bold) {
string = `<strong>${string}</strong>`;
}
return string;
}
const children = node.children.map(n => serialize(n)).join('');
switch (node.type) {
case 'paragraph':
return `<p>${children}</p>`;
case 'image':
return `<img src="${node.url}" alt="emoji" />`;
default:
return children;
}
};
export default function ChatTextField(props: Props) {
const { value: originalValue } = props;
const [value, setValue] = useState(originalValue);
const [showEmojis, setShowEmojis] = useState(false);
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
const [editor] = useState(() => withImages(withReact(createEditor())));
const text = useRef(value);
// large is 40px
const size = 'small';
const sendMessage = () => {
@ -30,41 +104,46 @@ export default function ChatTextField(props: Props) {
return;
}
const message = convertToText(value);
const message = serialize(editor);
websocketService.send({ type: MessageType.CHAT, body: message });
setValue('');
// Clear the editor.
Transforms.select(editor, [0, editor.children.length - 1]);
Transforms.delete(editor);
};
const handleChange = evt => {
text.current = evt.target.value;
setValue(evt.target.value);
};
const handleChange = e => {};
const handleKeyDown = event => {
const key = event && event.key;
if (key === 'Enter') {
const onKeyDown = e => {
if (e.key === 'Enter') {
e.preventDefault();
sendMessage();
}
};
const initialValue = [
{
type: 'paragraph',
children: [{ text: originalValue }],
},
];
return (
<div>
<Input.Group compact style={{ display: 'flex', width: '100%', position: 'absolute' }}>
<ContentEditable
style={{ width: '60%', maxHeight: '50px', padding: '5px' }}
html={text.current}
onChange={handleChange}
onKeyDown={e => {
handleKeyDown(e);
}}
<Slate editor={editor} value={initialValue} onChange={handleChange}>
<Editable
onKeyDown={onKeyDown}
renderElement={props => <Element {...props} />}
placeholder="Chat message goes here..."
/>
<Button type="default" ghost title="Emoji" onClick={() => setShowEmojis(!showEmojis)}>
<SmileOutlined style={{ color: 'rgba(0,0,0,.45)' }} />
</Button>
<Button size={size} type="primary" onClick={sendMessage}>
Submit
</Button>
</Input.Group>
</Slate>
<Button type="default" ghost title="Emoji" onClick={() => setShowEmojis(!showEmojis)}>
<SmileOutlined style={{ color: 'rgba(0,0,0,.45)' }} />
</Button>
<Button size={size} type="primary" onClick={sendMessage}>
Submit
</Button>
</div>
);
}

168
web/package-lock.json generated
View file

@ -33,6 +33,8 @@
"react-markdown-editor-lite": "1.3.2",
"react-virtuoso": "^2.10.2",
"recoil": "^0.7.2",
"slate": "^0.78.0",
"slate-react": "^0.79.0",
"ua-parser-js": "1.0.2",
"video.js": "^7.18.1"
},
@ -10813,6 +10815,11 @@
"resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.1.tgz",
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q=="
},
"node_modules/@types/is-hotkey": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
"integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -10859,6 +10866,11 @@
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q=="
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
@ -15461,6 +15473,18 @@
"node": ">=8"
}
},
"node_modules/direction": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
"integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==",
"bin": {
"direction": "cli.js"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -19065,6 +19089,15 @@
"node": ">=0.10.0"
}
},
"node_modules/immer": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
@ -19597,6 +19630,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-hotkey": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz",
"integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ=="
},
"node_modules/is-installed-globally": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@ -28434,6 +28472,52 @@
"node": ">=8"
}
},
"node_modules/slate": {
"version": "0.78.0",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.78.0.tgz",
"integrity": "sha512-VwQ0RafT3JPf9SFrXI02Dh3S4Iz9en7d1nn50C/LJjjqjfgv+a2ORbgWMdYjhycPYldaxJwcI3OpP9D1g4SXEg==",
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
},
"node_modules/slate-react": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.79.0.tgz",
"integrity": "sha512-uPzYArGwHKq4QvpzRvP/DVvsDgB2Zw6x9Som/hJWaydu18r0Vp2vmgHL0Dbc4EU6pUNV6o83XbBJLLEwISzBHQ==",
"dependencies": {
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
"direction": "^1.0.3",
"is-hotkey": "^0.1.6",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.4",
"scroll-into-view-if-needed": "^2.2.20",
"tiny-invariant": "1.0.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
"slate": ">=0.65.3"
}
},
"node_modules/slate-react/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/slate/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -29808,6 +29892,16 @@
"node": ">=0.6.0"
}
},
"node_modules/tiny-invariant": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tlds": {
"version": "1.231.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.231.0.tgz",
@ -39698,6 +39792,11 @@
"resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.1.tgz",
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q=="
},
"@types/is-hotkey": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
"integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ=="
},
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -39744,6 +39843,11 @@
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
"dev": true
},
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q=="
},
"@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
@ -43358,6 +43462,11 @@
"path-type": "^4.0.0"
}
},
"direction": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
"integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ=="
},
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -46160,6 +46269,11 @@
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
"optional": true
},
"immer": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA=="
},
"immutable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
@ -46529,6 +46643,11 @@
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
},
"is-hotkey": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz",
"integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ=="
},
"is-installed-globally": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@ -53282,6 +53401,45 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
},
"slate": {
"version": "0.78.0",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.78.0.tgz",
"integrity": "sha512-VwQ0RafT3JPf9SFrXI02Dh3S4Iz9en7d1nn50C/LJjjqjfgv+a2ORbgWMdYjhycPYldaxJwcI3OpP9D1g4SXEg==",
"requires": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
},
"dependencies": {
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
}
}
},
"slate-react": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.79.0.tgz",
"integrity": "sha512-uPzYArGwHKq4QvpzRvP/DVvsDgB2Zw6x9Som/hJWaydu18r0Vp2vmgHL0Dbc4EU6pUNV6o83XbBJLLEwISzBHQ==",
"requires": {
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
"direction": "^1.0.3",
"is-hotkey": "^0.1.6",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.4",
"scroll-into-view-if-needed": "^2.2.20",
"tiny-invariant": "1.0.6"
},
"dependencies": {
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
}
}
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -54368,6 +54526,16 @@
"setimmediate": "^1.0.4"
}
},
"tiny-invariant": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tlds": {
"version": "1.231.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.231.0.tgz",

View file

@ -36,6 +36,8 @@
"react-markdown-editor-lite": "1.3.2",
"react-virtuoso": "^2.10.2",
"recoil": "^0.7.2",
"slate": "^0.78.0",
"slate-react": "^0.79.0",
"ua-parser-js": "1.0.2",
"video.js": "^7.18.1"
},