UNPKG

lexical-vue

Version:

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

154 lines (153 loc) 8.02 kB
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 };