UNPKG

lexical-vue

Version:

An extensible Vue 3 web text-editor based on Lexical.

150 lines (149 loc) 7.8 kB
import { defineComponent, onMounted, onUnmounted, ref, renderSlot, unref, useSlots } from "vue"; import { $isHeadingNode, HeadingNode } from "@lexical/rich-text"; import { $getNextRightPreorderNode } from "@lexical/utils"; import { $getNodeByKey, $getRoot, $isElementNode, TextNode } from "lexical"; import { useLexicalComposer } from "./LexicalComposer.vine.js"; function toEntry(heading) { return [ heading.getKey(), heading.getTextContent(), heading.getTag() ]; } function $insertHeadingIntoTableOfContents(prevHeading, newHeading, currentTableOfContents) { if (null === newHeading) return currentTableOfContents; const newEntry = toEntry(newHeading); let newTableOfContents = []; if (null === prevHeading) { if (currentTableOfContents.length > 0 && currentTableOfContents[0][0] === newHeading.__key) return currentTableOfContents; newTableOfContents = [ newEntry, ...currentTableOfContents ]; } else for(let i = 0; i < currentTableOfContents.length; i++){ const key = currentTableOfContents[i][0]; newTableOfContents.push(currentTableOfContents[i]); if (key === prevHeading.getKey() && key !== newHeading.getKey()) { if (i + 1 < currentTableOfContents.length && currentTableOfContents[i + 1][0] === newHeading.__key) return currentTableOfContents; newTableOfContents.push(newEntry); } } return newTableOfContents; } function $deleteHeadingFromTableOfContents(key, currentTableOfContents) { const newTableOfContents = []; for (const heading of currentTableOfContents)if (heading[0] !== key) newTableOfContents.push(heading); return newTableOfContents; } function $updateHeadingInTableOfContents(heading, currentTableOfContents) { const newTableOfContents = []; for (const oldHeading of currentTableOfContents)if (oldHeading[0] === heading.getKey()) newTableOfContents.push(toEntry(heading)); else newTableOfContents.push(oldHeading); return newTableOfContents; } function $updateHeadingPosition(prevHeading, heading, currentTableOfContents) { const newTableOfContents = []; const newEntry = toEntry(heading); if (!prevHeading) newTableOfContents.push(newEntry); for (const oldHeading of currentTableOfContents)if (oldHeading[0] !== heading.getKey()) { newTableOfContents.push(oldHeading); if (prevHeading && oldHeading[0] === prevHeading.getKey()) newTableOfContents.push(newEntry); } return newTableOfContents; } function $getPreviousHeading(node) { let prevHeading = $getNextRightPreorderNode(node); while(null !== prevHeading && !$isHeadingNode(prevHeading))prevHeading = $getNextRightPreorderNode(prevHeading); return prevHeading; } const TableOfContentsPlugin = (()=>{ const __vine = defineComponent({ name: 'TableOfContentsPlugin', setup (__props, param) { let { expose: __expose } = param; __expose(); useSlots(); const tableOfContents = ref([]); const editor = useLexicalComposer(); onMounted(()=>{ let currentTableOfContents = []; editor.getEditorState().read(()=>{ const updateCurrentTableOfContents = (node)=>{ for (const child of node.getChildren())if ($isHeadingNode(child)) currentTableOfContents.push([ child.getKey(), child.getTextContent(), child.getTag() ]); else if ($isElementNode(child)) updateCurrentTableOfContents(child); }; updateCurrentTableOfContents($getRoot()); tableOfContents.value = currentTableOfContents; }); const removeRootUpdateListener = editor.registerUpdateListener((param)=>{ let { editorState, dirtyElements } = param; editorState.read(()=>{ const updateChildHeadings = (node)=>{ for (const child of node.getChildren())if ($isHeadingNode(child)) { const prevHeading = $getPreviousHeading(child); currentTableOfContents = $updateHeadingPosition(prevHeading, child, currentTableOfContents); tableOfContents.value = currentTableOfContents; } else if ($isElementNode(child)) updateChildHeadings(child); }; $getRoot().getChildren().forEach((node)=>{ if ($isElementNode(node) && dirtyElements.get(node.__key)) updateChildHeadings(node); }); }); }); const removeHeaderMutationListener = editor.registerMutationListener(HeadingNode, (mutatedNodes)=>{ editor.getEditorState().read(()=>{ for (const [nodeKey, mutation] of mutatedNodes)if ('created' === mutation) { const newHeading = $getNodeByKey(nodeKey); if (null !== newHeading) { const prevHeading = $getPreviousHeading(newHeading); currentTableOfContents = $insertHeadingIntoTableOfContents(prevHeading, newHeading, currentTableOfContents); } } else if ('destroyed' === mutation) currentTableOfContents = $deleteHeadingFromTableOfContents(nodeKey, currentTableOfContents); else if ('updated' === mutation) { const newHeading = $getNodeByKey(nodeKey); if (null !== newHeading) { const prevHeading = $getPreviousHeading(newHeading); currentTableOfContents = $updateHeadingPosition(prevHeading, newHeading, currentTableOfContents); } } tableOfContents.value = currentTableOfContents; }); }, { skipInitialization: true }); const removeTextNodeMutationListener = editor.registerMutationListener(TextNode, (mutatedNodes)=>{ editor.getEditorState().read(()=>{ for (const [nodeKey, mutation] of mutatedNodes)if ('updated' === mutation) { const currNode = $getNodeByKey(nodeKey); if (null !== currNode) { const parentNode = currNode.getParentOrThrow(); if ($isHeadingNode(parentNode)) { currentTableOfContents = $updateHeadingInTableOfContents(parentNode, currentTableOfContents); tableOfContents.value = currentTableOfContents; } } } }); }, { skipInitialization: true }); onUnmounted(()=>{ removeHeaderMutationListener(); removeTextNodeMutationListener(); removeRootUpdateListener(); }); }); return (_ctx, _cache)=>renderSlot(_ctx.$slots, "default", { tableOfContents: tableOfContents.value, editor: unref(editor) }); } }); __vine.__vue_vine = true; return __vine; })(); export { TableOfContentsPlugin };