lexical-vue
Version:
An extensible Vue 3 web text-editor based on Lexical.
154 lines (153 loc) • 8.02 kB
JavaScript
import { $createOverflowNode, $isOverflowNode, OverflowNode } from "@lexical/overflow";
import { $rootTextContent } from "@lexical/text";
import { $dfs, mergeRegister } from "@lexical/utils";
import { $getSelection, $isElementNode, $isLeafNode, $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_LOW, DELETE_CHARACTER_COMMAND } from "lexical";
import tiny_invariant from "tiny-invariant";
import { toValue, watchEffect } from "vue";
function useCharacterLimit(editor, maxCharacters, optional = Object.freeze({})) {
watchEffect((onInvalidate)=>{
if (!editor.hasNodes([
OverflowNode
])) tiny_invariant(false, 'useCharacterLimit: OverflowNode not registered on editor');
const { strlen = (input)=>input.length, remainingCharacters = (_characters)=>{} } = toValue(optional);
let text = editor.getEditorState().read($rootTextContent);
let lastComputedTextLength = 0;
const unregister = mergeRegister(editor.registerTextContentListener((currentText)=>{
text = currentText;
}), editor.registerUpdateListener(({ dirtyLeaves })=>{
const isComposing = editor.isComposing();
const hasDirtyLeaves = dirtyLeaves.size > 0;
if (isComposing || !hasDirtyLeaves) return;
const textLength = strlen(text);
const textLengthAboveThreshold = textLength > toValue(maxCharacters) || null !== lastComputedTextLength && lastComputedTextLength > toValue(maxCharacters);
const diff = toValue(maxCharacters) - textLength;
remainingCharacters(diff);
if (null === lastComputedTextLength || textLengthAboveThreshold) {
const offset = findOffset(text, toValue(maxCharacters), strlen);
editor.update(()=>{
$wrapOverflowedNodes(offset);
}, {
tag: 'history-merge'
});
}
lastComputedTextLength = textLength;
}), editor.registerCommand(DELETE_CHARACTER_COMMAND, (isBackward)=>{
const selection = $getSelection();
if (!$isRangeSelection(selection)) return false;
const anchorNode = selection.anchor.getNode();
const overflow = anchorNode.getParent();
const overflowParent = overflow ? overflow.getParent() : null;
const parentNext = overflowParent ? overflowParent.getNextSibling() : null;
selection.deleteCharacter(isBackward);
if (overflowParent && overflowParent.isEmpty()) overflowParent.remove();
else if ($isElementNode(parentNext) && parentNext.isEmpty()) parentNext.remove();
return true;
}, COMMAND_PRIORITY_LOW));
onInvalidate(unregister);
});
}
function findOffset(text, maxCharacters, strlen) {
const Segmenter = Intl.Segmenter;
let offsetUtf16 = 0;
let offset = 0;
if ('function' == typeof Segmenter) {
const segmenter = new Segmenter();
const graphemes = segmenter.segment(text);
for (const { segment: grapheme } of graphemes){
const nextOffset = offset + strlen(grapheme);
if (nextOffset > maxCharacters) break;
offset = nextOffset;
offsetUtf16 += grapheme.length;
}
} else {
const codepoints = Array.from(text);
const codepointsLength = codepoints.length;
for(let i = 0; i < codepointsLength; i++){
const codepoint = codepoints[i];
const nextOffset = offset + strlen(codepoint);
if (nextOffset > maxCharacters) break;
offset = nextOffset;
offsetUtf16 += codepoint.length;
}
}
return offsetUtf16;
}
function $wrapOverflowedNodes(offset) {
const dfsNodes = $dfs();
const dfsNodesLength = dfsNodes.length;
let accumulatedLength = 0;
for(let i = 0; i < dfsNodesLength; i += 1){
const { node } = dfsNodes[i];
if ($isOverflowNode(node)) {
const previousLength = accumulatedLength;
const nextLength = accumulatedLength + node.getTextContentSize();
if (nextLength <= offset) {
const parent = node.getParent();
const previousSibling = node.getPreviousSibling();
const nextSibling = node.getNextSibling();
$unwrapNode(node);
const selection = $getSelection();
if ($isRangeSelection(selection) && (!selection.anchor.getNode().isAttached() || !selection.focus.getNode().isAttached())) {
if ($isTextNode(previousSibling)) previousSibling.select();
else if ($isTextNode(nextSibling)) nextSibling.select();
else if (null !== parent) parent.select();
}
} else if (previousLength < offset) {
const descendant = node.getFirstDescendant();
const descendantLength = null !== descendant ? descendant.getTextContentSize() : 0;
const previousPlusDescendantLength = previousLength + descendantLength;
const firstDescendantIsSimpleText = $isTextNode(descendant) && descendant.isSimpleText();
const firstDescendantDoesNotOverflow = previousPlusDescendantLength <= offset;
if (firstDescendantIsSimpleText || firstDescendantDoesNotOverflow) $unwrapNode(node);
}
} else if ($isLeafNode(node)) {
const previousAccumulatedLength = accumulatedLength;
accumulatedLength += node.getTextContentSize();
if (accumulatedLength > offset && !$isOverflowNode(node.getParent())) {
const previousSelection = $getSelection();
let overflowNode;
if (previousAccumulatedLength < offset && $isTextNode(node) && node.isSimpleText()) {
const [, overflowedText] = node.splitText(offset - previousAccumulatedLength);
overflowNode = $wrapNode(overflowedText);
} else overflowNode = $wrapNode(node);
if (null !== previousSelection) $setSelection(previousSelection);
mergePrevious(overflowNode);
}
}
}
}
function $wrapNode(node) {
const overflowNode = $createOverflowNode();
node.insertBefore(overflowNode);
overflowNode.append(node);
return overflowNode;
}
function $unwrapNode(node) {
const children = node.getChildren();
const childrenLength = children.length;
for(let i = 0; i < childrenLength; i++)node.insertBefore(children[i]);
node.remove();
return childrenLength > 0 ? children[childrenLength - 1] : null;
}
function mergePrevious(overflowNode) {
const previousNode = overflowNode.getPreviousSibling();
if (!$isOverflowNode(previousNode)) return;
const firstChild = overflowNode.getFirstChild();
const previousNodeChildren = previousNode.getChildren();
const previousNodeChildrenLength = previousNodeChildren.length;
if (null === firstChild) overflowNode.append(...previousNodeChildren);
else for(let i = 0; i < previousNodeChildrenLength; i++)firstChild.insertBefore(previousNodeChildren[i]);
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchor = selection.anchor;
const anchorNode = anchor.getNode();
const focus = selection.focus;
const focusNode = anchor.getNode();
if (anchorNode.is(previousNode)) anchor.set(overflowNode.getKey(), anchor.offset, 'element');
else if (anchorNode.is(overflowNode)) anchor.set(overflowNode.getKey(), previousNodeChildrenLength + anchor.offset, 'element');
if (focusNode.is(previousNode)) focus.set(overflowNode.getKey(), focus.offset, 'element');
else if (focusNode.is(overflowNode)) focus.set(overflowNode.getKey(), previousNodeChildrenLength + focus.offset, 'element');
}
previousNode.remove();
}
export { mergePrevious, useCharacterLimit };