lexical-vue
Version:
An extensible Vue 3 web text-editor based on Lexical.
1 lines • 240 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/composables/useCanShowPlaceholder.ts","../src/composables/useMounted.ts","../src/composables/useCharacterLimit.ts","../src/composables/useDecorators.ts","../src/composables/useEffect.ts","../src/composables/useHistory.ts","../src/composables/useLexicalCommandsLog.ts","../src/composables/useLexicalComposer.ts","../src/composables/inject.ts","../src/composables/useLexicalIsTextContentEmpty.ts","../src/composables/useLexicalNodeSelection.ts","../src/composables/useLexicalTextEntity.ts","../src/composables/useList.ts","../src/composables/usePlainTextSetup.ts","../src/composables/useRichTextSetup.ts","../src/composables/useTableOfContents.ts","../src/composables/useYjsCollaboration.ts","../src/components/LexicalDecoratedTeleports.ts","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalContentEditable.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalContentEditableElement.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalComposer.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalOnChangePlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalTreeViewPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalAutoFocusPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalListPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalLinkPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalTablePlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalClearEditorPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalCharacterLimitPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalAutoScrollPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalHashtagPlugin.vue","../src/components/LexicalDecoratorBlockNode.ts","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalBlockWithAlignableContents.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalCheckListPlugin.vue","../src/components/LexicalMarkdownShortcutPlugin/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalMarkdownShortcutPlugin/index.vue","../src/components/LexicalHorizontalRuleNode.ts","../src/components/LexicalMarkdownShortcutPlugin/shared.ts","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalTabIndentationPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalCollaborationPlugin.vue","../src/composables/useCollaborationContext.ts","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalClickableLinkPlugin.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalContextMenuPlugin.vue","../src/components/LexicalMenu/shared.ts","../src/components/LexicalMenu/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalMenu/index.vue","../src/components/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalNodeMenuPlugin.vue","../src/components/LexicalAutoEmbedPlugin/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalAutoEmbedPlugin/index.vue","../src/components/LexicalAutoEmbedPlugin/shared.ts","../src/components/LexicalAutoLinkPlugin/shared.ts","../src/components/LexicalTypeaheadMenuPlugin/home/runner/work/lexical-vue/lexical-vue/src/components/LexicalTypeaheadMenuPlugin/index.vue","../src/components/LexicalTypeaheadMenuPlugin/shared.ts"],"sourcesContent":["export * from './composables'\nexport * from './components'\n","import { readonly, ref } from 'vue'\nimport { $canShowPlaceholderCurry } from '@lexical/text'\nimport type { LexicalEditor } from 'lexical'\nimport { mergeRegister } from '@lexical/utils'\nimport { useMounted } from './useMounted'\n\nfunction canShowPlaceholderFromCurrentEditorState(\n editor: LexicalEditor,\n): boolean {\n const currentCanShowPlaceholder = editor\n .getEditorState()\n .read($canShowPlaceholderCurry(editor.isComposing()))\n\n return currentCanShowPlaceholder\n}\n\nexport function useCanShowPlaceholder(editor: LexicalEditor) {\n const initialState = editor\n .getEditorState()\n .read($canShowPlaceholderCurry(editor.isComposing()))\n\n const canShowPlaceholder = ref(initialState)\n\n function resetCanShowPlaceholder() {\n const currentCanShowPlaceholder\n = canShowPlaceholderFromCurrentEditorState(editor)\n canShowPlaceholder.value = currentCanShowPlaceholder\n }\n\n useMounted(() => {\n return mergeRegister(\n editor.registerUpdateListener(() => {\n resetCanShowPlaceholder()\n }),\n editor.registerEditableListener(() => {\n resetCanShowPlaceholder()\n }),\n )\n })\n\n return readonly(canShowPlaceholder)\n}\n","import { onMounted, onUnmounted } from 'vue'\n\n/**\n * @internal\n */\nexport function useMounted(cb: () => undefined | (() => any)) {\n let unregister: (() => void) | undefined\n\n onMounted(() => {\n unregister = cb()\n })\n\n onUnmounted(() => {\n unregister?.()\n })\n}\n","import type { LexicalEditor, LexicalNode } from 'lexical'\n\nimport {\n $createOverflowNode,\n $isOverflowNode,\n OverflowNode,\n} from '@lexical/overflow'\nimport { $rootTextContent } from '@lexical/text'\nimport { $dfs, mergeRegister } from '@lexical/utils'\nimport {\n $getSelection,\n $isLeafNode,\n $isRangeSelection,\n $isTextNode,\n $setSelection,\n} from 'lexical'\nimport invariant from 'tiny-invariant'\n\nimport { type MaybeRefOrGetter, toValue, watchEffect } from 'vue'\n\ninterface OptionalProps {\n remainingCharacters?: (characters: number) => void\n strlen?: (input: string) => number\n}\n\nexport function useCharacterLimit(\n editor: LexicalEditor,\n maxCharacters: MaybeRefOrGetter<number>,\n optional: MaybeRefOrGetter<OptionalProps> = Object.freeze({}),\n) {\n watchEffect((onInvalidate) => {\n if (!editor.hasNodes([OverflowNode])) {\n invariant(\n false,\n 'useCharacterLimit: OverflowNode not registered on editor',\n )\n }\n\n const {\n strlen = (input: string): number => input.length, // UTF-16\n remainingCharacters = (_characters: number): void => {},\n } = toValue(optional)\n\n let text = editor.getEditorState().read($rootTextContent)\n let lastComputedTextLength = 0\n\n const fn = mergeRegister(\n editor.registerTextContentListener((currentText: string) => {\n text = currentText\n }),\n editor.registerUpdateListener(({ dirtyLeaves }) => {\n const isComposing = editor.isComposing()\n const hasDirtyLeaves = dirtyLeaves.size > 0\n if (isComposing || !hasDirtyLeaves)\n return\n\n const textLength = strlen(text)\n const textLengthAboveThreshold\n = textLength > toValue(maxCharacters)\n || (lastComputedTextLength !== null\n && lastComputedTextLength > toValue(maxCharacters))\n const diff = toValue(maxCharacters) - textLength\n remainingCharacters(diff)\n if (lastComputedTextLength === null || textLengthAboveThreshold) {\n const offset = findOffset(text, toValue(maxCharacters), strlen)\n editor.update(\n () => {\n $wrapOverflowedNodes(offset)\n },\n {\n tag: 'history-merge',\n },\n )\n }\n lastComputedTextLength = textLength\n }),\n )\n\n onInvalidate(() => {\n fn()\n })\n })\n}\n\nfunction findOffset(\n text: string,\n maxCharacters: number,\n strlen: (input: string) => number,\n): number {\n const Segmenter = Intl.Segmenter\n let offsetUtf16 = 0\n let offset = 0\n\n if (typeof Segmenter === 'function') {\n const segmenter = new Segmenter()\n const graphemes = segmenter.segment(text)\n\n for (const { segment: grapheme } of graphemes) {\n const nextOffset = offset + strlen(grapheme)\n\n if (nextOffset > maxCharacters)\n break\n\n offset = nextOffset\n offsetUtf16 += grapheme.length\n }\n }\n else {\n const codepoints = Array.from(text)\n const codepointsLength = codepoints.length\n\n for (let i = 0; i < codepointsLength; i++) {\n const codepoint = codepoints[i]\n const nextOffset = offset + strlen(codepoint)\n\n if (nextOffset > maxCharacters)\n break\n\n offset = nextOffset\n offsetUtf16 += codepoint.length\n }\n }\n\n return offsetUtf16\n}\n\nfunction $wrapOverflowedNodes(offset: number): void {\n const dfsNodes = $dfs()\n const dfsNodesLength = dfsNodes.length\n let accumulatedLength = 0\n\n for (let i = 0; i < dfsNodesLength; i += 1) {\n const { node } = dfsNodes[i]\n\n if ($isOverflowNode(node)) {\n const previousLength = accumulatedLength\n const nextLength = accumulatedLength + node.getTextContentSize()\n\n if (nextLength <= offset) {\n const parent = node.getParent()\n const previousSibling = node.getPreviousSibling()\n const nextSibling = node.getNextSibling()\n $unwrapNode(node)\n const selection = $getSelection()\n\n // Restore selection when the overflow children are removed\n if (\n $isRangeSelection(selection)\n && (!selection.anchor.getNode().isAttached()\n || !selection.focus.getNode().isAttached())\n ) {\n if ($isTextNode(previousSibling))\n previousSibling.select()\n else if ($isTextNode(nextSibling))\n nextSibling.select()\n else if (parent !== null)\n parent.select()\n }\n }\n else if (previousLength < offset) {\n const descendant = node.getFirstDescendant()\n const descendantLength\n = descendant !== null ? descendant.getTextContentSize() : 0\n const previousPlusDescendantLength = previousLength + descendantLength\n // For simple text we can redimension the overflow into a smaller and more accurate\n // container\n const firstDescendantIsSimpleText\n = $isTextNode(descendant) && descendant.isSimpleText()\n const firstDescendantDoesNotOverflow\n = previousPlusDescendantLength <= offset\n\n if (firstDescendantIsSimpleText || firstDescendantDoesNotOverflow)\n $unwrapNode(node)\n }\n }\n else if ($isLeafNode(node)) {\n const previousAccumulatedLength = accumulatedLength\n accumulatedLength += node.getTextContentSize()\n\n if (accumulatedLength > offset && !$isOverflowNode(node.getParent())) {\n const previousSelection = $getSelection()\n let overflowNode\n\n // For simple text we can improve the limit accuracy by splitting the TextNode\n // on the split point\n if (\n previousAccumulatedLength < offset\n && $isTextNode(node)\n && node.isSimpleText()\n ) {\n const [, overflowedText] = node.splitText(\n offset - previousAccumulatedLength,\n )\n overflowNode = $wrapNode(overflowedText)\n }\n else {\n overflowNode = $wrapNode(node)\n }\n\n if (previousSelection !== null)\n $setSelection(previousSelection)\n\n mergePrevious(overflowNode)\n }\n }\n }\n}\n\nfunction $wrapNode(node: LexicalNode): OverflowNode {\n const overflowNode = $createOverflowNode()\n node.insertBefore(overflowNode)\n overflowNode.append(node)\n return overflowNode\n}\n\nfunction $unwrapNode(node: OverflowNode): LexicalNode | null {\n const children = node.getChildren()\n const childrenLength = children.length\n\n for (let i = 0; i < childrenLength; i++)\n node.insertBefore(children[i])\n\n node.remove()\n return childrenLength > 0 ? children[childrenLength - 1] : null\n}\n\nexport function mergePrevious(overflowNode: OverflowNode): void {\n const previousNode = overflowNode.getPreviousSibling()\n\n if (!$isOverflowNode(previousNode))\n return\n\n const firstChild = overflowNode.getFirstChild()\n const previousNodeChildren = previousNode.getChildren()\n const previousNodeChildrenLength = previousNodeChildren.length\n\n if (firstChild === null) {\n overflowNode.append(...previousNodeChildren)\n }\n else {\n for (let i = 0; i < previousNodeChildrenLength; i++)\n firstChild.insertBefore(previousNodeChildren[i])\n }\n\n const selection = $getSelection()\n\n if ($isRangeSelection(selection)) {\n const anchor = selection.anchor\n const anchorNode = anchor.getNode()\n const focus = selection.focus\n const focusNode = anchor.getNode()\n\n if (anchorNode.is(previousNode)) {\n anchor.set(overflowNode.getKey(), anchor.offset, 'element')\n }\n else if (anchorNode.is(overflowNode)) {\n anchor.set(\n overflowNode.getKey(),\n previousNodeChildrenLength + anchor.offset,\n 'element',\n )\n }\n\n if (focusNode.is(previousNode)) {\n focus.set(overflowNode.getKey(), focus.offset, 'element')\n }\n else if (focusNode.is(overflowNode)) {\n focus.set(\n overflowNode.getKey(),\n previousNodeChildrenLength + focus.offset,\n 'element',\n )\n }\n }\n\n previousNode.remove()\n}\n","import type { LexicalEditor } from 'lexical'\nimport type { DefineComponent } from 'vue'\nimport { Teleport, computed, h, shallowRef, unref } from 'vue'\nimport { useMounted } from './useMounted'\n\nexport function useDecorators(editor: LexicalEditor) {\n const decorators = shallowRef<Record<string, DefineComponent>>(editor.getDecorators())\n\n useMounted(() => {\n return editor.registerDecoratorListener((nextDecorators) => {\n decorators.value = nextDecorators as Record<string, DefineComponent>\n })\n })\n\n // Return decorators defined as Vue Teleports\n return computed(() => {\n const decoratedTeleports = []\n const decoratorKeys = Object.keys(unref(decorators))\n for (let i = 0; i < decoratorKeys.length; i++) {\n const nodeKey = decoratorKeys[i]\n const vueDecorator = decorators.value[nodeKey]\n const element = editor.getElementByKey(nodeKey)\n if (element !== null) {\n decoratedTeleports.push(\n h(Teleport, {\n to: element,\n }, vueDecorator),\n )\n }\n }\n\n return decoratedTeleports\n })\n}\n","import type { WatchOptionsBase } from 'vue'\nimport { watchEffect } from 'vue'\n\n/**\n * @internal\n */\nexport function useEffect(cb: () => ((() => any) | void), options?: WatchOptionsBase) {\n watchEffect((onInvalidate) => {\n const unregister = cb()\n\n onInvalidate(() => unregister?.())\n }, {\n ...options,\n })\n}\n","import type { HistoryState } from '@lexical/history'\nimport type { LexicalEditor } from 'lexical'\nimport { type MaybeRefOrGetter, computed, toValue, watchEffect } from 'vue'\n\nimport { createEmptyHistoryState, registerHistory } from '@lexical/history'\n\nexport function useHistory(\n editor: MaybeRefOrGetter<LexicalEditor>,\n externalHistoryState?: MaybeRefOrGetter<HistoryState | undefined>,\n delay?: MaybeRefOrGetter<number | undefined>,\n) {\n const historyState = computed<HistoryState>(\n () => toValue(externalHistoryState) || createEmptyHistoryState(),\n )\n\n watchEffect((onInvalidate) => {\n const unregisterListener = registerHistory(toValue(editor), historyState.value, toValue(delay) || 1000)\n\n onInvalidate(unregisterListener)\n })\n}\n","import { COMMAND_PRIORITY_HIGH, type LexicalCommand, type LexicalEditor } from 'lexical'\nimport { readonly, ref } from 'vue'\nimport { useMounted } from './useMounted'\n\nexport function useLexicalCommandsLog(\n editor: LexicalEditor,\n) {\n const loggedCommands = ref<Array<LexicalCommand<unknown> & { payload: unknown }>>([])\n\n useMounted(() => {\n const unregisterCommandListeners = new Set<() => void>()\n\n for (const [command] of editor._commands) {\n unregisterCommandListeners.add(\n editor.registerCommand(\n command,\n (payload) => {\n loggedCommands.value = [\n ...loggedCommands.value,\n {\n payload,\n type: command.type ? command.type : 'UNKNOWN',\n },\n ]\n\n if (loggedCommands.value.length > 10)\n loggedCommands.value.shift()\n\n return false\n },\n COMMAND_PRIORITY_HIGH,\n ),\n )\n }\n\n return () => {\n unregisterCommandListeners.forEach(unregister => unregister())\n }\n })\n\n return readonly(loggedCommands)\n}\n","import { inject } from 'vue'\nimport invariant from 'tiny-invariant'\nimport type { LexicalEditor } from 'lexical'\nimport { LexicalEditorProviderKey } from './inject'\n\nexport function useLexicalComposer() {\n const editor = inject<LexicalEditor>(LexicalEditorProviderKey)\n\n if (!editor) {\n invariant(\n false,\n 'useLexicalComposer: cannot find a LexicalComposer',\n )\n }\n\n return editor\n}\n","export const LexicalEditorProviderKey = 'LexicalEditorProviderKey'\n","import { readonly, ref } from 'vue'\nimport { $isRootTextContentEmptyCurry } from '@lexical/text'\nimport type { LexicalEditor } from 'lexical'\nimport { useMounted } from './useMounted'\n\nexport function useLexicalIsTextContentEmpty(editor: LexicalEditor, trim?: boolean) {\n const isEmpty = ref(\n editor\n .getEditorState()\n .read($isRootTextContentEmptyCurry(editor.isComposing(), trim)),\n )\n\n useMounted(() => {\n return editor.registerUpdateListener(({ editorState }) => {\n const isComposing = editor.isComposing()\n isEmpty.value = editorState.read(\n $isRootTextContentEmptyCurry(isComposing, trim),\n )\n })\n })\n\n return readonly(isEmpty)\n}\n","import type { LexicalEditor, NodeKey } from 'lexical'\n\nimport {\n $createNodeSelection,\n $getNodeByKey,\n $getSelection,\n $isNodeSelection,\n $setSelection,\n} from 'lexical'\nimport { type MaybeRefOrGetter, readonly, ref, toValue, watchEffect } from 'vue'\nimport { useLexicalComposer } from './useLexicalComposer'\n\nfunction isNodeSelected(editor: LexicalEditor, key: NodeKey): boolean {\n return editor.getEditorState().read(() => {\n const node = $getNodeByKey(key)\n if (node === null)\n return false\n\n return node.isSelected()\n })\n}\n\nexport function useLexicalNodeSelection(\n key: MaybeRefOrGetter<NodeKey>,\n) {\n const editor = useLexicalComposer()\n const isSelected = ref(isNodeSelected(editor, toValue(key)))\n\n watchEffect((onInvalidate) => {\n const unregisterListener = editor.registerUpdateListener(() => {\n isSelected.value = isNodeSelected(editor, toValue(key))\n })\n\n onInvalidate(() => {\n unregisterListener()\n })\n })\n\n const setSelected = (selected: boolean) => {\n editor.update(() => {\n let selection = $getSelection()\n\n if (!$isNodeSelection(selection)) {\n selection = $createNodeSelection()\n $setSelection(selection)\n }\n if ($isNodeSelection(selection)) {\n if (selected)\n selection.add(toValue(key))\n else\n selection.delete(toValue(key))\n }\n })\n }\n\n const clearSelection = () => {\n editor.update(() => {\n const selection = $getSelection()\n if ($isNodeSelection(selection))\n (selection).clear()\n })\n }\n\n return {\n isSelected: readonly(isSelected),\n setSelected,\n clearSelection,\n }\n}\n","import type { EntityMatch } from '@lexical/text'\nimport type { Klass, TextNode } from 'lexical'\n\nimport { registerLexicalTextEntity } from '@lexical/text'\nimport { mergeRegister } from '@lexical/utils'\nimport { useLexicalComposer } from './useLexicalComposer'\nimport { useMounted } from './useMounted'\n\nexport function useLexicalTextEntity<T extends TextNode>(\n getMatch: (text: string) => null | EntityMatch,\n targetNode: Klass<T>,\n createNode: (textNode: TextNode) => T,\n): void {\n const editor = useLexicalComposer()\n\n useMounted(() => {\n return mergeRegister(\n ...registerLexicalTextEntity(editor, getMatch, targetNode, createNode),\n )\n })\n}\n","import type { LexicalEditor } from 'lexical'\n\nimport {\n $handleListInsertParagraph,\n INSERT_ORDERED_LIST_COMMAND,\n INSERT_UNORDERED_LIST_COMMAND,\n REMOVE_LIST_COMMAND,\n insertList,\n removeList,\n} from '@lexical/list'\nimport { mergeRegister } from '@lexical/utils'\nimport {\n COMMAND_PRIORITY_LOW,\n INSERT_PARAGRAPH_COMMAND,\n} from 'lexical'\nimport { useMounted } from './useMounted'\n\nexport function useList(editor: LexicalEditor) {\n useMounted(() => {\n return mergeRegister(\n editor.registerCommand(\n INSERT_ORDERED_LIST_COMMAND,\n () => {\n insertList(editor, 'number')\n return true\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n INSERT_UNORDERED_LIST_COMMAND,\n () => {\n insertList(editor, 'bullet')\n return true\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n REMOVE_LIST_COMMAND,\n () => {\n removeList(editor)\n return true\n },\n COMMAND_PRIORITY_LOW,\n ),\n editor.registerCommand(\n INSERT_PARAGRAPH_COMMAND,\n () => {\n const hasHandledInsertParagraph = $handleListInsertParagraph()\n\n if (hasHandledInsertParagraph)\n return true\n\n return false\n },\n COMMAND_PRIORITY_LOW,\n ),\n )\n })\n}\n","import { registerDragonSupport } from '@lexical/dragon'\nimport { registerPlainText } from '@lexical/plain-text'\nimport { mergeRegister } from '@lexical/utils'\nimport type { LexicalEditor } from 'lexical'\nimport { useMounted } from './useMounted'\n\nexport function usePlainTextSetup(editor: LexicalEditor) {\n useMounted(() => {\n return mergeRegister(\n registerPlainText(editor),\n registerDragonSupport(editor),\n )\n })\n}\n","import { registerDragonSupport } from '@lexical/dragon'\nimport { registerRichText } from '@lexical/rich-text'\nimport { mergeRegister } from '@lexical/utils'\nimport type { LexicalEditor } from 'lexical'\nimport { useMounted } from './useMounted'\n\nexport function useRichTextSetup(editor: LexicalEditor) {\n useMounted(() => {\n return mergeRegister(\n registerRichText(editor),\n registerDragonSupport(editor),\n )\n })\n}\n","import { $isHeadingNode, HeadingNode, type HeadingTagType } from '@lexical/rich-text'\nimport { $getNextRightPreorderNode, mergeRegister } from '@lexical/utils'\nimport {\n $getNodeByKey,\n $getRoot,\n $isElementNode,\n type ElementNode,\n type LexicalEditor,\n type NodeKey,\n type NodeMutation,\n TextNode,\n} from 'lexical'\nimport { ref } from 'vue'\nimport { useMounted } from './useMounted'\n\nexport type TableOfContentsEntry = [\n key: NodeKey,\n text: string,\n tag: HeadingTagType,\n]\n\nfunction toEntry(heading: HeadingNode): TableOfContentsEntry {\n return [heading.getKey(), heading.getTextContent(), heading.getTag()]\n}\n\nfunction $insertHeadingIntoTableOfContents(\n prevHeading: HeadingNode | null,\n newHeading: HeadingNode | null,\n currentTableOfContents: Array<TableOfContentsEntry>,\n): Array<TableOfContentsEntry> {\n if (newHeading === null)\n return currentTableOfContents\n\n const newEntry: TableOfContentsEntry = toEntry(newHeading)\n let newTableOfContents: Array<TableOfContentsEntry> = []\n if (prevHeading === null) {\n // check if key already exists\n if (\n currentTableOfContents.length > 0\n && currentTableOfContents[0][0] === newHeading.__key\n ) {\n return currentTableOfContents\n }\n\n newTableOfContents = [newEntry, ...currentTableOfContents]\n }\n else {\n for (let i = 0; i < currentTableOfContents.length; i++) {\n const key = currentTableOfContents[i][0]\n newTableOfContents.push(currentTableOfContents[i])\n if (key === prevHeading.getKey() && key !== newHeading.getKey()) {\n // check if key already exists\n if (\n i + 1 < currentTableOfContents.length\n && currentTableOfContents[i + 1][0] === newHeading.__key\n ) {\n return currentTableOfContents\n }\n\n newTableOfContents.push(newEntry)\n }\n }\n }\n return newTableOfContents\n}\n\nfunction $deleteHeadingFromTableOfContents(\n key: NodeKey,\n currentTableOfContents: Array<TableOfContentsEntry>,\n): Array<TableOfContentsEntry> {\n const newTableOfContents = []\n for (const heading of currentTableOfContents) {\n if (heading[0] !== key)\n newTableOfContents.push(heading)\n }\n return newTableOfContents\n}\n\nfunction $updateHeadingInTableOfContents(\n heading: HeadingNode,\n currentTableOfContents: Array<TableOfContentsEntry>,\n): Array<TableOfContentsEntry> {\n const newTableOfContents: Array<TableOfContentsEntry> = []\n for (const oldHeading of currentTableOfContents) {\n if (oldHeading[0] === heading.getKey())\n newTableOfContents.push(toEntry(heading))\n\n else\n newTableOfContents.push(oldHeading)\n }\n return newTableOfContents\n}\n\n/**\n * Returns the updated table of contents, placing the given `heading` before the given `prevHeading`. If `prevHeading`\n * is undefined, `heading` is placed at the start of table of contents\n */\nfunction $updateHeadingPosition(\n prevHeading: HeadingNode | null,\n heading: HeadingNode,\n currentTableOfContents: Array<TableOfContentsEntry>,\n): Array<TableOfContentsEntry> {\n const newTableOfContents: Array<TableOfContentsEntry> = []\n const newEntry: TableOfContentsEntry = toEntry(heading)\n\n if (!prevHeading)\n newTableOfContents.push(newEntry)\n\n for (const oldHeading of currentTableOfContents) {\n if (oldHeading[0] === heading.getKey())\n continue\n\n newTableOfContents.push(oldHeading)\n if (prevHeading && oldHeading[0] === prevHeading.getKey())\n newTableOfContents.push(newEntry)\n }\n\n return newTableOfContents\n}\n\nfunction $getPreviousHeading(node: HeadingNode): HeadingNode | null {\n let prevHeading = $getNextRightPreorderNode(node)\n while (prevHeading !== null && !$isHeadingNode(prevHeading))\n prevHeading = $getNextRightPreorderNode(prevHeading)\n\n return prevHeading\n}\n\nexport function useTableOfContents(editor: LexicalEditor) {\n const tableOfContents = ref<Array<TableOfContentsEntry>>([])\n // Set table of contents initial state\n editor.getEditorState().read(() => {\n const root = $getRoot()\n const rootChildren = root.getChildren()\n for (const child of rootChildren) {\n if ($isHeadingNode(child)) {\n tableOfContents.value.push([\n child.getKey(),\n child.getTextContent(),\n child.getTag(),\n ])\n }\n }\n })\n\n const removeRootUpdateListener = editor.registerUpdateListener(\n ({ editorState, dirtyElements }) => {\n editorState.read(() => {\n const updateChildHeadings = (node: ElementNode) => {\n for (const child of node.getChildren()) {\n if ($isHeadingNode(child)) {\n const prevHeading = $getPreviousHeading(child)\n tableOfContents.value = $updateHeadingPosition(\n prevHeading,\n child,\n tableOfContents.value,\n )\n }\n else if ($isElementNode(child)) {\n updateChildHeadings(child)\n }\n }\n }\n\n // If a node is changes, all child heading positions need to be updated\n $getRoot()\n .getChildren()\n .forEach((node) => {\n if ($isElementNode(node) && dirtyElements.get(node.__key))\n updateChildHeadings(node)\n })\n })\n },\n )\n\n // Listen to updates to heading mutations and update state\n const removeHeaderMutationListener = editor.registerMutationListener(\n HeadingNode,\n (mutatedNodes: Map<string, NodeMutation>) => {\n editor.getEditorState().read(() => {\n for (const [nodeKey, mutation] of mutatedNodes) {\n if (mutation === 'created') {\n const newHeading = $getNodeByKey<HeadingNode>(nodeKey)\n if (newHeading !== null) {\n const prevHeading = $getPreviousHeading(newHeading)\n tableOfContents.value = $insertHeadingIntoTableOfContents(\n prevHeading,\n newHeading,\n tableOfContents.value,\n )\n }\n }\n else if (mutation === 'destroyed') {\n tableOfContents.value = $deleteHeadingFromTableOfContents(\n nodeKey,\n tableOfContents.value,\n )\n }\n else if (mutation === 'updated') {\n const newHeading = $getNodeByKey<HeadingNode>(nodeKey)\n if (newHeading !== null) {\n const prevHeading = $getPreviousHeading(newHeading)\n tableOfContents.value = $updateHeadingPosition(\n prevHeading,\n newHeading,\n tableOfContents.value,\n )\n }\n }\n }\n })\n },\n )\n\n // Listen to text node mutation updates\n const removeTextNodeMutationListener = editor.registerMutationListener(\n TextNode,\n (mutatedNodes: Map<string, NodeMutation>) => {\n editor.getEditorState().read(() => {\n for (const [nodeKey, mutation] of mutatedNodes) {\n if (mutation === 'updated') {\n const currNode = $getNodeByKey(nodeKey)\n if (currNode !== null) {\n const parentNode = currNode.getParentOrThrow()\n if ($isHeadingNode(parentNode)) {\n tableOfContents.value = $updateHeadingInTableOfContents(\n parentNode,\n tableOfContents.value,\n )\n }\n }\n }\n }\n })\n },\n )\n\n useMounted(() => mergeRegister(\n removeRootUpdateListener,\n removeHeaderMutationListener,\n removeTextNodeMutationListener,\n ))\n\n return tableOfContents\n}\n","import type { Binding, ExcludedProperties, Provider } from '@lexical/yjs'\nimport type { LexicalEditor } from 'lexical'\n\nimport { mergeRegister } from '@lexical/utils'\nimport {\n CONNECTED_COMMAND,\n TOGGLE_CONNECT_COMMAND,\n createBinding,\n createUndoManager,\n initLocalState,\n setLocalStateFocus,\n syncCursorPositions,\n syncLexicalUpdateToYjs,\n syncYjsChangesToLexical,\n} from '@lexical/yjs'\nimport {\n $createParagraphNode,\n $getRoot,\n $getSelection,\n BLUR_COMMAND,\n COMMAND_PRIORITY_EDITOR,\n FOCUS_COMMAND,\n REDO_COMMAND,\n UNDO_COMMAND,\n} from 'lexical'\nimport type { Doc, Transaction, YEvent } from 'yjs'\nimport { UndoManager } from 'yjs'\nimport type { ComputedRef } from 'vue'\nimport { computed, ref, toRaw } from 'vue'\nimport type { InitialEditorStateType } from '../types'\nimport { useEffect } from './useEffect'\n\nexport function useYjsCollaboration(\n editor: LexicalEditor,\n id: string,\n provider: Provider,\n docMap: Map<string, Doc>,\n name: string,\n color: string,\n shouldBootstrap: boolean,\n initialEditorState?: InitialEditorStateType,\n excludedProperties?: ExcludedProperties,\n awarenessData?: object,\n): ComputedRef<Binding> {\n const isReloadingDoc = ref(false)\n const doc = ref(docMap.get(id))\n\n const binding = computed(() => createBinding(editor, provider, id, toRaw(doc.value), docMap, excludedProperties))\n\n const connect = () => {\n provider.connect()\n }\n\n const disconnect = () => {\n try {\n provider.disconnect()\n }\n catch {\n // Do nothing\n }\n }\n\n useEffect(() => {\n const { root } = binding.value\n const { awareness } = provider\n\n const onStatus = ({ status }: { status: string }) => {\n editor.dispatchCommand(CONNECTED_COMMAND, status === 'connected')\n }\n\n const onSync = (isSynced: boolean) => {\n if (\n shouldBootstrap\n && isSynced\n && root.isEmpty()\n && root._xmlText._length === 0\n && isReloadingDoc.value === false\n ) {\n initializeEditor(editor, initialEditorState)\n }\n\n isReloadingDoc.value = false\n }\n\n const onAwarenessUpdate = () => {\n syncCursorPositions(binding.value, provider)\n }\n\n const onYjsTreeChanges = (\n // The below `any` type is taken directly from the vendor types for YJS.\n events: Array<YEvent<any>>,\n transaction: Transaction,\n ) => {\n const origin = transaction.origin\n if (toRaw(origin) !== binding.value) {\n const isFromUndoManger = origin instanceof UndoManager\n syncYjsChangesToLexical(binding.value, provider, events, isFromUndoManger)\n }\n }\n\n initLocalState(\n provider,\n name,\n color,\n document.activeElement === editor.getRootElement(),\n awarenessData || {},\n )\n\n const onProviderDocReload = (ydoc: Doc) => {\n clearEditorSkipCollab(editor, binding.value)\n doc.value = ydoc\n docMap.set(id, ydoc)\n isReloadingDoc.value = true\n }\n\n provider.on('reload', onProviderDocReload)\n provider.on('status', onStatus)\n provider.on('sync', onSync)\n awareness.on('update', onAwarenessUpdate)\n // This updates the local editor state when we recieve updates from other clients\n root.getSharedType().observeDeep(onYjsTreeChanges)\n const removeListener = editor.registerUpdateListener(\n ({ prevEditorState, editorState, dirtyLeaves, dirtyElements, normalizedNodes, tags }) => {\n if (tags.has('skip-collab') === false) {\n syncLexicalUpdateToYjs(\n binding.value,\n provider,\n prevEditorState,\n editorState,\n dirtyElements,\n dirtyLeaves,\n normalizedNodes,\n tags,\n )\n }\n },\n )\n connect()\n\n return () => {\n if (isReloadingDoc.value === false)\n disconnect()\n\n provider.off('sync', onSync)\n provider.off('status', onStatus)\n provider.off('reload', onProviderDocReload)\n awareness.off('update', onAwarenessUpdate)\n root.getSharedType().unobserveDeep(onYjsTreeChanges)\n docMap.delete(id)\n removeListener()\n }\n })\n\n useEffect(() => {\n return editor.registerCommand(\n TOGGLE_CONNECT_COMMAND,\n (payload) => {\n if (connect !== undefined && disconnect !== undefined) {\n const shouldConnect = payload\n\n if (shouldConnect) {\n // eslint-disable-next-line no-console\n console.log('Collaboration connected!')\n connect()\n }\n else {\n // eslint-disable-next-line no-console\n console.log('Collaboration disconnected!')\n disconnect()\n }\n }\n\n return true\n },\n COMMAND_PRIORITY_EDITOR,\n )\n })\n\n return binding\n}\n\nexport function useYjsFocusTracking(\n editor: LexicalEditor,\n provider: Provider,\n name: string,\n color: string,\n awarenessData?: object,\n) {\n useEffect(() => {\n return mergeRegister(\n editor.registerCommand(\n FOCUS_COMMAND,\n () => {\n setLocalStateFocus(provider, name, color, true, awarenessData || {})\n return false\n },\n COMMAND_PRIORITY_EDITOR,\n ),\n editor.registerCommand(\n BLUR_COMMAND,\n () => {\n setLocalStateFocus(provider, name, color, false, awarenessData || {})\n return false\n },\n COMMAND_PRIORITY_EDITOR,\n ),\n )\n })\n}\n\nexport function useYjsHistory(editor: LexicalEditor, binding: Binding): () => void {\n const undoManager = computed(() => createUndoManager(binding, binding.root.getSharedType()))\n\n useEffect(() => {\n const undo = () => {\n undoManager.value.undo()\n }\n\n const redo = () => {\n undoManager.value.redo()\n }\n\n return mergeRegister(\n editor.registerCommand(\n UNDO_COMMAND,\n () => {\n undo()\n return true\n },\n COMMAND_PRIORITY_EDITOR,\n ),\n editor.registerCommand(\n REDO_COMMAND,\n () => {\n redo()\n return true\n },\n COMMAND_PRIORITY_EDITOR,\n ),\n )\n })\n const clearHistory = () => {\n undoManager.value.clear()\n }\n return clearHistory\n}\n\nfunction initializeEditor(\n editor: LexicalEditor,\n initialEditorState?: InitialEditorStateType,\n): void {\n editor.update(\n () => {\n const root = $getRoot()\n\n if (root.isEmpty()) {\n if (initialEditorState) {\n switch (typeof initialEditorState) {\n case 'string': {\n const parsedEditorState = editor.parseEditorState(initialEditorState)\n editor.setEditorState(parsedEditorState, { tag: 'history-merge' })\n break\n }\n case 'object': {\n editor.setEditorState(initialEditorState, { tag: 'history-merge' })\n break\n }\n case 'function': {\n editor.update(\n () => {\n const root1 = $getRoot()\n if (root1.isEmpty())\n initialEditorState(editor)\n },\n { tag: 'history-merge' },\n )\n break\n }\n }\n }\n else {\n const paragraph = $createParagraphNode()\n root.append(paragraph)\n const { activeElement } = document\n\n if (\n $getSelection() !== null\n || (activeElement !== null && activeElement === editor.getRootElement())\n ) {\n paragraph.select()\n }\n }\n }\n },\n {\n tag: 'history-merge',\n },\n )\n}\n\nfunction clearEditorSkipCollab(editor: LexicalEditor, binding: Binding) {\n // reset editor state\n editor.update(\n () => {\n const root = $getRoot()\n root.clear()\n root.select()\n },\n {\n tag: 'skip-collab',\n },\n )\n\n if (binding.cursors == null)\n return\n\n const cursors = binding.cursors\n\n if (cursors == null)\n return\n\n const cursorsContainer = binding.cursorsContainer\n\n if (cursorsContainer == null)\n return\n\n // reset cursors in dom\n const cursorsArr = Array.from(cursors.values())\n\n for (let i = 0; i < cursorsArr.length; i++) {\n const cursor = cursorsArr[i]\n const selection = cursor.selection\n\n if (selection && selection.selections !== null) {\n const selections = selection.selections\n\n for (let j = 0; j < selections.length; j++) cursorsContainer.removeChild(selections[i])\n }\n }\n}\n","import { defineComponent } from 'vue'\nimport { useDecorators, useLexicalComposer } from '../composables'\n\nexport default defineComponent({\n name: 'LexicalDecoratedTeleports',\n setup() {\n const editor = useLexicalComposer()\n const decorators = useDecorators(editor)\n\n return () => decorators.value\n },\n})\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useLexicalComposer, useMounted } from '../composables'\nimport { useCanShowPlaceholder } from '../composables/useCanShowPlaceholder'\nimport type { Props as ElementProps } from './LexicalContentEditableElement.vue'\nimport LexicalContentEditableElement from './LexicalContentEditableElement.vue'\n\ntype ContentEditableProps = Omit<ElementProps, 'editor' | 'placeholder'>\n\nwithDefaults(defineProps<ContentEditableProps>(), {\n role: 'textbox',\n spellcheck: true,\n})\n\nconst editor = useLexicalComposer()\nconst isEditable = ref(false)\nconst showPlaceholder = useCanShowPlaceholder(editor)\n\nuseMounted(() => {\n isEditable.value = editor.isEditable()\n return editor.registerEditableListener((currentIsEditable) => {\n isEditable.value = currentIsEditable\n })\n})\n</script>\n\n<template>\n <LexicalContentEditableElement\n :editor=\"editor\"\n v-bind=\"$props\"\n />\n <div\n v-if=\"showPlaceholder\"\n aria-hidden=\"true\"\n >\n <slot name=\"placeholder\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { LexicalEditor } from 'lexical'\nimport type { AriaAttributes } from 'vue'\nimport { computed, ref } from 'vue'\nimport { useMounted } from '../composables/useMounted'\nimport type { HTMLAttributes } from '../types'\n\nexport type Props = {\n editor: LexicalEditor\n ariaActiveDescendant?: AriaAttributes['aria-activedescendant']\n ariaAutoComplete?: AriaAttributes['aria-autocomplete']\n ariaControls?: AriaAttributes['aria-controls']\n ariaDescribedBy?: AriaAttributes['aria-describedby']\n ariaErrorMessage?: AriaAttributes['aria-errormessage']\n ariaExpanded?: AriaAttributes['aria-expanded']\n ariaInvalid?: AriaAttributes['aria-invalid']\n ariaLabel?: AriaAttributes['aria-label']\n ariaLabelledBy?: AriaAttributes['aria-labelledby']\n ariaMultiline?: AriaAttributes['aria-multiline']\n ariaOwns?: AriaAttributes['aria-owns']\n ariaRequired?: AriaAttributes['aria-required']\n dataTestid?: string\n} & Omit<HTMLAttributes, 'placeholder'>\n\nconst props = withDefaults(defineProps<Props>(), {\n role: 'textbox',\n spellcheck: true,\n})\n\nconst root = ref<HTMLElement | null>(null)\nconst isEditable = ref(props.editor.isEditable())\n\nconst otherAttrs = computed(() => {\n // for compat, only override if defined\n const ariaAttrs: Record<string, string | boolean> = {}\n if (props.ariaInvalid != null)\n ariaAttrs['aria-invalid'] = props.ariaInvalid\n if (props.ariaErrorMessage != null)\n ariaAttrs['aria-errormessage'] = props.ariaErrorMessage\n return {\n ...props,\n ...ariaAttrs,\n }\n})\n\nuseMounted(() => {\n function handleRef(rootElement: HTMLElement | null) {\n // defaultView is required for a root element.\n // In multi-window setups, the defaultView may not exist at certain points.\n if (\n rootElement\n && rootElement.ownerDocument\n && rootElement.ownerDocument.defaultView\n ) {\n props.editor.setRootElement(rootElement)\n }\n else {\n props.editor.setRootElement(null)\n }\n }\n\n handleRef(root.value)\n\n isEditable.value = props.editor.isEditable()\n return props.editor.registerEditableListener((currentIsEditable) => {\n isEditable.value = currentIsEditable\n })\n})\n</script>\n\n<template>\n <div\n ref=\"root\"\n v-bind=\"otherAttrs\"\n :aria-activedescendant=\"isEditable ? ariaActiveDescendant : undefined\"\n :aria-autocomplete=\"isEditable ? ariaAutoComplete : 'none'\"\n :aria-controls=\"isEditable ? ariaControls : undefined\"\n :aria-describedby=\"ariaDescribedBy\"\n :aria-expanded=\"isEditable && role === 'combobox' ? !!ariaExpanded : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledBy\"\n :aria-multiline=\"ariaMultiline\"\n :aria-owns=\"isEditable ? ariaOwns : undefined\"\n :aria-readonly=\"isEditable ? undefined : true\"\n :aria-required=\"ariaRequired\"\n :autocapitalize=\"autocapitalize\"\n :contenteditable=\"isEditable\"\n :data-testid=\"dataTestid\"\n :role=\"isEditable ? role : undefined\"\n :spellcheck=\"spellcheck\"\n :style=\"style\"\n :tabindex=\"tabindex\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport { onMounted, provide } from 'vue'\nimport type { EditorThemeClasses, HTMLConfig, Klass, LexicalEditor, LexicalNode, LexicalNodeReplacement } from 'lexical'\nimport { $createParagraphNode, $getRoot, $getSelection, createEditor } from 'lexical'\nimport { LexicalEditorProviderKey } from '../composables/inject'\nimport type { InitialEditorStateType } from '../types'\n\nexport type InitialConfigType = Readonly<{\n namespace: string\n nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>\n onError: (error: Error, editor: LexicalEditor) => void\n editable?: boolean\n theme?: EditorThemeClasses\n editorState?: InitialEditorStateType\n html?: HTMLConfig\n}>\n\nconst props = defineProps<{\n initialConfig: InitialConfigType\n}>()\n\nconst emit = defineEmits<{\n (e: 'error', error: Error, editor: LexicalEditor): void\n}>()\n\nconst HISTORY_MERGE_OPTIONS = { tag: 'history-merge' }\n\nconst {\n theme,\n namespace,\n nodes,\n onError,\n editorState: initialEditorState,\n html,\n} = props.initialConfig\n\nconst editor = createEditor({\n editable: props.initialConfig.editable,\n html,\n namespace,\n nodes,\n theme,\n onError(error) {\n emit('error', error, editor)\n onError?.(error, editor)\n },\n})\n\ninitializeEditor(editor, initialEditorState)\n\nfunction initializeEditor(\n editor: LexicalEditor,\n initialEditorState?: InitialEditorStateType,\n): void {\n if (initialEditorState === null)\n return\n\n if (initialEditorState === undefined) {\n editor.update(() => {\n const root = $getRoot()\n if (root.isEmpty()) {\n const paragraph = $createParagraphNode()\n root.append(paragraph)\n const activeElement = document.activeElement\n if (\n $getSelection() !== null\n || (activeElement !== null && activeElement === editor.getRootElement())\n ) {\n paragraph.select()\n }\n }\n }, HISTORY_MERGE_OPTIONS)\n }\n else if (initialEditorState !== null) {\n switch (typeof initialEditorState) {\n case 'string': {\n const parsedEditorState = editor.parseEditorState(initialEditorState)\n editor.setEditorState(parsedEditorState, HISTORY_MERGE_OPTIONS)\n break\n }\n case 'object': {\n editor.setEditorState(initialEditorState, HISTORY_MERGE_OPTIONS)\n break\n }\n case 'function': {\n editor.update(() => {\n const root = $getRoot()\n if (root.isEmpty())\n initialEditorState(editor)\n }, HISTORY_MERGE_OPTIONS)\n break\n }\n }\n }\n}\n\nprovide<LexicalEditor>(LexicalEditorProviderKey, editor)\n\nonMounted(() => {\n const isEditable = props.initialConfig.editable\n\n editor.setEditable(isEditable !== undefined ? isEditable : true)\n})\n</script>\n\n<template>\n <slot />\n</template>\n","<script setup lang=\"ts\">\nimport type { EditorState, LexicalEditor } from 'lexical'\nimport { watchEffect } from 'vue'\nimport { useLexicalComposer } from '../composables'\n\nconst props = withDefaults(defineProps<{\n ignoreInitialChange?: boolean\n ignoreSelectionChange?: boolean\n ignoreHistoryMergeTagChange?: boolean\n}>(), {\n ignoreInitialChange: true,\n ignoreSelectionChange: false,\n ignoreHistoryMergeTagChange: true,\n})\n\nconst emit = defineEmits<{\n (e: 'change', editorState: EditorState, editor: LexicalEditor, tags: Set<string>): void\n}>()\n\nconst editor = useLexicalComposer()\n\nwatchEffect(() => {\n return editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves, prevEditorState, tags }) => {\n if (\n (props.ignoreSelectionChange && dirtyElements.size === 0 && dirtyLeaves.size === 0)\n || (props.ignoreHistoryMergeTagChange && tags.has('history-merge'))\n || (props.ignoreInitialChange && prevEditorState.isEmpty())\n ) {\n return\n }\n\n emit('change', editorState, editor, tags)\n })\n})\n</script>\n\n<template />\n","<script setup lang=\"ts\">\nimport type {\n BaseSelection,\n EditorState,\n ElementNode,\n LexicalCommand,\n LexicalEditor,\n LexicalNode,\n RangeSelection,\n TextNode,\n} from 'lexical'\n\nimport { $generateHtmlFromNodes } from '@lexical/html'\nimport type { LinkNode } from '@lexical/link'\nimport { $isLinkNode } from '@lexical/link'\nimport { $isMarkNode } from '@lexical/mark'\nimport type { TableSelection } from '@lexical/table'\nimport { $isTableSelection } from '@lexical/table'\nimport { mergeRegister } from '@lexical/utils'\nimport {\n $getRoot,\n $getSelection,\n $isElementNode,\n $isNodeSelection,\n $isRangeSelection,\n $isTextNode,\n} from 'lexical'\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useLexicalCommandsLog, useLexicalComposer } from '../composables'\n\ndefineProps<{\n treeTypeButtonClassName: string\n timeTravelButtonClassName: string\n timeTravelPanelSliderClassName: string\n timeTravelPanelButtonClassName: string\n timeTravelPanelClassName: string\n viewClassName: string\n}>()\n\nconst NON_SINGLE_WIDTH_CHARS_REPLACEMENT: Readonly<Record<string, string>>\n = Object.freeze({\n '\\t': '\\\\t',\n '\\n': '\\\\n',\n })\nconst NON_SINGLE_WIDTH_CHARS_REGEX = new RegExp(\n Object.keys(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).join('|'),\n 'g',\n)\nconst SYMBOLS: Record<string, string> = Object.freeze({\n ancestorHasNextSibling: '|',\n ancestorIsLastChild: ' ',\n hasNextSibling: '├',\n isLastChild: '└',\n selectedChar: '^',\n selectedLine: '>',\n})\n\nfunction printRangeSelection(selection: RangeSelection): string {\n let res = ''\n\n const formatText = printFormatProperties(selection)\n\n res += `: range ${formatText !== '' ? `{ ${formatText} }` : ''}`\n\n const anchor = selection.anchor\n const focus = selection.focus\n const anchorOffset = anchor.offset\n const focusOffset = focus.offset\n\n res += `\\n ├ anchor { key: ${an