import { Button, Card, Stack, Text, Wrap } from '@chakra-ui/react'; import isHotkey from 'is-hotkey'; import React, { KeyboardEvent, MouseEvent, useCallback, useMemo } from 'react'; import { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate'; import { withHistory } from 'slate-history'; import { Editable, RenderElementProps, RenderLeafProps, Slate, useSlate, withReact } from 'slate-react'; import { CustomEditor, CustomElement, CustomElementType, CustomElementWithAlign, CustomTextKey } from '../types'; import { Prose } from './ui/prose'; export function EditorSlate({ initialValue, onChange, readOnly }: { readonly initialValue?: Descendant[]; readonly onChange?: (values: Descendant[]) => void; readonly readOnly?: boolean; }) { return ( <> { if (onChange != null) onChange(values); }} readOnly={readOnly} /> ); } const HOTKEYS: Record = { 'mod+b': 'bold', 'mod+i': 'italic', 'mod+u': 'underline', 'mod+`': 'code' }; const LIST_TYPES = ['numbered-list', 'bulleted-list'] as const; const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify'] as const; type AlignType = (typeof TEXT_ALIGN_TYPES)[number]; type ListType = (typeof LIST_TYPES)[number]; type CustomElementFormat = CustomElementType | AlignType | ListType; function EditorViewport({ initialValue, onChange, readOnly }: { readonly initialValue: Descendant[]; readonly onChange: (values: Descendant[]) => void; readonly readOnly?: boolean; }) { const renderElement = useCallback((props: RenderElementProps) => , []); const renderLeaf = useCallback((props: RenderLeafProps) => , []); const editor = useMemo(() => withHistory(withReact(createEditor())), []); return ( {!readOnly && ( )} ) => { for (const hotkey in HOTKEYS) { if (isHotkey(hotkey, event as any)) { event.preventDefault(); const mark = HOTKEYS[hotkey]; toggleMark(editor, mark); } } }} /> ); } function toggleBlock(editor: CustomEditor, format: CustomElementFormat) { const isActive = isBlockActive(editor, format, isAlignType(format) ? 'align' : 'type'); const isList = isListType(format); Transforms.unwrapNodes(editor, { match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && isListType(n.type) && !isAlignType(format), split: true }); let newProperties: Partial; if (isAlignType(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); } } function toggleMark(editor: CustomEditor, format: CustomTextKey) { const isActive = isMarkActive(editor, format); if (isActive) { Editor.removeMark(editor, format); } else { Editor.addMark(editor, format, true); } } function isBlockActive( editor: CustomEditor, format: CustomElementFormat, blockType: 'type' | 'align' = 'type' ) { const { selection } = editor; if (!selection) return false; const [match] = Array.from( Editor.nodes(editor, { at: Editor.unhangRange(editor, selection), match: n => { if (!Editor.isEditor(n) && SlateElement.isElement(n)) { if (blockType === 'align' && isAlignElement(n)) { return n.align === format; } return n.type === format; } return false; } }) ); return Boolean(match); } function isMarkActive(editor: CustomEditor, format: CustomTextKey) { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; } function Element({ attributes, children, element }: RenderElementProps) { const style: React.CSSProperties = {}; if (isAlignElement(element)) { style.textAlign = element.align as AlignType; } 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}

    ); } } function Leaf({ attributes, children, leaf }: RenderLeafProps) { if (leaf.bold) { children = {children}; } if (leaf.code) { children = {children}; } if (leaf.italic) { children = {children}; } if (leaf.underline) { children = {children}; } return {children}; } interface BlockButtonProps { format: CustomElementFormat; icon: string; text?: string; } function BlockButton({ format, icon, text }: BlockButtonProps) { const editor = useSlate(); return ( ); } interface MarkButtonProps { format: CustomTextKey; icon: string; text?: string; } function MarkButton({ format, icon, text }: MarkButtonProps) { const editor = useSlate(); return ( ); } function isAlignType(format: CustomElementFormat): format is AlignType { return TEXT_ALIGN_TYPES.includes(format as AlignType); } function isListType(format: CustomElementFormat): format is ListType { return LIST_TYPES.includes(format as ListType); } function isAlignElement(element: CustomElement): element is CustomElementWithAlign { return 'align' in element; }