slate#Point TypeScript Examples

The following examples show how to use slate#Point. 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: on-code-block-keydown.ts    From fantasy-editor with MIT License 6 votes vote down vote up
onKeyDownCodeBlock = (e: KeyboardEvent, editor: Editor) => {
  if (isHotkey('Enter', e)) {
    const match = Editor.above(editor, {
      match: n => n.type === BLOCK_CODE,
    });
    if (match) {
      const [, path] = match;
      const text = Editor.string(editor, path);
      e.preventDefault();
      const {selection} = editor;
      if(selection && Range.isCollapsed(selection)){
        const start = Editor.end(editor, path);
        if(text.endsWith('\n')&&Point.equals(selection.anchor, start)){
          Transforms.delete(editor, {
            at: {
              anchor: {
                offset: selection.anchor.offset-1,
                path: selection.anchor.path
              },
              focus: selection.focus
            }
          })
          let nextPath = Path.next(path);
          Transforms.insertNodes(editor, {
            type: BLOCK_PARAGRAPH,
            children: [{
              text: ''
            }]
          }, {
            at: nextPath
          });
          Transforms.select(editor, nextPath);
          return;
        }
      }
      editor.insertText('\n');
    }
  }
}
Example #2
Source File: use-mention.tsx    From fantasy-editor with MIT License 6 votes vote down vote up
isWordAfterTrigger = (editor: Editor, { at, trigger }: { at: Point; trigger: string }) => {
  // Point at the start of previous word (excluding punctuation)
  const wordBefore = Editor.before(editor, at, { unit: 'word' });

  // Point before wordBefore
  const before = wordBefore && Editor.before(editor, wordBefore);

  // Range from before to start
  const beforeRange = before && Editor.range(editor, before, at);

  // Before text
  const beforeText = beforeRange && Editor.string(editor, beforeRange);

  // Starts with char and ends with word characters
  const escapedTrigger = escapeRegExp(trigger);
  const beforeRegex = new RegExp(`^${escapedTrigger}(\\w+)$`);

  // Match regex on before text
  const match = beforeText && beforeText.match(beforeRegex);

  return {
    range: beforeRange,
    match,
  };
}
Example #3
Source File: use-mention.tsx    From fantasy-editor with MIT License 6 votes vote down vote up
isPointAtWordEnd = (editor: Editor, { at }: { at: Point }) => {
  // Point after at
  const after = Editor.after(editor, at);

  // From at to after
  const afterRange = Editor.range(editor, at, after);
  const afterText = Editor.string(editor, afterRange);

  // Match regex on after text
  return !!afterText.match(AFTER_MATCH_REGEX);
}
Example #4
Source File: with-table.ts    From fantasy-editor with MIT License 6 votes vote down vote up
withTable = () => <T extends Editor>(editor:T) => {
  const {deleteBackward, deleteForward} = editor;

  const preventDeleteCell = (operation: any, pointCallback: any) => (
    unit: any
  ) => {
    const { selection } = editor;

    if (selection && Range.isCollapsed(selection)) {
      const [cell] = Editor.nodes(editor, {
        match: (n) => n.type === BLOCK_TABLE_CELL,
      });

      if (cell) {
        const [, cellPath] = cell;
        const start = pointCallback(editor, cellPath);

        if (Point.equals(selection.anchor, start)) {
          return;
        }
      }
    }

    operation(unit);
  };

  editor.deleteBackward = preventDeleteCell(deleteBackward, Editor.start);

  editor.deleteForward = preventDeleteCell(deleteForward, Editor.end);

  return editor;
}
Example #5
Source File: LastFocusedNode.tsx    From react-editor-kit with MIT License 6 votes vote down vote up
useLastFocused = (editor: ReactEditor) => {
  const [state, setState] = useState<LastFocusedState>({
    element: undefined,
    point: undefined,
    selection: undefined,
  });
  const current = getActiveNodeType(editor);

  useEffect(() => {
    if (!ReactEditor.isFocused(editor)) {
      return;
    }
    const { selection } = editor;
    if (!selection) {
      return;
    }
    if (current) {
      const point = selection.focus;
      const [element] = Editor.parent(editor, point);
      if (
        state.element === element &&
        state.point &&
        Point.equals(state.point, point)
      ) {
        return;
      }
      setState({
        element: element,
        point,
        selection: clone(selection),
      });
    }
  }, [current, state, editor.operations]);

  return state;
}
Example #6
Source File: markdown-shortcuts.component.ts    From slate-angular with MIT License 5 votes vote down vote up
withShortcuts = editor => {
    const { deleteBackward, insertText } = editor;

    editor.insertText = text => {
        const { selection } = editor;

        if (text === ' ' && selection && Range.isCollapsed(selection)) {
            const { anchor } = selection;
            const block = Editor.above<Element>(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 = SHORTCUTS[beforeText];

            if (type) {
                Transforms.select(editor, range);
                Transforms.delete(editor);
                Transforms.setNodes(editor, { type }, { match: n => Editor.isBlock(editor, n) });

                if (type === 'list-item') {
                    const list: BulletedListElement = { type: 'bulleted-list', children: [] };
                    Transforms.wrapNodes<Element>(editor, list, {
                        match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item'
                    });
                }

                return;
            }
        }

        insertText(text);
    };

    editor.deleteBackward = (...args) => {
        const { selection } = editor;

        if (selection && Range.isCollapsed(selection)) {
            const match = Editor.above<Element>(editor, {
                match: n => Editor.isBlock(editor, n)
            });

            if (match) {
                const [block, path] = match;
                const start = Editor.start(editor, path);

                if (block.type !== 'paragraph' && Point.equals(selection.anchor, start)) {
                    Transforms.setNodes(editor, { type: 'paragraph' });

                    if (block.type === 'list-item') {
                        Transforms.unwrapNodes(editor, {
                            match: n => Element.isElement(n) && n.type === 'bulleted-list',
                            split: true
                        });
                    }

                    return;
                }
            }

            deleteBackward(...args);
        }
    };

    return editor;
}
Example #7
Source File: vue-editor.ts    From slate-vue with MIT License 4 votes vote down vote up
VueEditor = {
  /**
   * Find a key for a Slate node.
   */

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

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

    return key
  },

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

  findPath(editor: VueEditor, 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)}`
    )
  },

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

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

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

  isReadOnly(editor: VueEditor): boolean {
    return !!IS_READ_ONLY.get(editor)
  },

  /**
   * Blur the editor.
   */

  blur(editor: VueEditor): void {
    const el = VueEditor.toDOMNode(editor, editor)
    IS_FOCUSED.set(editor, false)

    if (window.document.activeElement === el) {
      el.blur()
    }
  },

  /**
   * Focus the editor.
   */

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

    if (window.document.activeElement !== el) {
      el.focus({ preventScroll: true })
    }
  },

  /**
   * Deselect the editor.
   */

  deselect(editor: VueEditor): void {
    const { selection } = editor
    const domSelection = window.getSelection()

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

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

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

  hasDOMNode(
    editor: VueEditor,
    target: DOMNode,
    options: { editable?: boolean } = {}
  ): boolean {
    const { editable = false } = options
    const editorEl = VueEditor.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: VueEditor, data: DataTransfer): void {
    editor.insertData(data)
  },

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

  setFragmentData(editor: VueEditor, data: DataTransfer): void {
    editor.setFragmentData(data)
  },

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

  toDOMNode(editor: VueEditor, node: Node): HTMLElement {
    const domNode = Editor.isEditor(node)
      ? EDITOR_TO_ELEMENT.get(editor)
      : KEY_TO_ELEMENT.get(VueEditor.findKey(editor, 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: VueEditor, point: Point): DOMPoint {
    const [node] = Editor.node(editor, point.path)
    const el = VueEditor.toDOMNode(editor, node)
    let domPoint: DOMPoint | undefined

    // 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]
        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`.
   *
   * Notice: the returned range will always be ordinal regardless of the direction of Slate `range` due to DOM API limit.
   *
   * there is no way to create a reverse DOM Range using Range.setStart/setEnd
   * according to https://dom.spec.whatwg.org/#concept-range-bp-set.
   */

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

    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: VueEditor, 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: VueEditor, 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 = VueEditor.toSlateNode(editor, event.target)
    const path = VueEditor.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) {
        const range = Editor.range(editor, point)
        return range
      }
    }

    // Else resolve a range from the caret position where the drop occured.
    let domRange
    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.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 = VueEditor.toSlateRange(editor, domRange)
    return range
  },

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

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

    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 range = window.document.createRange()
        range.setStart(textNode, 0)
        range.setEnd(nearestNode, nearestOffset)
        const contents = range.cloneContents()
        const removals = [
          ...contents.querySelectorAll('[data-slate-zero-width]'),
          ...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]')!
        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.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 = VueEditor.toSlateNode(editor, textNode!)
    const path = VueEditor.findPath(editor, slateNode)
    return { path, offset }
  },

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

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

    if (el) {
      if (domRange instanceof Selection) {
        anchorNode = domRange.anchorNode
        anchorOffset = domRange.anchorOffset
        focusNode = domRange.focusNode
        focusOffset = domRange.focusOffset
        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 = VueEditor.toSlatePoint(editor, [anchorNode, anchorOffset])
    const focus = isCollapsed
      ? anchor
      : VueEditor.toSlatePoint(editor, [focusNode, focusOffset])

    return { anchor, focus }
  },
}
Example #8
Source File: withShortcuts.ts    From ui-schema with MIT License 4 votes vote down vote up
withShortcuts: (options: CustomOptions) => SlateHocType<ReactEditor> = (options) => (editor) => {
    if(options.onlyInline){
        return editor
    }
    const {deleteBackward, insertText} = editor

    editor.insertText = (text: string) => {
        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}
            let beforeText = Editor.string(editor, range)
            const dotPos = beforeText.indexOf('.')
            if (dotPos !== -1 && !isNaN(Number(beforeText.slice(0, dotPos)))) {
                beforeText = '1'
            }
            const type = SHORTCUTS[beforeText]

            if (
                type &&
                editorIsEnabled(options.enableOnly, type) &&
                // only apply shortcuts when not inside a li, as this may produce invalid data
                // @ts-ignore
                !(block && block[0].type === pluginOptions.li.type)
            ) {
                Transforms.select(editor, range)
                Transforms.delete(editor)
                const newProperties: Partial<SlateElement> = {
                    // @ts-ignore
                    type: type === pluginOptions.ol.type ? pluginOptions.li.type : type,
                }
                Transforms.setNodes(editor, newProperties, {
                    match: n => Editor.isBlock(editor, n),
                })

                if (type === pluginOptions.ol.type) {
                    const list: BulletedListElement = {
                        type: pluginOptions.ol.type,
                        children: [],
                    }
                    // todo: here seems to be the `li` error, currently missing `ul` around `li`
                    Transforms.wrapNodes(editor, list, {
                        match: n =>
                            !Editor.isEditor(n) &&
                            SlateElement.isElement(n) &&
                            // @ts-ignore
                            n.type === pluginOptions.li.type,
                    })
                } else if (type === pluginOptions.li.type) {
                    const list: BulletedListElement = {
                        type: pluginOptions.ul.type,
                        children: [],
                    }
                    // todo: here seems to be the `li` error, currently missing `ul` around `li`
                    Transforms.wrapNodes(editor, list, {
                        match: n =>
                            !Editor.isEditor(n) &&
                            SlateElement.isElement(n) &&
                            // @ts-ignore
                            n.type === pluginOptions.li.type,
                    })
                }

                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 (
                    !Editor.isEditor(block) &&
                    SlateElement.isElement(block) &&
                    // @ts-ignore
                    block.type !== pluginOptions.p.type &&
                    Point.equals(selection.anchor, start)
                ) {
                    const newProperties: Partial<SlateElement> = {
                        // @ts-ignore
                        type: pluginOptions.p.type,
                    }
                    Transforms.setNodes(editor, newProperties)

                    // @ts-ignore
                    if (block.type === pluginOptions.li.type) {
                        Transforms.unwrapNodes(editor, {
                            match: n =>
                                !Editor.isEditor(n) &&
                                SlateElement.isElement(n) &&
                                (
                                    // @ts-ignore
                                    n.type === pluginOptions.ul.type ||
                                    // @ts-ignore
                                    n.type === pluginOptions.ol.type
                                ),
                            split: true,
                        })
                    }

                    return
                }
            }

            deleteBackward(...args)
        }
    }

    return editor
}
Example #9
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)
        );
    },
}