diff --git a/packages/svelte-lexical/package.json b/packages/svelte-lexical/package.json index a7fe0ac..f9a7a38 100644 --- a/packages/svelte-lexical/package.json +++ b/packages/svelte-lexical/package.json @@ -96,6 +96,7 @@ "@lexical/utils": "0.14.3", "@lexical/yjs": "0.14.3", "@lexical/markdown": "0.14.3", + "@lexical/text": "0.14.3", "lexical": "0.14.3", "lodash-es": "^4.17.21", "prettier": "^3.2.5", diff --git a/packages/svelte-lexical/src/lib/components/toolbar/getSelectionInfo.ts b/packages/svelte-lexical/src/lib/components/toolbar/getSelectionInfo.ts index 4c14469..0f6b2da 100644 --- a/packages/svelte-lexical/src/lib/components/toolbar/getSelectionInfo.ts +++ b/packages/svelte-lexical/src/lib/components/toolbar/getSelectionInfo.ts @@ -1,5 +1,5 @@ import {$isAtNodeEnd as isAtNodeEnd} from '@lexical/selection'; -import type {RangeSelection} from 'lexical/LexicalSelection.js'; +import type {RangeSelection} from 'lexical'; export default function getSelectedNode(selection: RangeSelection) { const anchor = selection.anchor; diff --git a/packages/svelte-lexical/src/lib/core/LexicalText.ts b/packages/svelte-lexical/src/lib/core/LexicalText.ts deleted file mode 100644 index 51c0c71..0000000 --- a/packages/svelte-lexical/src/lib/core/LexicalText.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** @module @lexical/text */ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type {Klass, LexicalEditor, LexicalNode, RootNode} from 'lexical'; - -import { - $createTextNode, - $getRoot, - $isDecoratorNode, - $isElementNode, - $isParagraphNode, - $isTextNode, - TextNode, -} from 'lexical'; - -export type TextNodeWithOffset = { - node: TextNode; - offset: number; -}; - -/** - * Finds a TextNode with a size larger than targetCharacters and returns - * the node along with the remaining length of the text. - * @param root - The RootNode. - * @param targetCharacters - The number of characters whose TextNode must be larger than. - * @returns The TextNode and the intersections offset, or null if no TextNode is found. - */ -export function $findTextIntersectionFromCharacters( - root: RootNode, - targetCharacters: number, -): null | {node: TextNode; offset: number} { - let node = root.getFirstChild(); - let currentCharacters = 0; - - mainLoop: while (node !== null) { - if ($isElementNode(node)) { - const child = node.getFirstChild(); - - if (child !== null) { - node = child; - continue; - } - } else if ($isTextNode(node)) { - const characters = node.getTextContentSize(); - - if (currentCharacters + characters > targetCharacters) { - return {node, offset: targetCharacters - currentCharacters}; - } - currentCharacters += characters; - } - const sibling = node.getNextSibling(); - - if (sibling !== null) { - node = sibling; - continue; - } - let parent = node.getParent(); - while (parent !== null) { - const parentSibling = parent.getNextSibling(); - - if (parentSibling !== null) { - node = parentSibling; - continue mainLoop; - } - parent = parent.getParent(); - } - break; - } - - return null; -} - -/** - * Determines if the root has any text content and can trim any whitespace if it does. - * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor? - * @param trim - Should the root text have its whitespaced trimmed? Defaults to true. - * @returns true if text content is empty, false if there is text or isEditorComposing is true. - */ -export function $isRootTextContentEmpty( - isEditorComposing: boolean, - trim = true, -): boolean { - if (isEditorComposing) { - return false; - } - - let text = $rootTextContent(); - - if (trim) { - text = text.trim(); - } - - return text === ''; -} - -/** - * Returns a function that executes {@link $isRootTextContentEmpty} - * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor? - * @param trim - Should the root text have its whitespaced trimmed? Defaults to true. - * @returns A function that executes $isRootTextContentEmpty based on arguments. - */ -export function $isRootTextContentEmptyCurry( - isEditorComposing: boolean, - trim?: boolean, -): () => boolean { - return () => $isRootTextContentEmpty(isEditorComposing, trim); -} - -/** - * Returns the root's text content. - * @returns The root's text content. - */ -export function $rootTextContent(): string { - const root = $getRoot(); - - return root.getTextContent(); -} - -/** - * Determines if the input should show the placeholder. If anything is in - * in the root the placeholder should not be shown. - * @param isComposing - Is the editor in composition mode due to an active Input Method Editor? - * @returns true if the input should show the placeholder, false otherwise. - */ -export function $canShowPlaceholder(isComposing: boolean): boolean { - if (!$isRootTextContentEmpty(isComposing, false)) { - return false; - } - - const root = $getRoot(); - const children = root.getChildren(); - const childrenLength = children.length; - - if (childrenLength > 1) { - return false; - } - - for (let i = 0; i < childrenLength; i++) { - const topBlock = children[i]; - - if ($isDecoratorNode(topBlock)) { - return false; - } - - if ($isElementNode(topBlock)) { - if (!$isParagraphNode(topBlock)) { - return false; - } - - if (topBlock.__indent !== 0) { - return false; - } - - const topBlockChildren = topBlock.getChildren(); - const topBlockChildrenLength = topBlockChildren.length; - - for (let s = 0; s < topBlockChildrenLength; s++) { - const child = topBlockChildren[i]; - - if (!$isTextNode(child)) { - return false; - } - } - } - } - - return true; -} - -/** - * Returns a function that executes {@link $canShowPlaceholder} - * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor? - * @returns A function that executes $canShowPlaceholder with arguments. - */ -export function $canShowPlaceholderCurry( - isEditorComposing: boolean, -): () => boolean { - return () => $canShowPlaceholder(isEditorComposing); -} - -export type EntityMatch = {end: number; start: number}; - -/** - * Returns a tuple that can be rested (...) into mergeRegister to clean up - * node transforms listeners that transforms text into another node, eg. a HashtagNode. - * @example - * ```ts - * useEffect(() => { - return mergeRegister( - ...registerLexicalTextEntity(editor, getMatch, targetNode, createNode), - ); - }, [createNode, editor, getMatch, targetNode]); - * ``` - * Where targetNode is the type of node containing the text you want to transform (like a text input), - * then getMatch uses a regex to find a matching text and creates the proper node to include the matching text. - * @param editor - The lexical editor. - * @param getMatch - Finds a matching string that satisfies a regex expression. - * @param targetNode - The node type that contains text to match with. eg. HashtagNode - * @param createNode - A function that creates a new node to contain the matched text. eg createHashtagNode - * @returns An array containing the plain text and reverse node transform listeners. - */ -export function registerLexicalTextEntity( - editor: LexicalEditor, - getMatch: (text: string) => null | EntityMatch, - targetNode: Klass, - createNode: (textNode: TextNode) => T, -): Array<() => void> { - const isTargetNode = (node: LexicalNode | null | undefined): node is T => { - return node instanceof targetNode; - }; - - const replaceWithSimpleText = (node: TextNode): void => { - const textNode = $createTextNode(node.getTextContent()); - textNode.setFormat(node.getFormat()); - node.replace(textNode); - }; - - const getMode = (node: TextNode): number => { - return node.getLatest().__mode; - }; - - const textNodeTransform = (node: TextNode) => { - if (!node.isSimpleText()) { - return; - } - - const prevSibling = node.getPreviousSibling(); - let text = node.getTextContent(); - let currentNode = node; - let match; - - if ($isTextNode(prevSibling)) { - const previousText = prevSibling.getTextContent(); - const combinedText = previousText + text; - const prevMatch = getMatch(combinedText); - - if (isTargetNode(prevSibling)) { - if (prevMatch === null || getMode(prevSibling) !== 0) { - replaceWithSimpleText(prevSibling); - - return; - } else { - const diff = prevMatch.end - previousText.length; - - if (diff > 0) { - const concatText = text.slice(0, diff); - const newTextContent = previousText + concatText; - prevSibling.select(); - prevSibling.setTextContent(newTextContent); - - if (diff === text.length) { - node.remove(); - } else { - const remainingText = text.slice(diff); - node.setTextContent(remainingText); - } - - return; - } - } - } else if (prevMatch === null || prevMatch.start < previousText.length) { - return; - } - } - - // eslint-disable-next-line no-constant-condition - while (true) { - match = getMatch(text); - let nextText = match === null ? '' : text.slice(match.end); - text = nextText; - - if (nextText === '') { - const nextSibling = currentNode.getNextSibling(); - - if ($isTextNode(nextSibling)) { - nextText = - currentNode.getTextContent() + nextSibling.getTextContent(); - const nextMatch = getMatch(nextText); - - if (nextMatch === null) { - if (isTargetNode(nextSibling)) { - replaceWithSimpleText(nextSibling); - } else { - nextSibling.markDirty(); - } - - return; - } else if (nextMatch.start !== 0) { - return; - } - } - } else { - const nextMatch = getMatch(nextText); - - if (nextMatch !== null && nextMatch.start === 0) { - return; - } - } - - if (match === null) { - return; - } - - if ( - match.start === 0 && - $isTextNode(prevSibling) && - prevSibling.isTextEntity() - ) { - continue; - } - - let nodeToReplace; - - if (match.start === 0) { - [nodeToReplace, currentNode] = currentNode.splitText(match.end); - } else { - [, nodeToReplace, currentNode] = currentNode.splitText( - match.start, - match.end, - ); - } - - const replacementNode = createNode(nodeToReplace); - replacementNode.setFormat(nodeToReplace.getFormat()); - nodeToReplace.replace(replacementNode); - - if (currentNode == null) { - return; - } - } - }; - - const reverseNodeTransform = (node: T) => { - const text = node.getTextContent(); - const match = getMatch(text); - - if (match === null || match.start !== 0) { - replaceWithSimpleText(node); - - return; - } - - if (text.length > match.end) { - // This will split out the rest of the text as simple text - node.splitText(match.end); - - return; - } - - const prevSibling = node.getPreviousSibling(); - - if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) { - replaceWithSimpleText(prevSibling); - replaceWithSimpleText(node); - } - - const nextSibling = node.getNextSibling(); - - if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) { - replaceWithSimpleText(nextSibling); - - // This may have already been converted in the previous block - if (isTargetNode(node)) { - replaceWithSimpleText(node); - } - } - }; - - const removePlainTextTransform = editor.registerNodeTransform( - TextNode, - textNodeTransform, - ); - const removeReverseNodeTransform = editor.registerNodeTransform( - targetNode, - reverseNodeTransform, - ); - - return [removePlainTextTransform, removeReverseNodeTransform]; -} diff --git a/packages/svelte-lexical/src/lib/core/plugins/HashtagPlugin.svelte b/packages/svelte-lexical/src/lib/core/plugins/HashtagPlugin.svelte index 0a31c57..32905d3 100644 --- a/packages/svelte-lexical/src/lib/core/plugins/HashtagPlugin.svelte +++ b/packages/svelte-lexical/src/lib/core/plugins/HashtagPlugin.svelte @@ -4,7 +4,7 @@ $createHashtagNode as createHashtagNode, HashtagNode, } from './HashtagNode.js'; - import {registerLexicalTextEntity} from '../LexicalText.js'; + import {registerLexicalTextEntity} from '@lexical/text'; import {onMount} from 'svelte'; import {mergeRegister} from '@lexical/utils'; import {getEditor} from '../composerContext.js'; diff --git a/packages/svelte-lexical/src/lib/core/plugins/KeywordPlugin.svelte b/packages/svelte-lexical/src/lib/core/plugins/KeywordPlugin.svelte index 72d0593..92e9b2b 100644 --- a/packages/svelte-lexical/src/lib/core/plugins/KeywordPlugin.svelte +++ b/packages/svelte-lexical/src/lib/core/plugins/KeywordPlugin.svelte @@ -7,7 +7,7 @@ } from './KeywordNode.js'; import {getEditor} from '../composerContext.js'; import {mergeRegister} from '@lexical/utils'; - import {registerLexicalTextEntity} from '../LexicalText.js'; + import {registerLexicalTextEntity} from '@lexical/text'; import {onMount} from 'svelte'; export let keywordsRegex: RegExp; diff --git a/packages/svelte-lexical/src/lib/core/plugins/PlaceHolder.svelte b/packages/svelte-lexical/src/lib/core/plugins/PlaceHolder.svelte index d6a9a35..d43154e 100644 --- a/packages/svelte-lexical/src/lib/core/plugins/PlaceHolder.svelte +++ b/packages/svelte-lexical/src/lib/core/plugins/PlaceHolder.svelte @@ -1,6 +1,6 @@