/* eslint-disable no-param-reassign */ import React, { ClipboardEventHandler, KeyboardEventHandler, ReactNode, forwardRef, useCallback, useState, } from 'react'; import { Box, Scroll, Text } from 'folds'; import { Descendant, Editor, createEditor } from 'slate'; import { Slate, Editable, withReact, RenderLeafProps, RenderElementProps, RenderPlaceholderProps, } from 'slate-react'; import { withHistory } from 'slate-history'; import { BlockType } from './types'; import { RenderElement, RenderLeaf } from './Elements'; import { CustomElement } from './slate'; import * as css from './Editor.css'; import { toggleKeyboardShortcut } from './keyboard'; // Factory — NOT a module-level constant. Slate's slate-react keeps its // internal NODE_TO_INDEX / NODE_TO_PARENT WeakMaps keyed by object // identity (slate#6016, slate#4850). When two `` // instances share the same array+leaf reference (e.g. channel composer // and thread drawer composer mounted at the same time), the second // mount overwrites the first's WeakMap entry → `findPath` returns the // wrong index → "Unable to find the path for Slate node: {text:''}" // crash on cold-load. Fresh array per call sidesteps the collision. const makeInitialValue = (): CustomElement[] => [ { type: BlockType.Paragraph, children: [{ text: '' }], }, ]; const withInline = (editor: Editor): Editor => { const { isInline } = editor; editor.isInline = (element) => [BlockType.Mention, BlockType.Emoticon, BlockType.Link, BlockType.Command].includes( element.type ) || isInline(element); return editor; }; const withVoid = (editor: Editor): Editor => { const { isVoid } = editor; editor.isVoid = (element) => [BlockType.Mention, BlockType.Emoticon, BlockType.Command].includes(element.type) || isVoid(element); return editor; }; export const useEditor = (): Editor => { const [editor] = useState(() => withInline(withVoid(withReact(withHistory(createEditor()))))); return editor; }; export type EditorChangeHandler = (value: Descendant[]) => void; type CustomEditorProps = { editableName?: string; top?: ReactNode; bottom?: ReactNode; before?: ReactNode; after?: ReactNode; maxHeight?: string; editor: Editor; placeholder?: string; onKeyDown?: KeyboardEventHandler; onKeyUp?: KeyboardEventHandler; onChange?: EditorChangeHandler; onPaste?: ClipboardEventHandler; }; export const CustomEditor = forwardRef( ( { editableName, top, bottom, before, after, maxHeight = '50vh', editor, placeholder, onKeyDown, onKeyUp, onChange, onPaste, }, ref ) => { // Fresh value per CustomEditor instance — see comment on // `makeInitialValue` for why we cannot share a module-level const. // useState ensures we don't recreate the value across renders of // the same editor (Slate would not accept a new array reference // anyway after the editor has mounted). const [initialValue] = useState(makeInitialValue); const renderElement = useCallback( (props: RenderElementProps) => , [] ); const renderLeaf = useCallback((props: RenderLeafProps) => , []); const handleKeydown: KeyboardEventHandler = useCallback( (evt) => { onKeyDown?.(evt); const shortcutToggled = toggleKeyboardShortcut(editor, evt); if (shortcutToggled) evt.preventDefault(); }, [editor, onKeyDown] ); const renderPlaceholder = useCallback( ({ attributes, children }: RenderPlaceholderProps) => ( {/* Inner component to style the actual text position and appearance */} {children} ), [] ); return (
{top} {before && ( {before} )} {after && ( {after} )} {bottom}
); } );