lexical-vue
Version:
An extensible Vue 3 web text-editor based on Lexical.
150 lines (149 loc) • 7.8 kB
JavaScript
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 };