slate#NodeEntry TypeScript Examples

The following examples show how to use slate#NodeEntry. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: code-block-decorate.ts    From fantasy-editor with MIT License 6 votes vote down vote up
codeBlockDecorate = (entry: NodeEntry, editor: Editor) => {
  const ranges: any = [];
  const [node, path] = entry;
  if (node.type === BLOCK_CODE) {
    const text = Node.string(node);
    const langName: any = node.lang || 'markup';
    const lang = languages[langName];
    if (lang) {
      const tokens = tokenize(text, lang);
      let offset = 0;
      const {ranges: r} = buildRange(tokens, path, offset);
      ranges.push(...r);
    }
  }
  return ranges;
}
Example #2
Source File: on-table-keydown.ts    From fantasy-editor with MIT License 6 votes vote down vote up
getLastNode = (editor: Editor, lastPath: Path): NodeEntry => {
  let i = lastPath.length;
  while (i > 0) {
    const path = lastPath.slice(0, i);
    const node = Editor.node(editor, path);
    if (!!node[0].type) {
      return node;
    }
    i--;
  }
  return Editor.node(editor, lastPath.slice(0, 1));
}
Example #3
Source File: decorate-plugins.ts    From fantasy-editor with MIT License 6 votes vote down vote up
decoratePlugins = (editor: Editor, plugins: SlatePlugin[], decorateList: Decorate[]) => (
  entry: NodeEntry,
) => {
  let ranges: Range[] = [];

  const addRanges = (newRanges: Range[]) => {
    if (newRanges.length) {
      ranges = [...ranges, ...newRanges];
    }
  };

  decorateList.forEach(decorate => {
    addRanges(decorate(entry, editor));
  });

  plugins.forEach(({ decorate }) => {
    decorate && addRanges(decorate(entry, editor));
  });

  return ranges;
}
Example #4
Source File: get-last-node.ts    From fantasy-editor with MIT License 6 votes vote down vote up
getLastNode = (editor: Editor, level: number): NodeEntry => {
  const [, lastPath] = Editor.last(editor, []);
  let i = lastPath.length;
  while (i > 0) {
    const path = lastPath.slice(0, i);
    const node = Editor.node(editor, path);
    if (!!node[0].type) {
      return node;
    }
    i--;
  }
  return Editor.node(editor, lastPath.slice(0, 1));
}
Example #5
Source File: MessageFormatEditor.tsx    From project-loved-web with MIT License 6 votes vote down vote up
function decorate([node, path]: NodeEntry): Range[] {
  if (!Text.isText(node)) {
    return [];
  }

  const ranges: Range[] = [];

  try {
    addRanges(ranges, path, parse(node.text, { captureLocation: true }));
  } catch {}

  return ranges;
}
Example #6
Source File: isListActive.ts    From payload with MIT License 6 votes vote down vote up
isListActive = (editor: Editor, format: string): boolean => {
  let parentLI: NodeEntry<Ancestor>;

  try {
    parentLI = Editor.parent(editor, editor.selection);
  } catch (e) {
    // swallow error, Slate
  }

  if (parentLI?.[1]?.length > 0) {
    const ancestor = Editor.above(editor, {
      at: parentLI[1],
    });

    return Element.isElement(ancestor[0]) && ancestor[0].type === format;
  }

  return false;
}
Example #7
Source File: Editor.tsx    From react-editor-kit with MIT License 6 votes vote down vote up
handleDecorate = (
  entry: NodeEntry,
  plugins: Plugin[],
  editor: ReactEditor
) => {
  let ranges: Range[] = [];
  for (let plugin of plugins) {
    if (plugin.decorate) {
      const result = plugin.decorate(entry, editor);
      if (result) {
        return (ranges = ranges.concat(result));
      }
    }
  }
  return ranges;
}
Example #8
Source File: CodeHighlighter.ts    From react-editor-kit with MIT License 6 votes vote down vote up
highlightCode = ([node, path]: NodeEntry, language: string) => {
  const ranges: Range[] = [];
  let offset = 0;
  let string = Node.string(node);
  const grammar = Prism.languages[Languages[language]];
  const tokens = Prism.tokenize(string, grammar);
  for (const token of tokens) {
    if (typeof token === "string") {
      offset += token.length;
      continue;
    }
    const text = getContent(token);
    offset += text.length;
    ranges.push({
      anchor: { path, offset: offset - text.length },
      focus: { path, offset: offset },
      codeRange: true,
      type: token.type
    });
  }
  return ranges;
}
Example #9
Source File: advanced-editable.component.ts    From slate-angular with MIT License 5 votes vote down vote up
decorate = (nodeEntry: NodeEntry) => [];
Example #10
Source File: editable.component.ts    From slate-angular with MIT License 5 votes vote down vote up
@Input() decorate: (entry: NodeEntry) => Range[] = () => [];
Example #11
Source File: search-highlighting.component.ts    From slate-angular with MIT License 5 votes vote down vote up
decorate: (nodeEntry: NodeEntry) => Range[];
Example #12
Source File: CodePlugin.tsx    From react-editor-kit with MIT License 5 votes vote down vote up
createCodePlugin = (
  options: CodePluginOptions = { hideToolbar: false }
): Code => {
  return {
    name: "code",
    ...options,
    withPlugin: (editor) => {
      const { insertData } = editor;
      editor.insertData = (data) => {
        const type = getActiveNodeType(editor);
        if (type === "code") {
          const text = data.getData("text/plain");
          if (text) {
            editor.insertText(text);
            return;
          }
        }
        insertData(data);
      };
      return editor;
    },
    triggers: [CodeMarkdownTrigger, CodeNamedTrigger],
    actions: [CodePluginAction],
    onKey: [
      CodeTabKeyHandler,
      CodeDeleteKeyHandler,
      CodeBackspaceKeyHandler,
      CodeEnterKeyHandler,
    ],
    renderLeaf: (props: RenderLeafProps) => {
      const { attributes, children, leaf } = props;
      if (leaf.codeRange) {
        return (
          <span {...attributes} className={`rek-code-${leaf.type}`}>
            {children}
          </span>
        );
      }
    },
    renderElement: (props: RenderElementProps) => {
      if (props.element.type === "code") {
        return <CodeElement {...props} />;
      }
      return undefined;
    },
    decorate: (entry: NodeEntry, editor: ReactEditor) => {
      if (entry[1].length) {
        const [parent] = Editor.parent(editor, entry[1]);
        if (parent && parent.type === "code") {
          return highlightCode(entry, parent.lang);
        }
      }
      return [];
    },
    editorStyle: CodeEditorStyle,
    globalStyle: CodeGlobalStyle,
  };
}
Example #13
Source File: CodeHighlighter.d.ts    From react-editor-kit with MIT License 5 votes vote down vote up
highlightCode: ([node, path]: NodeEntry<Node>, language: string) => Range[]
Example #14
Source File: runtime-util.ts    From slate-vue with MIT License 5 votes vote down vote up
runtimeNode = {
  child(root: Node, index: number): Descendant  {
    if (Text.isText(root)) {
      throw new Error(
        `Cannot get the child of a text node: ${JSON.stringify(root)}`
      )
    }

    const c = getChildren(root)[index] as Descendant

    if (c == null) {
      throw new Error(
        `Cannot get child at index \`${index}\` in node: ${JSON.stringify(
          root
        )}`
      )
    }

    return c
  },
  has(root: Node, path: Path): boolean {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)
      if (Text.isText(node) || !children[p]) {
        return false
      }

      node = children[p]
    }

    return true
  },
  get(root: Node, path: Path): Node {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)

      if (Text.isText(node) || !children[p]) {
        throw new Error(
          `Cannot find a descendant at path [${path}] in node: ${JSON.stringify(
            root
          )}`
        )
      }

      node = children[p]
    }

    return node
  },
  first(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        n = children[0]
        p.push(0)
      }
    }

    return [n, p]
  },
  last(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        const i = children.length - 1
        n = children[i]
        p.push(i)
      }
    }

    return [n, p]
  },
  *nodes(
    root: Node,
    options: {
      from?: Path
      to?: Path
      reverse?: boolean
      pass?: (entry: NodeEntry) => boolean
    } = {}
  ): Generator<NodeEntry> {
    const { pass, reverse = false } = options
    const { from = [], to } = options
    const visited = new Set()
    let p: Path = []
    let n = root

    while (true) {
      if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) {
        break
      }

      if (!visited.has(n)) {
        yield [n, p]
      }

      // If we're allowed to go downward and we haven't decsended yet, do.
      if (
        !visited.has(n) &&
        !Text.isText(n) &&
        getChildren(n).length !== 0 &&
        (pass == null || pass([n, p]) === false)
      ) {
        visited.add(n)
        let nextIndex = reverse ? getChildren(n).length - 1 : 0

        if (Path.isAncestor(p, from)) {
          nextIndex = from[p.length]
        }

        p = p.concat(nextIndex)
        n = Node.get(root, p)
        continue
      }

      // If we're at the root and we can't go down, we're done.
      if (p.length === 0) {
        break
      }

      // If we're going forward...
      if (!reverse) {
        const newPath = Path.next(p)

        if (Node.has(root, newPath)) {
          p = newPath
          n = Node.get(root, p)
          continue
        }
      }

      // If we're going backward...
      if (reverse && p[p.length - 1] !== 0) {
        const newPath = Path.previous(p)
        p = newPath
        n = Node.get(root, p)
        continue
      }

      // Otherwise we're going upward...
      p = Path.parent(p)
      n = Node.get(root, p)
      visited.add(n)
    }
  }
}
Example #15
Source File: runtime-util.ts    From slate-vue with MIT License 5 votes vote down vote up
runtimeNode = {
  child(root: Node, index: number): Descendant  {
    if (Text.isText(root)) {
      throw new Error(
        `Cannot get the child of a text node: ${JSON.stringify(root)}`
      )
    }

    const c = getChildren(root)[index] as Descendant

    if (c == null) {
      throw new Error(
        `Cannot get child at index \`${index}\` in node: ${JSON.stringify(
          root
        )}`
      )
    }

    return c
  },
  has(root: Node, path: Path): boolean {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)
      if (Text.isText(node) || !children[p]) {
        return false
      }

      node = children[p]
    }

    return true
},
  get(root: Node, path: Path): Node {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)

      if (Text.isText(node) || !children[p]) {
        throw new Error(
          `Cannot find a descendant at path [${path}] in node: ${JSON.stringify(
            root
          )}`
        )
      }

      node = children[p]
    }

    return node
  },
  first(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        n = children[0]
        p.push(0)
      }
    }

    return [n, p]
  },
  last(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        const i = children.length - 1
        n = children[i]
        p.push(i)
      }
    }

    return [n, p]
  },
  *nodes(
    root: Node,
    options: {
      from?: Path
      to?: Path
      reverse?: boolean
      pass?: (entry: NodeEntry) => boolean
    } = {}
  ): Generator<NodeEntry> {
    const { pass, reverse = false } = options
    const { from = [], to } = options
    const visited = new Set()
    let p: Path = []
    let n = root

    while (true) {
      if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) {
        break
      }

      if (!visited.has(n)) {
        yield [n, p]
      }

      // If we're allowed to go downward and we haven't decsended yet, do.
      if (
        !visited.has(n) &&
        !Text.isText(n) &&
        getChildren(n).length !== 0 &&
        (pass == null || pass([n, p]) === false)
      ) {
        visited.add(n)
        let nextIndex = reverse ? getChildren(n).length - 1 : 0

        if (Path.isAncestor(p, from)) {
          nextIndex = from[p.length]
        }

        p = p.concat(nextIndex)
        n = Node.get(root, p)
        continue
      }

      // If we're at the root and we can't go down, we're done.
      if (p.length === 0) {
        break
      }

      // If we're going forward...
      if (!reverse) {
        const newPath = Path.next(p)

        if (Node.has(root, newPath)) {
          p = newPath
          n = Node.get(root, p)
          continue
        }
      }

      // If we're going backward...
      if (reverse && p[p.length - 1] !== 0) {
        const newPath = Path.previous(p)
        p = newPath
        n = Node.get(root, p)
        continue
      }

      // Otherwise we're going upward...
      p = Path.parent(p)
      n = Node.get(root, p)
      visited.add(n)
    }
  }
}
Example #16
Source File: Editor.tsx    From react-editor-kit with MIT License 4 votes vote down vote up
Editor = memo((props: EditorProps) => {
  const { value, onChange, className, style, ...rest } = props;
  const {
    editor,
    plugins,
    spellCheck,
    delaySpellCheck,
    readOnly,
    id,
    onClick,
  } = useEditorKit();
  const [menu, setMenu] = useState<{
    items: ReactNode[];
    x: number;
    y: number;
  }>({ items: [], x: 0, y: 0 });
  useLastFocused(editor);

  const createState = () => {
    const { selection } = editor;
    const point = selection ? selection.focus : undefined;
    const [element] = point ? SlateEditor.parent(editor, point) : [];

    return createEditorState({ selection, point, element }, editor);
  };

  const renderElement = useCallback(
    (props: RenderElementProps) => handleRenderElement(props, plugins),
    [plugins]
  );
  const renderLeaf = useCallback(
    (props: RenderLeafProps) => handleRenderLeaf(props, plugins, createState()),
    [plugins]
  );

  const decorate = useCallback(
    (entry: NodeEntry) => handleDecorate(entry, plugins, editor),
    [plugins]
  );

  const keyDown = useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      delaySpellCheck();
      handleCloseMenu();
      handleKeyDown(event, plugins, createState());
    },
    [spellCheck, plugins]
  );

  const keyUp = useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      handleKeyUp(event, plugins, createState());
    },
    [plugins]
  );

  const click = useCallback(
    (event: React.MouseEvent<HTMLElement>) => {
      onClick();
      handleCloseMenu();
      handleClick(event, plugins, createState());
    },
    [plugins]
  );

  const contextMenu = useCallback(
    (event: React.MouseEvent) => {
      const items = handleContextMenu(event, plugins, createState());
      const x = event.clientX;
      const y = event.clientY;

      if (items.length) {
        event.stopPropagation();
        event.preventDefault();
      }
      setMenu({ items, x, y });
    },
    [plugins]
  );

  const drop = useCallback(
    (event: React.DragEvent) => {
      handleDrop(event, plugins, createState());
    },
    [plugins]
  );

  const handleCloseMenu = useCallback(() => {
    if (menu.items.length) {
      setMenu({ items: [], x: 0, y: 0 });
    }
  }, [menu]);

  return (
    <Slate editor={editor} value={ensureValue(value)} onChange={onChange}>
      <Fragment>
        <Editable
          renderElement={renderElement}
          renderLeaf={renderLeaf}
          decorate={decorate}
          onKeyDown={keyDown}
          onKeyUp={keyUp}
          onClick={click}
          onContextMenu={contextMenu}
          style={style}
          spellCheck={spellCheck}
          readOnly={readOnly}
          onDrop={drop}
          id={`${id}`}
          {...rest}
        />
        <Show when={menu.items.length}>
          <ContextMenu {...menu} onClose={handleCloseMenu} />
        </Show>
      </Fragment>
    </Slate>
  );
})
Example #17
Source File: Plugin.d.ts    From react-editor-kit with MIT License 4 votes vote down vote up
createPlugin: (plugin: Plugin, ...triggers: Trigger[]) => {
    triggers: Trigger[];
    /**
     * A name means your plugin can be called inside of React components
     * that existing under the <EditorKit> context scope using usePlugin("name").
     *
     * Names also allow you to replace internal plugins with your own. When you
     * register your plugins to <EditorKit plugins={[...]}>, the library will
     * check the names of each if it finds a name of an internal plugin then it will
     * swap the internal plugin with the plugin you registered. This allows you to
     * inject your own icons into Editor Kit by providing a plugin named "icon-provider",
     * for example.
     */
    name: string;
    /**
     * The order the Plugin is registered is important if you listen on events
     * that can only be handled by a single plugin. For example, EditorKit will
     * return early onKeyDown once a Plugin returns true from onKeyDown.
     *
     * Order can also be important for Plugins that contribute CSS (see below)
     * due to CSS cascading behaviour.
     *
     * Default order is the position in the Plugin array passed to EditorKit.
     *
     */
    order?: number;
    /**
     * Extend the Slate editor with additional functionality or
     * wrap existing functions like normalizeNode
     */
    withPlugin?(editor: ReactEditor): ReactEditor;
    /**
     * Array of actions that plugin provides. A name is required for each
     * if more than one is available.
     */
    actions?: PluginAction[];
    /**
     * Contribute style to an Element without having to render it. This avoids
     * having to create unnecessary wrapper Elements just to change things like
     * text-align of existing Elements.
     */
    styleElement?: (props: RenderElementProps) => CSSProperties | undefined;
    /**
     * Contribute classes to an Element without having to render it.
     * Similar to `styleElement` above.
     */
    getClasses?: (element: Element) => string | undefined;
    /**
     * Render an Element into the Editor. You can output any JSX content you like
     * here according to the `props.element.type`
     *
     * The is the same as Slate's renderElement function and requires that
     * you spread `props.attributes` on your root element and pass
     * the `props.children` prop to your lowest JSX element.
     */
    renderElement?: (props: RenderElementProps) => JSX.Element | undefined;
    /**
     * Render an Leaf into the Editor. You can output any JSX content you like
     * here according to the `props.leaf.type`. This Leaf content will become the
     * child of an Element that gets rendered by renderElement above.
     *
     * The is the same as Slate's renderLeaf function and requires that
     * you spread `props.attributes` on your root element and pass
     * the `props.children` prop to your lowest JSX element.
     */
    renderLeaf?: (props: RenderLeafProps, state: EditorState) => JSX.Element | undefined;
    /**
     *
     */
    decorate?: (entry: NodeEntry<import("slate").Node>, editor: ReactEditor) => Range[];
    /**
     * Similar to triggers but typical used for keyboard shortcuts or modifying the
     * Editors handling of delete/backspace or enter for certain elements
     */
    onKey?: KeyHandler[];
    /**
     * Respond to key down events on the Editor element. Use onKey if you are interested in a certain
     * key or key combo.
     */
    onKeyDown?(event: React.KeyboardEvent, state: EditorState): boolean | undefined;
    /**
     * Respond to click events that happen on the Editor element.
     */
    onClick?(event: React.MouseEvent<HTMLElement>, state: EditorState): void;
    /**
     * Allows contributing to the context menu and overriding the browser's default.
     * See ContextMenuContribution for more info.
     */
    onContextMenu?: ContextMenuContribution[];
    /**
     * Respond to drop events that happen on the Editor element.
     */
    onDrop?(event: React.DragEvent, state: EditorState): boolean;
    /**
     * Styles that should be applied to the Editor instance. EditorKit uses the npm
     * package stylis and supports nested selectors like:
     *
     *  .someClass {
     *    &:hover {
     *
     *    }
     *  }
     *
     */
    editorStyle?: string | StyleFunction;
    /**
     * These styles can target the whole page so be careful. Things like dialogs and toolbars
     * are rendered outside of the Editor and can be targeted here.
     */
    globalStyle?: string | StyleFunction;
}
Example #18
Source File: angular-editor.ts    From slate-angular with MIT License 4 votes vote down vote up
AngularEditor = {
    /**
     * Return the host window of the current editor.
     */

    getWindow(editor: AngularEditor): Window {
        const window = EDITOR_TO_WINDOW.get(editor);
        if (!window) {
            throw new Error('Unable to find a host window element for this editor');
        }
        return window;
    },
    /**
     * Find a key for a Slate node.
     */

    findKey(editor: AngularEditor, node: Node): Key {
        let key = NODE_TO_KEY.get(node);

        if (!key) {
            key = new Key();
            NODE_TO_KEY.set(node, key);
        }

        return key;
    },

    /**
     * handle editor error.
     */

    onError(errorData: SlateError) {
        if (errorData.nativeError) {
            throw errorData.nativeError;
        }
    },

    /**
     * Find the path of Slate node.
     */

    findPath(editor: AngularEditor, node: Node): Path {
        const path: Path = [];
        let child = node;

        while (true) {
            const parent = NODE_TO_PARENT.get(child);

            if (parent == null) {
                if (Editor.isEditor(child)) {
                    return path;
                } else {
                    break;
                }
            }

            const i = NODE_TO_INDEX.get(child);

            if (i == null) {
                break;
            }

            path.unshift(i);
            child = parent;
        }
        throw new Error(`Unable to find the path for Slate node: ${JSON.stringify(node)}`);
    },

    /**
     * Find the DOM node that implements DocumentOrShadowRoot for the editor.
     */

    findDocumentOrShadowRoot(editor: AngularEditor): Document | ShadowRoot {
        const el = AngularEditor.toDOMNode(editor, editor)
        const root = el.getRootNode()
        if (
            (root instanceof Document || root instanceof ShadowRoot) &&
            (root as Document).getSelection != null
        ) {
            return root
        }

        return el.ownerDocument
    },

    /**
     * Check if the editor is focused.
     */

    isFocused(editor: AngularEditor): boolean {
        return !!IS_FOCUSED.get(editor);
    },

    /**
     * Check if the editor is in read-only mode.
     */

    isReadonly(editor: AngularEditor): boolean {
        return !!IS_READONLY.get(editor);
    },

    /**
     * Check if the editor is hanging right.
     */
    isBlockHangingRight(editor: AngularEditor): boolean {
        const { selection } = editor;
        if (!selection) {
            return false;
        }
        if (Range.isCollapsed(selection)) {
            return false;
        }
        const [start, end] = Range.edges(selection);
        const endBlock = Editor.above(editor, { at: end, match: (node) => Editor.isBlock(editor, node) });
        return Editor.isStart(editor, end, endBlock[1]);
    },

    /**
     * Blur the editor.
     */

    blur(editor: AngularEditor): void {
        const el = AngularEditor.toDOMNode(editor, editor);
        const root = AngularEditor.findDocumentOrShadowRoot(editor);
        IS_FOCUSED.set(editor, false);

        if (root.activeElement === el) {
            el.blur();
        }
    },

    /**
     * Focus the editor.
     */

    focus(editor: AngularEditor): void {
        const el = AngularEditor.toDOMNode(editor, editor);
        IS_FOCUSED.set(editor, true);

        const window = AngularEditor.getWindow(editor);
        if (window.document.activeElement !== el) {
            el.focus({ preventScroll: true });
        }
    },

    /**
     * Deselect the editor.
     */

    deselect(editor: AngularEditor): void {
        const { selection } = editor;
        const root = AngularEditor.findDocumentOrShadowRoot(editor);
        const domSelection = (root as Document).getSelection();

        if (domSelection && domSelection.rangeCount > 0) {
            domSelection.removeAllRanges();
        }

        if (selection) {
            Transforms.deselect(editor);
        }
    },

    /**
     * Check if a DOM node is within the editor.
     */

    hasDOMNode(editor: AngularEditor, target: DOMNode, options: { editable?: boolean } = {}): boolean {
        const { editable = false } = options;
        const editorEl = AngularEditor.toDOMNode(editor, editor);
        let targetEl;

        // COMPAT: In Firefox, reading `target.nodeType` will throw an error if
        // target is originating from an internal "restricted" element (e.g. a
        // stepper arrow on a number input). (2018/05/04)
        // https://github.com/ianstormtaylor/slate/issues/1819
        try {
            targetEl = (isDOMElement(target) ? target : target.parentElement) as HTMLElement;
        } catch (err) {
            if (!err.message.includes('Permission denied to access property "nodeType"')) {
                throw err;
            }
        }

        if (!targetEl) {
            return false;
        }

        return targetEl.closest(`[data-slate-editor]`) === editorEl &&
            (!editable || targetEl.isContentEditable ||
                !!targetEl.getAttribute('data-slate-zero-width'));
    },

    /**
     * Insert data from a `DataTransfer` into the editor.
     */

    insertData(editor: AngularEditor, data: DataTransfer): void {
        editor.insertData(data);
    },

    /**
   * Insert fragment data from a `DataTransfer` into the editor.
   */

    insertFragmentData(editor: AngularEditor, data: DataTransfer): boolean {
        return editor.insertFragmentData(data)
    },

    /**
     * Insert text data from a `DataTransfer` into the editor.
     */

    insertTextData(editor: AngularEditor, data: DataTransfer): boolean {
        return editor.insertTextData(data)
    },

    /**
     * onKeydown hook.
     */
    onKeydown(editor: AngularEditor, data: KeyboardEvent): void {
        editor.onKeydown(data);
    },

    /**
    * onClick hook.
    */
    onClick(editor: AngularEditor, data: MouseEvent): void {
        editor.onClick(data);
    },

    /**
     * Sets data from the currently selected fragment on a `DataTransfer`.
     */

    setFragmentData(editor: AngularEditor, data: DataTransfer, originEvent?: 'drag' | 'copy' | 'cut'): void {
        editor.setFragmentData(data, originEvent);
    },

    deleteCutData(editor: AngularEditor): void {
        editor.deleteCutData();
    },

    /**
     * Find the native DOM element from a Slate node.
     */

    toDOMNode(editor: AngularEditor, node: Node): HTMLElement {
        const domNode = Editor.isEditor(node)
            ? EDITOR_TO_ELEMENT.get(editor)
            : NODE_TO_ELEMENT.get(node);

        if (!domNode) {
            throw new Error(`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`);
        }

        return domNode;
    },

    /**
     * Find a native DOM selection point from a Slate point.
     */

    toDOMPoint(editor: AngularEditor, point: Point): DOMPoint {
        const [node] = Editor.node(editor, point.path);
        const el = AngularEditor.toDOMNode(editor, node);
        let domPoint: DOMPoint | undefined;

        // block card
        const cardTargetAttr = getCardTargetAttribute(el);
        if (cardTargetAttr) {
            if (point.offset === FAKE_LEFT_BLOCK_CARD_OFFSET) {
                const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'left' });
                return [cursorNode, 1];
            } else {
                const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'right' });
                return [cursorNode, 1];
            }
        }

        // If we're inside a void node, force the offset to 0, otherwise the zero
        // width spacing character will result in an incorrect offset of 1
        if (Editor.void(editor, { at: point })) {
            point = { path: point.path, offset: 0 };
        }

        // For each leaf, we need to isolate its content, which means filtering
        // to its direct text and zero-width spans. (We have to filter out any
        // other siblings that may have been rendered alongside them.)
        const selector = `[data-slate-string], [data-slate-zero-width]`;
        const texts = Array.from(el.querySelectorAll(selector));
        let start = 0;

        for (const text of texts) {
            const domNode = text.childNodes[0] as HTMLElement;

            if (domNode == null || domNode.textContent == null) {
                continue;
            }

            const { length } = domNode.textContent;
            const attr = text.getAttribute('data-slate-length');
            const trueLength = attr == null ? length : parseInt(attr, 10);
            const end = start + trueLength;

            if (point.offset <= end) {
                const offset = Math.min(length, Math.max(0, point.offset - start));
                domPoint = [domNode, offset];
                // fixed cursor position after zero width char
                if (offset === 0 && length === 1 && domNode.textContent === '\uFEFF') {
                    domPoint = [domNode, offset + 1];
                }
                break;
            }

            start = end;
        }

        if (!domPoint) {
            throw new Error(`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`);
        }

        return domPoint;
    },

    /**
     * Find a native DOM range from a Slate `range`.
     */

    toDOMRange(editor: AngularEditor, range: Range): DOMRange {
        const { anchor, focus } = range;
        const isBackward = Range.isBackward(range);
        const domAnchor = AngularEditor.toDOMPoint(editor, anchor);
        const domFocus = Range.isCollapsed(range) ? domAnchor : AngularEditor.toDOMPoint(editor, focus);

        const window = AngularEditor.getWindow(editor);
        const domRange = window.document.createRange();
        const [startNode, startOffset] = isBackward ? domFocus : domAnchor;
        const [endNode, endOffset] = isBackward ? domAnchor : domFocus;

        // A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
        // zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
        // adjust the offset accordingly.
        const startEl = (isDOMElement(startNode)
            ? startNode
            : startNode.parentElement) as HTMLElement;
        const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
        const endEl = (isDOMElement(endNode)
            ? endNode
            : endNode.parentElement) as HTMLElement;
        const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');

        domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset);
        domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset);
        return domRange;
    },

    /**
     * Find a Slate node from a native DOM `element`.
     */

    toSlateNode(editor: AngularEditor, domNode: DOMNode): Node {
        let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;

        if (domEl && !domEl.hasAttribute('data-slate-node')) {
            domEl = domEl.closest(`[data-slate-node]`);
        }

        const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null;

        if (!node) {
            throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`);
        }

        return node;
    },

    /**
     * Get the target range from a DOM `event`.
     */

    findEventRange(editor: AngularEditor, event: any): Range {
        if ('nativeEvent' in event) {
            event = event.nativeEvent;
        }

        const { clientX: x, clientY: y, target } = event;

        if (x == null || y == null) {
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
        }

        const node = AngularEditor.toSlateNode(editor, event.target);
        const path = AngularEditor.findPath(editor, node);

        // If the drop target is inside a void node, move it into either the
        // next or previous node, depending on which side the `x` and `y`
        // coordinates are closest to.
        if (Editor.isVoid(editor, node)) {
            const rect = target.getBoundingClientRect();
            const isPrev = editor.isInline(node)
                ? x - rect.left < rect.left + rect.width - x
                : y - rect.top < rect.top + rect.height - y;

            const edge = Editor.point(editor, path, {
                edge: isPrev ? 'start' : 'end'
            });
            const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge);

            if (point) {
                return Editor.range(editor, point);
            }
        }

        // Else resolve a range from the caret position where the drop occured.
        let domRange: DOMRange;
        const window = AngularEditor.getWindow(editor);
        const { document } = window;

        // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
        if (document.caretRangeFromPoint) {
            domRange = document.caretRangeFromPoint(x, y);
        } else {
            const position = (document as SafeAny).caretPositionFromPoint(x, y);

            if (position) {
                domRange = document.createRange();
                domRange.setStart(position.offsetNode, position.offset);
                domRange.setEnd(position.offsetNode, position.offset);
            }
        }

        if (!domRange) {
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
        }

        // Resolve a Slate range from the DOM range.
        const range = AngularEditor.toSlateRange(editor, domRange);
        return range;
    },

    /**
     * Find a Slate point from a DOM selection's `domNode` and `domOffset`.
     */

    toSlatePoint(editor: AngularEditor, domPoint: DOMPoint): Point {
        const [domNode] = domPoint;
        const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint);
        let parentNode = nearestNode.parentNode as DOMElement;
        let textNode: DOMElement | null = null;
        let offset = 0;

        // block card
        const cardTargetAttr = getCardTargetAttribute(domNode);
        if (cardTargetAttr) {
            const domSelection = window.getSelection();
            const isBackward = editor.selection && Range.isBackward(editor.selection);
            const blockCardEntry = AngularEditor.toSlateCardEntry(editor, domNode) || AngularEditor.toSlateCardEntry(editor, nearestNode);
            const [, blockPath] = blockCardEntry;
            if (domSelection.isCollapsed) {
                if (isCardLeftByTargetAttr(cardTargetAttr)) {
                    return { path: blockPath, offset: -1 };
                }
                else {
                    return { path: blockPath, offset: -2 };
                }
            }
            // forward
            // and to the end of previous node
            if (isCardLeftByTargetAttr(cardTargetAttr) && !isBackward) {
                const endPath =
                    blockPath[blockPath.length - 1] <= 0
                        ? blockPath
                        : Path.previous(blockPath);
                return Editor.end(editor, endPath);
            }
            // to the of current node
            if (
                (isCardCenterByTargetAttr(cardTargetAttr) ||
                    isCardRightByTargetAttr(cardTargetAttr)) &&
                !isBackward
            ) {
                return Editor.end(editor, blockPath);
            }
            // backward
            // and to the start of next node
            if (isCardRightByTargetAttr(cardTargetAttr) && isBackward) {
                return Editor.start(editor, Path.next(blockPath));
            }
            // and to the start of current node
            if (
                (isCardCenterByTargetAttr(cardTargetAttr) ||
                    isCardLeftByTargetAttr(cardTargetAttr)) &&
                isBackward
            ) {
                return Editor.start(editor, blockPath);
            }
        }

        if (parentNode) {
            const voidNode = parentNode.closest('[data-slate-void="true"]');
            let leafNode = parentNode.closest('[data-slate-leaf]');
            let domNode: DOMElement | null = null;

            // Calculate how far into the text node the `nearestNode` is, so that we
            // can determine what the offset relative to the text node is.
            if (leafNode) {
                textNode = leafNode.closest('[data-slate-node="text"]')!;
                const window = AngularEditor.getWindow(editor);
                const range = window.document.createRange();
                range.setStart(textNode, 0);
                range.setEnd(nearestNode, nearestOffset);
                const contents = range.cloneContents();
                const removals = [
                    ...Array.prototype.slice.call(
                        contents.querySelectorAll('[data-slate-zero-width]')
                    ),
                    ...Array.prototype.slice.call(
                        contents.querySelectorAll('[contenteditable=false]')
                    ),
                ];

                removals.forEach(el => {
                    el!.parentNode!.removeChild(el);
                });

                // COMPAT: Edge has a bug where Range.prototype.toString() will
                // convert \n into \r\n. The bug causes a loop when slate-react
                // attempts to reposition its cursor to match the native position. Use
                // textContent.length instead.
                // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
                offset = contents.textContent!.length;
                domNode = textNode;
            } else if (voidNode) {
                // For void nodes, the element with the offset key will be a cousin, not an
                // ancestor, so find it by going down from the nearest void parent.

                leafNode = voidNode.querySelector('[data-slate-leaf]')!;
                parentNode = voidNode.querySelector('[data-slate-length="0"]');
                textNode = leafNode.closest('[data-slate-node="text"]')!;
                domNode = leafNode;
                offset = domNode.textContent!.length;
            }

            // COMPAT: If the parent node is a Slate zero-width space, editor is
            // because the text node should have no characters. However, during IME
            // composition the ASCII characters will be prepended to the zero-width
            // space, so subtract 1 from the offset to account for the zero-width
            // space character.
            if (domNode &&
                offset === domNode.textContent!.length &&
                (parentNode && parentNode.hasAttribute('data-slate-zero-width'))
            ) {
                offset--;
            }
        }

        if (!textNode) {
            throw new Error(`Cannot resolve a Slate point from DOM point: ${domPoint}`);
        }

        // COMPAT: If someone is clicking from one Slate editor into another,
        // the select event fires twice, once for the old editor's `element`
        // first, and then afterwards for the correct `element`. (2017/03/03)
        const slateNode = AngularEditor.toSlateNode(editor, textNode!);
        const path = AngularEditor.findPath(editor, slateNode);
        return { path, offset };
    },

    /**
     * Find a Slate range from a DOM range or selection.
     */

    toSlateRange(editor: AngularEditor, domRange: DOMRange | DOMStaticRange | DOMSelection): Range {
        const el = isDOMSelection(domRange) ? domRange.anchorNode : domRange.startContainer;
        let anchorNode;
        let anchorOffset;
        let focusNode;
        let focusOffset;
        let isCollapsed;

        if (el) {
            if (isDOMSelection(domRange)) {
                anchorNode = domRange.anchorNode;
                anchorOffset = domRange.anchorOffset;
                focusNode = domRange.focusNode;
                focusOffset = domRange.focusOffset;
                // COMPAT: There's a bug in chrome that always returns `true` for
                // `isCollapsed` for a Selection that comes from a ShadowRoot.
                // (2020/08/08)
                // https://bugs.chromium.org/p/chromium/issues/detail?id=447523
                if (IS_CHROME && hasShadowRoot()) {
                    isCollapsed =
                        domRange.anchorNode === domRange.focusNode &&
                        domRange.anchorOffset === domRange.focusOffset;
                } else {
                    isCollapsed = domRange.isCollapsed;
                }
            } else {
                anchorNode = domRange.startContainer;
                anchorOffset = domRange.startOffset;
                focusNode = domRange.endContainer;
                focusOffset = domRange.endOffset;
                isCollapsed = domRange.collapsed;
            }
        }

        if (anchorNode == null || focusNode == null || anchorOffset == null || focusOffset == null) {
            throw new Error(`Cannot resolve a Slate range from DOM range: ${domRange}`);
        }

        const anchor = AngularEditor.toSlatePoint(editor, [anchorNode, anchorOffset]);
        const focus = isCollapsed ? anchor : AngularEditor.toSlatePoint(editor, [focusNode, focusOffset]);

        return { anchor, focus };
    },

    isLeafBlock(editor: AngularEditor, node: Node): boolean {
        return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
    },

    isBlockCardLeftCursor(editor: AngularEditor) {
        return editor.selection.anchor.offset === FAKE_LEFT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_LEFT_BLOCK_CARD_OFFSET;
    },

    isBlockCardRightCursor(editor: AngularEditor) {
        return editor.selection.anchor.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET;
    },

    getCardCursorNode(editor: AngularEditor, blockCardNode: Node, options: {
        direction: 'left' | 'right' | 'center'
    }) {
        const blockCardElement = AngularEditor.toDOMNode(editor, blockCardNode);
        const cardCenter = blockCardElement.parentElement;
        return options.direction === 'left'
            ? cardCenter.previousElementSibling
            : cardCenter.nextElementSibling;
    },

    toSlateCardEntry(editor: AngularEditor, node: DOMNode): NodeEntry {
        const element = node.parentElement
            .closest('.slate-block-card')?.querySelector('[card-target="card-center"]')
            .firstElementChild;
        const slateNode = AngularEditor.toSlateNode(editor, element);
        const path = AngularEditor.findPath(editor, slateNode);
        return [slateNode, path];
    },

    /**
     * move native selection to card-left or card-right
     * @param editor 
     * @param blockCardNode 
     * @param options 
     */
    moveBlockCard(editor: AngularEditor, blockCardNode: Node, options: {
        direction: 'left' | 'right'
    }) {
        const cursorNode = AngularEditor.getCardCursorNode(editor, blockCardNode, options);
        const window = AngularEditor.getWindow(editor);
        const domSelection = window.getSelection();
        domSelection.setBaseAndExtent(cursorNode, 1, cursorNode, 1);
    },

    /**
     * move slate selection to card-left or card-right
     * @param editor 
     * @param path 
     * @param options 
     */
    moveBlockCardCursor(editor: AngularEditor, path: Path, options: {
        direction: 'left' | 'right'
    }) {
        const cursor = { path, offset: options.direction === 'left' ? FAKE_LEFT_BLOCK_CARD_OFFSET : FAKE_RIGHT_BLOCK_CARD_OFFSET };
        Transforms.select(editor, { anchor: cursor, focus: cursor });
    },

    hasRange(editor: AngularEditor, range: Range): boolean {
        const { anchor, focus } = range;
        return (
            Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path)
        );
    },
}