import { css, cx } from '@emotion/css'; import isHotkey from 'is-hotkey'; import React, { useCallback, useMemo } from 'react'; import { createEditor, Editor, Element as SlateElement, Node, Transforms } from 'slate'; import { withHistory } from 'slate-history'; import type { ReactEditor } from 'slate-react'; import { Editable, Slate, useSlate, withReact } from 'slate-react'; import { EllipsisWithTooltip } from '../schema-component/antd/input/EllipsisWithTooltip'; import { Button, Icon, Toolbar } from './components'; const HOTKEYS = { 'mod+b': 'bold', 'mod+i': 'italic', 'mod+u': 'underline', 'mod+`': 'code', }; const LIST_TYPES = ['numbered-list', 'bulleted-list']; const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']; const DEFAULT_VALUE = [ { type: 'paragraph', children: [{ text: '' }], }, ]; export const RichText = (props: any) => { const { value, placeholder = '', className, readOnly, autop = true, ellipsis, onChange } = props; const renderElement = useCallback((props) => , []); const renderLeaf = useCallback((props) => , []); const editor = useMemo(() => withHistory(withReact(createEditor() as ReactEditor)), []); const slateValue = useMemo(() => { editor.children = JSON.parse(JSON.stringify(value || DEFAULT_VALUE)); Editor.normalize(editor, { force: true }); return editor.children; }, [editor, value]); if (readOnly) { const slateContent = ( ); const slatePlainText = serialize(slateValue); const content = ( {ellipsis ? slatePlainText : slateContent} ); return content; } return (
{ for (const hotkey in HOTKEYS) { if (isHotkey(hotkey, event as any)) { event.preventDefault(); const mark = HOTKEYS[hotkey]; toggleMark(editor, mark); } } }} />
); }; const serialize = (nodes) => { return nodes.map((n) => Node.string(n)).join('\n'); }; const toggleBlock = (editor, format) => { const isActive = isBlockActive(editor, format, TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'); const isList = LIST_TYPES.includes(format); Transforms.unwrapNodes(editor, { match: (n: Node) => !Editor.isEditor(n) && SlateElement.isElement(n) && LIST_TYPES.includes((n as any).type) && !TEXT_ALIGN_TYPES.includes(format), split: true, }); let newProperties: Partial; if (TEXT_ALIGN_TYPES.includes(format)) { newProperties = { align: isActive ? undefined : format, }; } else { newProperties = { type: isActive ? 'paragraph' : isList ? 'list-item' : format, }; } Transforms.setNodes(editor, newProperties); if (!isActive && isList) { const block = { type: format, children: [] }; Transforms.wrapNodes(editor, block); } }; const toggleMark = (editor, format) => { const isActive = isMarkActive(editor, format); if (isActive) { Editor.removeMark(editor, format); } else { Editor.addMark(editor, format, true); } }; const isBlockActive = (editor, format, blockType = 'type') => { const { selection } = editor; if (!selection) return false; const [match] = Array.from( Editor.nodes(editor, { at: Editor.unhangRange(editor, selection), match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n[blockType] === format, }), ); return !!match; }; const isMarkActive = (editor, format) => { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; }; const Element = ({ attributes, children, element }) => { const style = { textAlign: element.align }; switch (element.type) { case 'block-quote': return (
{children}
); case 'bulleted-list': return (
    {children}
); case 'heading-one': return (

{children}

); case 'heading-two': return (

{children}

); case 'list-item': return (
  • {children}
  • ); case 'numbered-list': return (
      {children}
    ); default: return (

    {children}

    ); } }; const Leaf = ({ attributes, children, leaf }) => { if (leaf.bold) { children = {children}; } if (leaf.code) { children = {children}; } if (leaf.italic) { children = {children}; } if (leaf.underline) { children = {children}; } return {children}; }; const BlockButton = ({ format, icon }) => { const editor = useSlate(); return ( ); }; const MarkButton = ({ format, icon }) => { const editor = useSlate(); return ( ); };