import { useApolloClient } from "@apollo/react-hooks"; import styled from "@emotion/styled"; import isHotkey from "is-hotkey"; import { inject, observer } from "mobx-react"; import React, { useEffect, useMemo } from "react"; import { createEditor, Editor, Node, Point, Range, Transforms } from "slate"; import { withHistory } from "slate-history"; import { Editable, ReactEditor, Slate, withReact } from "slate-react"; import { StoreProps } from "../../platform/SlideshowStore"; import withPlatform, { PlatformProps } from "../../platform/withPlatform"; import { deserialize, toggleBlock, toggleMark } from "./EditorToolbar"; export enum HOTKEYS { ENTER = "enter", TAB = "tab", SHIFT_TAB = "shift+tab", CMD_B = "mod+b", CMD_I = "mod+i", CMD_U = "mod+u", CMD_ALT_1 = "mod+alt+1", CMD_ALT_2 = "mod+alt+2", CMD_ALT_3 = "mod+alt+3", CMD_H = "mod+h", } const HOTKEY_MAP = { "mod+b": "bold", "mod+i": "italic", "mod+u": "underline", "mod+h": "tag", "mod+alt+1": "heading-one", "mod+alt+2": "heading-two", "mod+alt+3": "heading-three", }; const MARKDOWN = { "*": "list-item", "-": "list-item", "+": "list-item", "#": "heading-one", "##": "heading-two", "###": "heading-three", }; type Shortcut = { keys: string; callback: () => any }; const prefilledText = [{ type: "paragraph", children: [{ text: "" }] }]; type Props = { content: Node[]; setContent: (content: Node[]) => any; shortcuts?: Shortcut[]; } & PlatformProps & StoreProps; function OmniscientEditor(props: Props) { // NEEDS TO BE A REF, trying to keep track of it as state messes up the editor re-renders const isMouseDown = React.useRef(false); const highlightModalDiv = React.useRef(null); const client = useApolloClient(); const editor = useMemo( () => withMarkdown(withHistory(withReact(createEditor()))), [] ); const focusEditor = () => { console.log("FOCUS"); ReactEditor.focus(editor); Transforms.select(editor, Editor.end(editor, [])); }; useEffect(() => { window.addEventListener("editor-focus", focusEditor); return () => { window.removeEventListener("editor-focus", focusEditor); }; }); const editorOnChange = React.useRef(editor.onChange); // Need to have this ref to remember where the editor selection was before the modal opened const editorSelection = React.useRef(editor.selection); return ( { props.setContent(value); }} > { return ; }} renderLeaf={(props: any) => { return ( ); }} onKeyDown={(e) => { if (props.shortcuts) { for (const index in props.shortcuts) { const hotkey = props.shortcuts[index].keys; if (isHotkey(hotkey, e as any)) { handleHotkey(hotkey, props.shortcuts, editor, e); return; } } } for (const hotkey in HOTKEYS) { const keyboardHotkey = (HOTKEYS as any)[hotkey]; if (isHotkey(keyboardHotkey, e as any)) { handleHotkey(keyboardHotkey, props.shortcuts, editor, e); return; } } }} /> ); } export default withPlatform(inject("store")(observer(OmniscientEditor))); // Markdown handling const withMarkdown = (editor: ReactEditor) => { const { deleteBackward, insertText, insertData } = editor; editor.insertText = (text) => { const { selection } = editor; if (text === " " && selection && Range.isCollapsed(selection)) { const { anchor } = selection; const block = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n), }); const path = block ? block[1] : []; const start = Editor.start(editor, path); const range = { anchor, focus: start }; const beforeText = Editor.string(editor, range); const type = (MARKDOWN as any)[beforeText]; if (type) { Transforms.select(editor, range); Transforms.delete(editor); Transforms.setNodes( editor, { type: type, }, { match: (n) => Editor.isBlock(editor, n) } ); if (type === "list-item") { const list = { type: "bulleted-list", children: [] }; Transforms.wrapNodes(editor, list, { match: (n) => n.type === "list-item", }); } return; } } insertText(text); }; editor.deleteBackward = (...args) => { const { selection } = editor; if (selection && Range.isCollapsed(selection)) { const match = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n), }); if (match) { const [block, path] = match; const start = Editor.start(editor, path); if ( block.type === "list-item" && path.length <= 2 && Point.equals(selection.anchor, start) ) { Transforms.setNodes(editor, { type: "paragraph", }); Transforms.liftNodes(editor, { match: (n) => n.type === "paragraph", }); return; } else if ( block.type !== "paragraph" && Point.equals(selection.anchor, start) ) { Transforms.setNodes(editor, { type: "paragraph" }); if (block.type === "list-item") { Transforms.unwrapNodes(editor, { match: (n) => n.type === "bulleted-list", }); } return; } } deleteBackward(...args); } }; editor.insertData = (data) => { const html = data.getData("text/html"); const plaintext = data.getData("text/plain"); if (html) { const parsed = new DOMParser().parseFromString(html, "text/html"); const fragment = deserialize(parsed.body); Transforms.insertFragment(editor, fragment); return; } insertData(data); }; return editor; }; // Hotkey Handling const handleHotkey = ( keypress: string, shortcuts: Shortcut[] | undefined, editor: Editor, event: any // some keyboard event` ) => { // Predefined shortcuts if (shortcuts) { for (const index in shortcuts) { const hotkey = shortcuts[index].keys; const callback = shortcuts[index].callback; if (hotkey === keypress) { callback(); event.preventDefault(); return; } } } // Editor's built in shortcuts switch (keypress) { case HOTKEYS.ENTER: handleEnter(editor, event); return; case HOTKEYS.TAB: handleTab(editor); event.preventDefault(); return; case HOTKEYS.SHIFT_TAB: handleShiftTab(editor); event.preventDefault(); return; case HOTKEYS.CMD_B: case HOTKEYS.CMD_I: case HOTKEYS.CMD_U: toggleMark(HOTKEY_MAP[keypress], editor); event.preventDefault(); return; case HOTKEYS.CMD_ALT_1: case HOTKEYS.CMD_ALT_2: case HOTKEYS.CMD_ALT_3: toggleBlock(HOTKEY_MAP[keypress], editor); event.preventDefault(); return; } }; const handleEnter = (editor: Editor, event: any) => { const { selection } = editor; const match = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n), }); if (match && selection && Range.isCollapsed(selection)) { const [block, path] = match; const start = Editor.start(editor, path); if ( block.type === "list-item" && path.length <= 2 && Point.equals(selection.anchor, start) ) { event.preventDefault(); Transforms.setNodes(editor, { type: "paragraph", }); Transforms.liftNodes(editor, { match: (n) => n.type === "paragraph", }); } else if ( block.type === "list-item" && Point.equals(selection.anchor, start) ) { event.preventDefault(); Transforms.liftNodes(editor, { match: (n) => n.type === "list-item", }); } } }; const handleTab = (editor: Editor) => { const match = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n), }); if (match) { const [block, path] = match; if (block.type === "list-item") { const list = { type: "bulleted-list", children: block.children }; Transforms.wrapNodes(editor, list, { match: (n) => n.type === "list-item", }); } } }; const handleShiftTab = (editor: Editor) => { const match = Editor.above(editor, { match: (n) => Editor.isBlock(editor, n), }); if (match) { const [block, path] = match; if (block.type === "list-item") { if (path.length <= 2) { Transforms.setNodes(editor, { type: "paragraph", }); Transforms.liftNodes(editor, { match: (n) => n.type === "paragraph", }); } else { Transforms.liftNodes(editor, { match: (n) => n.type === "list-item", }); } } } }; const Element = ({ attributes, children, element }: any) => { switch (element.type) { case "block-quote": return
{children}
; case "bulleted-list": return ; case "heading-one": return

{children}

; case "heading-two": return

{children}

; case "heading-three": return

{children}

; case "list-item": return
  • {children}
  • ; case "numbered-list": return
      {children}
    ; default: return

    {children}

    ; } }; const Ul = styled.ul` margin-block-start: 0px; margin-block-end: 0px; list-style-type: disc; `; const P = styled.p` margin-block-start: 0.5em; margin-block-end: 0.5em; `; const Leaf = (props: any) => { const { attributes, leaf, editor, store } = props; let { children } = props; if (leaf.bold) { children = {children}; } if (leaf.code) { children = {children}; } if (leaf.italic) { children = {children}; } if (leaf.underline) { children = {children}; } return {children}; }; const DraftHighlight = styled.span` background-color: gray; `;