From 7646a854e77ed7e4c6ea81017886aff3171a4578 Mon Sep 17 00:00:00 2001 From: Nadine Nguyen Date: Sun, 8 Sep 2024 03:48:06 +1000 Subject: [PATCH 001/133] [lexical-code][breaking change] Bug Fix: explicitly import instead of `window.` to support code nodes in nodejs (#6562) --- packages/lexical-code/src/CodeHighlighterPrism.ts | 5 +++++ packages/lexical-code/src/CodeNode.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/lexical-code/src/CodeHighlighterPrism.ts b/packages/lexical-code/src/CodeHighlighterPrism.ts index 3f1d679cbe1..9ec68588456 100644 --- a/packages/lexical-code/src/CodeHighlighterPrism.ts +++ b/packages/lexical-code/src/CodeHighlighterPrism.ts @@ -24,9 +24,14 @@ import 'prismjs/components/prism-swift'; import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-java'; import 'prismjs/components/prism-cpp'; +import {CAN_USE_DOM} from 'shared/canUseDOM'; declare global { interface Window { Prism: typeof import('prismjs'); } } + +export const Prism: typeof import('prismjs') = CAN_USE_DOM + ? window.Prism + : (global as unknown as {Prism: typeof import('prismjs')}).Prism; diff --git a/packages/lexical-code/src/CodeNode.ts b/packages/lexical-code/src/CodeNode.ts index 49af055d28d..728e23e64b1 100644 --- a/packages/lexical-code/src/CodeNode.ts +++ b/packages/lexical-code/src/CodeNode.ts @@ -22,8 +22,6 @@ import type { TabNode, } from 'lexical'; -import './CodeHighlighterPrism'; - import {addClassNamesToElement, isHTMLElement} from '@lexical/utils'; import { $applyNodeReplacement, @@ -35,6 +33,7 @@ import { ElementNode, } from 'lexical'; +import {Prism} from './CodeHighlighterPrism'; import { $createCodeHighlightNode, $isCodeHighlightNode, @@ -53,7 +52,7 @@ const isLanguageSupportedByPrism = ( ): boolean => { try { // eslint-disable-next-line no-prototype-builtins - return language ? window.Prism.languages.hasOwnProperty(language) : false; + return language ? Prism.languages.hasOwnProperty(language) : false; } catch { return false; } From 83d3cee43a11e0193184063fbe7526d495ed577d Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Sat, 7 Sep 2024 19:40:57 +0100 Subject: [PATCH 002/133] Multiple update tags (#6507) Co-authored-by: Bob Ippolito Co-authored-by: Ivaylo Pavlov --- packages/lexical/flow/Lexical.js.flow | 2 +- packages/lexical/src/LexicalEditor.ts | 2 +- packages/lexical/src/LexicalUpdates.ts | 27 +++++++++++-------- .../src/__tests__/unit/LexicalEditor.test.tsx | 23 ++++++++++++++++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 3a8937cedfa..33e5a952bd1 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -215,7 +215,7 @@ type EditorReadOptions = { }; type EditorUpdateOptions = { onUpdate?: () => void, - tag?: string, + tag?: string | Array, skipTransforms?: true, discrete?: true, }; diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index e86e7b215e4..6016ae84956 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -80,7 +80,7 @@ export type TextNodeThemeClasses = { export type EditorUpdateOptions = { onUpdate?: () => void; skipTransforms?: true; - tag?: string; + tag?: string | Array; discrete?: true; }; diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 38e83cc56e8..11296a2be5a 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -219,6 +219,20 @@ function $normalizeAllDirtyTextNodes( } } +function addTags(editor: LexicalEditor, tags: undefined | string | string[]) { + if (!tags) { + return; + } + const updateTags = editor._updateTags; + let tags_ = tags; + if (!Array.isArray(tags)) { + tags_ = [tags]; + } + for (const tag of tags_) { + updateTags.add(tag); + } +} + /** * Transform heuristic: * 1. We transform leaves first. If transforms generate additional dirty nodes we repeat step 1. @@ -829,11 +843,9 @@ function processNestedUpdates( const [nextUpdateFn, options] = queuedUpdate; let onUpdate; - let tag; if (options !== undefined) { onUpdate = options.onUpdate; - tag = options.tag; if (options.skipTransforms) { skipTransforms = true; @@ -851,9 +863,7 @@ function processNestedUpdates( editor._deferred.push(onUpdate); } - if (tag) { - editor._updateTags.add(tag); - } + addTags(editor, options.tag); } nextUpdateFn(); @@ -870,17 +880,12 @@ function $beginUpdate( ): void { const updateTags = editor._updateTags; let onUpdate; - let tag; let skipTransforms = false; let discrete = false; if (options !== undefined) { onUpdate = options.onUpdate; - tag = options.tag; - - if (tag != null) { - updateTags.add(tag); - } + addTags(editor, options.tag); skipTransforms = options.skipTransforms || false; discrete = options.discrete || false; diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index b7714f03532..4b89e56c232 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -61,6 +61,7 @@ import {createRoot, Root} from 'react-dom/client'; import invariant from 'shared/invariant'; import * as ReactTestUtils from 'shared/react-test-utils'; +import {emptyFunction} from '../../LexicalUtils'; import { $createTestDecoratorNode, $createTestElementNode, @@ -2041,6 +2042,28 @@ describe('LexicalEditor tests', () => { ]); }); + it('multiple update tags', async () => { + init(); + const $mutateSomething = $createTextNode; + + editor.update($mutateSomething, { + tag: ['a', 'b'], + }); + expect(editor._updateTags).toEqual(new Set(['a', 'b'])); + editor.update( + () => { + editor.update(emptyFunction, {tag: ['e', 'f']}); + }, + { + tag: ['c', 'd'], + }, + ); + expect(editor._updateTags).toEqual(new Set(['a', 'b', 'c', 'd', 'e', 'f'])); + + await Promise.resolve(); + expect(editor._updateTags).toEqual(new Set([])); + }); + it('mutation listeners does not trigger when other node types are mutated', async () => { init(); From 7aec43dcdaec5d7c57d69ecff89002645c70f611 Mon Sep 17 00:00:00 2001 From: Vinay Kushwaha <51132814+iamvinayvk@users.noreply.github.com> Date: Sun, 8 Sep 2024 00:30:54 +0530 Subject: [PATCH 003/133] [lexical-playground] Fix: Poll Option not clickable at some place after checked state (#6609) --- packages/lexical-playground/src/nodes/PollNode.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical-playground/src/nodes/PollNode.css b/packages/lexical-playground/src/nodes/PollNode.css index bfa30817bd0..2b41d80cdb6 100644 --- a/packages/lexical-playground/src/nodes/PollNode.css +++ b/packages/lexical-playground/src/nodes/PollNode.css @@ -105,6 +105,7 @@ margin: 0; transform: rotate(45deg); border-width: 0 2px 2px 0; + pointer-events: none; } .PollNode__optionCheckbox { border: 0px; From f7211e2945fd47d8b736f72c714aeebb746e23ec Mon Sep 17 00:00:00 2001 From: Sherry Date: Tue, 10 Sep 2024 21:02:59 +0800 Subject: [PATCH 004/133] [lexical-markdown] update markdown flow api (#6615) --- .../flow/LexicalMarkdown.js.flow | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow index 7318dda1171..7f2f34483dc 100644 --- a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow +++ b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow @@ -17,6 +17,7 @@ import type { export type Transformer = | ElementTransformer + | MultilineElementTransformer | TextFormatTransformer | TextMatchTransformer; @@ -32,10 +33,34 @@ export type ElementTransformer = { children: Array, match: Array, isImport: boolean, - ) => void, + ) => boolean | void, type: 'element', }; +export type MultilineElementTransformer = { + dependencies: Array>; + export?: ( + node: LexicalNode, + traverseChildren: (node: ElementNode) => string, + ) => string | null; + regExpStart: RegExp; + regExpEnd?: + | RegExp + | { + optional?: true; + regExp: RegExp; + }; + replace: ( + rootNode: ElementNode, + children: Array | null, + startMatch: Array, + endMatch: Array | null, + linesInBetween: Array | null, + isImport: boolean, + ) => boolean | void; + type: 'multilineElement'; +}; + export type TextFormatTransformer = $ReadOnly<{ format: $ReadOnlyArray, tag: string, @@ -90,7 +115,7 @@ declare export var ITALIC_UNDERSCORE: TextFormatTransformer; declare export var STRIKETHROUGH: TextFormatTransformer; declare export var UNORDERED_LIST: ElementTransformer; -declare export var CODE: ElementTransformer; +declare export var CODE: MultilineElementTransformer; declare export var HEADING: ElementTransformer; declare export var ORDERED_LIST: ElementTransformer; declare export var QUOTE: ElementTransformer; From 7d5820eb77679952f8b84ffc941f7b8249cee942 Mon Sep 17 00:00:00 2001 From: Melissa Freiser <17129292+mofoshow@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:19:24 +0200 Subject: [PATCH 005/133] Expose getStyleObjectFromCss in @lexical/selection (#6612) Co-authored-by: Bob Ippolito Co-authored-by: Sherry --- packages/lexical-selection/flow/LexicalSelection.js.flow | 1 + packages/lexical-selection/src/index.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/lexical-selection/flow/LexicalSelection.js.flow b/packages/lexical-selection/flow/LexicalSelection.js.flow index 7802dc55721..d1d68d66376 100644 --- a/packages/lexical-selection/flow/LexicalSelection.js.flow +++ b/packages/lexical-selection/flow/LexicalSelection.js.flow @@ -20,6 +20,7 @@ declare export function $cloneWithProperties(node: T): T; declare export function getStyleObjectFromCSS(css: string): { [string]: string, }; +declare export function getCSSFromStyleObject(styles: Record): string; declare export function $patchStyleText( selection: BaseSelection, patch: { diff --git a/packages/lexical-selection/src/index.ts b/packages/lexical-selection/src/index.ts index b2d18b1645a..d901ab4d4d9 100644 --- a/packages/lexical-selection/src/index.ts +++ b/packages/lexical-selection/src/index.ts @@ -26,6 +26,7 @@ import { import { createDOMRange, createRectsFromDOMRange, + getCSSFromStyleObject, getStyleObjectFromCSS, } from './utils'; @@ -53,4 +54,9 @@ export { $wrapNodes, }; -export {createDOMRange, createRectsFromDOMRange, getStyleObjectFromCSS}; +export { + createDOMRange, + createRectsFromDOMRange, + getCSSFromStyleObject, + getStyleObjectFromCSS, +}; From 8123ca7c5aef4d35ede68457f1799f7315b80bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:20:25 -0300 Subject: [PATCH 006/133] [lexical-markdown] Fix: normalize markdown in $convertFromMarkdownString to comply with CommonMark spec (#6608) --- .../src/MarkdownTransformers.ts | 66 ++++++++++++++-- .../__tests__/unit/LexicalMarkdown.test.ts | 79 +++++++++++++++++-- packages/lexical-markdown/src/index.ts | 4 +- .../__tests__/e2e/Markdown.spec.mjs | 11 +-- 4 files changed, 138 insertions(+), 22 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index fc0662726ae..efff5770e18 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -153,6 +153,14 @@ export type TextMatchTransformer = Readonly<{ type: 'text-match'; }>; +const ORDERED_LIST_REGEX = /^(\s*)(\d{1,})\.\s/; +const UNORDERED_LIST_REGEX = /^(\s*)[-*+]\s/; +const CHECK_LIST_REGEX = /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i; +const HEADING_REGEX = /^(#{1,6})\s/; +const QUOTE_REGEX = /^>\s/; +const CODE_START_REGEX = /^[ \t]*```(\w+)?/; +const CODE_END_REGEX = /[ \t]*```$/; + const createBlockNode = ( createNode: (match: Array) => ElementNode, ): ElementTransformer['replace'] => { @@ -266,7 +274,7 @@ export const HEADING: ElementTransformer = { const level = Number(node.getTag().slice(1)); return '#'.repeat(level) + ' ' + exportChildren(node); }, - regExp: /^(#{1,6})\s/, + regExp: HEADING_REGEX, replace: createBlockNode((match) => { const tag = ('h' + match[1].length) as HeadingTagType; return $createHeadingNode(tag); @@ -288,7 +296,7 @@ export const QUOTE: ElementTransformer = { } return output.join('\n'); }, - regExp: /^>\s/, + regExp: QUOTE_REGEX, replace: (parentNode, children, _match, isImport) => { if (isImport) { const previousNode = parentNode.getPreviousSibling(); @@ -328,9 +336,9 @@ export const CODE: MultilineElementTransformer = { }, regExpEnd: { optional: true, - regExp: /[ \t]*```$/, + regExp: CODE_END_REGEX, }, - regExpStart: /^[ \t]*```(\w+)?/, + regExpStart: CODE_START_REGEX, replace: ( rootNode, children, @@ -399,7 +407,7 @@ export const UNORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)[-*+]\s/, + regExp: UNORDERED_LIST_REGEX, replace: listReplace('bullet'), type: 'element', }; @@ -409,7 +417,7 @@ export const CHECK_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i, + regExp: CHECK_LIST_REGEX, replace: listReplace('check'), type: 'element', }; @@ -419,7 +427,7 @@ export const ORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)(\d{1,})\.\s/, + regExp: ORDERED_LIST_REGEX, replace: listReplace('number'), type: 'element', }; @@ -519,3 +527,47 @@ export const LINK: TextMatchTransformer = { trigger: ')', type: 'text-match', }; + +export function normalizeMarkdown(input: string): string { + const lines = input.split('\n'); + let inCodeBlock = false; + const sanitizedLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lastLine = sanitizedLines[sanitizedLines.length - 1]; + + // Detect the start or end of a code block + if (CODE_START_REGEX.test(line) || CODE_END_REGEX.test(line)) { + inCodeBlock = !inCodeBlock; + sanitizedLines.push(line); + continue; + } + + // If we are inside a code block, keep the line unchanged + if (inCodeBlock) { + sanitizedLines.push(line); + continue; + } + + // In markdown the concept of "empty paragraphs" does not exist. + // Blocks must be separated by an empty line. Non-empty adjacent lines must be merged. + if ( + line === '' || + lastLine === '' || + !lastLine || + HEADING_REGEX.test(lastLine) || + HEADING_REGEX.test(line) || + QUOTE_REGEX.test(line) || + ORDERED_LIST_REGEX.test(line) || + UNORDERED_LIST_REGEX.test(line) || + CHECK_LIST_REGEX.test(line) + ) { + sanitizedLines.push(line); + } else { + sanitizedLines[sanitizedLines.length - 1] = lastLine + line; + } + } + + return sanitizedLines.join('\n'); +} diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index 421394fcbf1..8d1a3a7bdb2 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -22,7 +22,10 @@ import { Transformer, TRANSFORMERS, } from '../..'; -import {MultilineElementTransformer} from '../../MarkdownTransformers'; +import { + MultilineElementTransformer, + normalizeMarkdown, +} from '../../MarkdownTransformers'; // Matches html within a mdx file const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { @@ -92,19 +95,36 @@ describe('Markdown', () => { html: '
Hello world
', md: '###### Hello world', }, + { + // Multiline paragraphs: https://spec.commonmark.org/dingus/?text=Hello%0Aworld%0A! + html: '

Helloworld!

', + md: ['Hello', 'world', '!'].join('\n'), + skipExport: true, + }, { // Multiline paragraphs - html: '

Hello
world
!

', + // TO-DO: It would be nice to support also hard line breaks (
) as \ or double spaces + // See https://spec.commonmark.org/0.31.2/#hard-line-breaks. + // Example: '

Hello\\\nworld\\\n!

', + html: '

Hello
world
!

', md: ['Hello', 'world', '!'].join('\n'), + skipImport: true, }, { html: '
Hello
world!
', md: '> Hello\n> world!', }, + // TO-DO:
should be preserved + // { + // html: '
  • Hello
  • world
    !
    !
', + // md: '- Hello\n- world
!
!', + // skipImport: true, + // }, { - // Multiline list items - html: '
  • Hello
  • world
    !
    !
', + // Multiline list items: https://spec.commonmark.org/dingus/?text=-%20Hello%0A-%20world%0A!%0A! + html: '
  • Hello
  • world!!
', md: '- Hello\n- world\n!\n!', + skipExport: true, }, { html: '
  • Hello
  • world
', @@ -274,8 +294,8 @@ describe('Markdown', () => { skipExport: true, }, { - // Import only: multiline quote will be prefixed with ">" on each line during export - html: '
Hello
world
!
', + // https://spec.commonmark.org/dingus/?text=%3E%20Hello%0Aworld%0A! + html: '
Helloworld!
', md: '> Hello\nworld\n!', skipExport: true, }, @@ -298,8 +318,9 @@ describe('Markdown', () => { }, { customTransformers: [MDX_HTML_TRANSFORMER], - html: '

Some HTML in mdx:

From HTML: Line 1\nSome Text
', + html: '

Some HTML in mdx:

From HTML: Line 1Some Text
', md: 'Some HTML in mdx:\n\nLine 1\nSome Text', + skipExport: true, }, ]; @@ -407,3 +428,47 @@ describe('Markdown', () => { }); } }); + +describe('normalizeMarkdown', () => { + it('should combine lines separated by a single \n unless they are in a codeblock', () => { + const markdown = ` +1 +2 + +3 + +\`\`\`md +1 +2 + +3 +\`\`\` + +\`\`\`js +1 +2 + +3 +\`\`\` +`; + expect(normalizeMarkdown(markdown)).toBe(` +12 + +3 + +\`\`\`md +1 +2 + +3 +\`\`\` + +\`\`\`js +1 +2 + +3 +\`\`\` +`); + }); +}); diff --git a/packages/lexical-markdown/src/index.ts b/packages/lexical-markdown/src/index.ts index dac5b260478..3fc2f21da17 100644 --- a/packages/lexical-markdown/src/index.ts +++ b/packages/lexical-markdown/src/index.ts @@ -31,6 +31,7 @@ import { ITALIC_STAR, ITALIC_UNDERSCORE, LINK, + normalizeMarkdown, ORDERED_LIST, QUOTE, STRIKETHROUGH, @@ -82,11 +83,12 @@ function $convertFromMarkdownString( node?: ElementNode, shouldPreserveNewLines = false, ): void { + const sanitizedMarkdown = normalizeMarkdown(markdown); const importMarkdown = createMarkdownImport( transformers, shouldPreserveNewLines, ); - return importMarkdown(markdown, node); + return importMarkdown(sanitizedMarkdown, node); } /** diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index b434d62be55..505b038ce28 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -1310,7 +1310,6 @@ const IMPORTED_MARKDOWN_HTML = html` bold italic strikethrough text, -
@@ -1408,9 +1407,7 @@ const IMPORTED_MARKDOWN_HTML = html` dir="ltr"> Blockquotes text goes here
- And second -
- line after + And secondline after
- And can be nested -
- and multiline as well + + And can be nested and multiline as well + From 46929e02e3182f0522cc2ff19c664d4914c93a48 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 10 Sep 2024 07:32:06 -0700 Subject: [PATCH 007/133] [lexical] Chore: Add more helpful invariants to $applyNodeReplacement (#6567) Co-authored-by: Sherry --- .../docs/concepts/node-replacement.md | 3 +- .../docs/concepts/serialization.md | 9 +- packages/lexical/src/LexicalUtils.ts | 57 +++- .../src/__tests__/unit/LexicalUtils.test.ts | 299 +++++++++++++++++- 4 files changed, 341 insertions(+), 27 deletions(-) diff --git a/packages/lexical-website/docs/concepts/node-replacement.md b/packages/lexical-website/docs/concepts/node-replacement.md index 386a7e83b5d..8090b41ac05 100644 --- a/packages/lexical-website/docs/concepts/node-replacement.md +++ b/packages/lexical-website/docs/concepts/node-replacement.md @@ -16,7 +16,8 @@ const editorConfig = { replace: ParagraphNode, with: (node: ParagraphNode) => { return new CustomParagraphNode(); - } + }, + withKlass: CustomParagraphNode, } ] } diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 64e7fe14193..03ed5f924e8 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -295,7 +295,11 @@ const initialConfig: InitialConfigType = { onError: (error: any) => console.log(error), nodes: [ ExtendedTextNode, - { replace: TextNode, with: (node: TextNode) => new ExtendedTextNode(node.__text) }, + { + replace: TextNode, + with: (node: TextNode) => new ExtendedTextNode(node.__text), + withKlass: ExtendedTextNode, + }, ListNode, ListItemNode, ] @@ -306,6 +310,7 @@ and create a new Extended Text Node plugin ```js import { + $applyNodeReplacement, $isTextNode, DOMConversion, DOMConversionMap, @@ -378,7 +383,7 @@ export class ExtendedTextNode extends TextNode { } export function $createExtendedTextNode(text: string): ExtendedTextNode { - return new ExtendedTextNode(text); + return $applyNodeReplacement(new ExtendedTextNode(text)); } export function $isExtendedTextNode(node: LexicalNode | null | undefined): node is ExtendedTextNode { diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 7e3b2e49e1b..65b45cf7362 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1405,30 +1405,53 @@ export function $copyNode(node: T): T { return copy; } -export function $applyNodeReplacement( - node: LexicalNode, -): N { +export function $applyNodeReplacement(node: N): N { const editor = getActiveEditor(); const nodeType = node.constructor.getType(); const registeredNode = editor._nodes.get(nodeType); - if (registeredNode === undefined) { - invariant( - false, - '$initializeNode failed. Ensure node has been registered to the editor. You can do this by passing the node class via the "nodes" array in the editor config.', - ); - } - const replaceFunc = registeredNode.replace; - if (replaceFunc !== null) { - const replacementNode = replaceFunc(node) as N; - if (!(replacementNode instanceof node.constructor)) { + invariant( + registeredNode !== undefined, + '$applyNodeReplacement node %s with type %s must be registered to the editor. You can do this by passing the node class via the "nodes" array in the editor config.', + node.constructor.name, + nodeType, + ); + const {replace, replaceWithKlass} = registeredNode; + if (replace !== null) { + const replacementNode = replace(node); + const replacementNodeKlass = replacementNode.constructor; + if (replaceWithKlass !== null) { invariant( - false, - '$initializeNode failed. Ensure replacement node is a subclass of the original node.', + replacementNode instanceof replaceWithKlass, + '$applyNodeReplacement failed. Expected replacement node to be an instance of %s with type %s but returned %s with type %s from original node %s with type %s', + replaceWithKlass.name, + replaceWithKlass.getType(), + replacementNodeKlass.name, + replacementNodeKlass.getType(), + node.constructor.name, + nodeType, + ); + } else { + invariant( + replacementNode instanceof node.constructor && + replacementNodeKlass !== node.constructor, + '$applyNodeReplacement failed. Ensure replacement node %s with type %s is a subclass of the original node %s with type %s.', + replacementNodeKlass.name, + replacementNodeKlass.getType(), + node.constructor.name, + nodeType, ); } - return replacementNode; + invariant( + replacementNode.__key !== node.__key, + '$applyNodeReplacement failed. Ensure that the key argument is *not* used in your replace function (from node %s with type %s to node %s with type %s), Node keys must never be re-used except by the static clone method.', + node.constructor.name, + nodeType, + replacementNodeKlass.name, + replacementNodeKlass.getType(), + ); + return replacementNode as N; } - return node as N; + return node; } export function errorOnInsertTextNodeOnRoot( diff --git a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts index 0026cf5d6ad..2a497860b07 100644 --- a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts @@ -7,24 +7,29 @@ */ import { + $applyNodeReplacement, + $createParagraphNode, + $createTextNode, $getNodeByKey, $getRoot, $isTokenOrSegmented, $nodesOfType, + createEditor, + isSelectionWithinEditor, + ParagraphNode, + resetRandomKey, + SerializedTextNode, + TextNode, +} from 'lexical'; + +import { emptyFunction, generateRandomKey, getCachedTypeToNodeMap, getTextDirection, isArray, - isSelectionWithinEditor, - resetRandomKey, scheduleMicroTask, } from '../../LexicalUtils'; -import { - $createParagraphNode, - ParagraphNode, -} from '../../nodes/LexicalParagraphNode'; -import {$createTextNode, TextNode} from '../../nodes/LexicalTextNode'; import {initializeUnitTest} from '../utils'; describe('LexicalUtils tests', () => { @@ -291,3 +296,283 @@ describe('LexicalUtils tests', () => { }); }); }); +describe('$applyNodeReplacement', () => { + class ExtendedTextNode extends TextNode { + static getType() { + return 'extended-text'; + } + static clone(node: ExtendedTextNode): ExtendedTextNode { + return new ExtendedTextNode(node.__text, node.getKey()); + } + exportJSON(): SerializedTextNode { + return {...super.exportJSON(), type: this.getType()}; + } + initWithTextNode(node: TextNode): this { + this.__text = node.__text; + TextNode.prototype.afterCloneFrom.call(this, node); + return this; + } + initWithJSON(serializedNode: SerializedTextNode): this { + this.setTextContent(serializedNode.text); + this.setFormat(serializedNode.format); + this.setDetail(serializedNode.detail); + this.setMode(serializedNode.mode); + this.setStyle(serializedNode.style); + return this; + } + static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode { + return $createExtendedTextNode().initWithJSON(serializedNode); + } + } + class ExtendedExtendedTextNode extends ExtendedTextNode { + static getType() { + return 'extended-extended-text'; + } + static clone(node: ExtendedExtendedTextNode): ExtendedExtendedTextNode { + return new ExtendedExtendedTextNode(node.__text, node.getKey()); + } + initWithExtendedTextNode(node: ExtendedTextNode): this { + return this.initWithTextNode(node); + } + static importJSON( + serializedNode: SerializedTextNode, + ): ExtendedExtendedTextNode { + return $createExtendedExtendedTextNode().initWithJSON(serializedNode); + } + exportJSON(): SerializedTextNode { + return {...super.exportJSON(), type: this.getType()}; + } + } + function $createExtendedTextNode(text: string = '') { + return $applyNodeReplacement(new ExtendedTextNode(text)); + } + function $createExtendedExtendedTextNode(text: string = '') { + return $applyNodeReplacement(new ExtendedExtendedTextNode(text)); + } + test('validates replace node configuration', () => { + const editor = createEditor({ + nodes: [ + { + replace: TextNode, + with: (node) => $createExtendedTextNode().initWithTextNode(node), + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + 'Attempted to create node ExtendedTextNode that was not configured to be used on the editor', + ); + }); + test('validates replace node type withKlass', () => { + const editor = createEditor({ + nodes: [ + { + replace: TextNode, + with: (node) => node, + withKlass: ExtendedTextNode, + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + '$applyNodeReplacement failed. Expected replacement node to be an instance of ExtendedTextNode with type extended-text but returned TextNode with type text from original node TextNode with type text', + ); + }); + test('validates replace node type change', () => { + const editor = createEditor({ + nodes: [ + { + replace: TextNode, + with: (node: TextNode) => new TextNode(node.__text), + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + '$applyNodeReplacement failed. Ensure replacement node TextNode with type text is a subclass of the original node TextNode with type text', + ); + }); + test('validates replace node key change', () => { + const editor = createEditor({ + nodes: [ + { + replace: TextNode, + with: (node: TextNode) => + new ExtendedTextNode(node.__text, node.getKey()), + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + 'Lexical node with constructor ExtendedTextNode attempted to re-use key from node in active editor state with constructor TextNode. Keys must not be re-used when the type is changed.', + ); + }); + test('validates replace node configuration withKlass', () => { + const editor = createEditor({ + nodes: [ + { + replace: TextNode, + with: (node) => $createExtendedTextNode().initWithTextNode(node), + withKlass: ExtendedTextNode, + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + 'Attempted to create node ExtendedTextNode that was not configured to be used on the editor', + ); + }); + test('validates nested replace node configuration', () => { + const editor = createEditor({ + nodes: [ + ExtendedTextNode, + { + replace: ExtendedTextNode, + with: (node) => + $createExtendedExtendedTextNode().initWithExtendedTextNode(node), + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append( + $createParagraphNode().append($createExtendedTextNode('text')), + ); + }, + {discrete: true}, + ); + }).toThrow( + 'Attempted to create node ExtendedExtendedTextNode that was not configured to be used on the editor', + ); + }); + test('validates nested replace node configuration withKlass', () => { + const editor = createEditor({ + nodes: [ + ExtendedTextNode, + { + replace: TextNode, + with: (node) => $createExtendedTextNode().initWithTextNode(node), + withKlass: ExtendedTextNode, + }, + { + replace: ExtendedTextNode, + with: (node) => + $createExtendedExtendedTextNode().initWithExtendedTextNode(node), + withKlass: ExtendedExtendedTextNode, + }, + ], + onError(err) { + throw err; + }, + }); + expect(() => { + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + }).toThrow( + 'Attempted to create node ExtendedExtendedTextNode that was not configured to be used on the editor', + ); + }); + test('nested replace node configuration works', () => { + const editor = createEditor({ + nodes: [ + ExtendedTextNode, + ExtendedExtendedTextNode, + { + replace: TextNode, + with: (node) => $createExtendedTextNode().initWithTextNode(node), + withKlass: ExtendedTextNode, + }, + { + replace: ExtendedTextNode, + with: (node) => + $createExtendedExtendedTextNode().initWithExtendedTextNode(node), + withKlass: ExtendedExtendedTextNode, + }, + ], + onError(err) { + throw err; + }, + }); + editor.update( + () => { + $getRoot() + .clear() + .append($createParagraphNode().append($createTextNode('text'))); + }, + {discrete: true}, + ); + editor.read(() => { + const textNodes = $getRoot().getAllTextNodes(); + expect(textNodes).toHaveLength(1); + expect(textNodes[0].constructor).toBe(ExtendedExtendedTextNode); + expect(textNodes[0].getTextContent()).toBe('text'); + }); + }); +}); From 5c82b38e34762acd06b1d3459644a149b2683886 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 11 Sep 2024 12:03:43 +0800 Subject: [PATCH 008/133] [lexical-markdown] Breaking Change: rename 'multilineElement' to 'multiline-element' (#6617) --- packages/lexical-markdown/flow/LexicalMarkdown.js.flow | 2 +- packages/lexical-markdown/src/MarkdownShortcuts.ts | 2 +- packages/lexical-markdown/src/MarkdownTransformers.ts | 4 ++-- .../src/__tests__/unit/LexicalMarkdown.test.ts | 2 +- packages/lexical-markdown/src/utils.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow index 7f2f34483dc..4ceb12cffaa 100644 --- a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow +++ b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow @@ -58,7 +58,7 @@ export type MultilineElementTransformer = { linesInBetween: Array | null, isImport: boolean, ) => boolean | void; - type: 'multilineElement'; + type: 'multiline-element'; }; export type TextFormatTransformer = $ReadOnly<{ diff --git a/packages/lexical-markdown/src/MarkdownShortcuts.ts b/packages/lexical-markdown/src/MarkdownShortcuts.ts index 353fbdd954f..c05295acb07 100644 --- a/packages/lexical-markdown/src/MarkdownShortcuts.ts +++ b/packages/lexical-markdown/src/MarkdownShortcuts.ts @@ -403,7 +403,7 @@ export function registerMarkdownShortcuts( if ( type === 'element' || type === 'text-match' || - type === 'multilineElement' + type === 'multiline-element' ) { const dependencies = transformer.dependencies; for (const node of dependencies) { diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index efff5770e18..1fc738a3f79 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -127,7 +127,7 @@ export type MultilineElementTransformer = { */ isImport: boolean, ) => boolean | void; - type: 'multilineElement'; + type: 'multiline-element'; }; export type TextFormatTransformer = Readonly<{ @@ -399,7 +399,7 @@ export const CODE: MultilineElementTransformer = { })(rootNode, children, startMatch, isImport); } }, - type: 'multilineElement', + type: 'multiline-element', }; export const UNORDERED_LIST: ElementTransformer = { diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index 8d1a3a7bdb2..5c1f8fef11d 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -55,7 +55,7 @@ const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { } return false; // Run next transformer }, - type: 'multilineElement', + type: 'multiline-element', }; describe('Markdown', () => { diff --git a/packages/lexical-markdown/src/utils.ts b/packages/lexical-markdown/src/utils.ts index 812d61e0269..f2cf71c04e1 100644 --- a/packages/lexical-markdown/src/utils.ts +++ b/packages/lexical-markdown/src/utils.ts @@ -432,7 +432,7 @@ export function transformersByType(transformers: Array): Readonly<{ return { element: (byType.element || []) as Array, - multilineElement: (byType.multilineElement || + multilineElement: (byType['multiline-element'] || []) as Array, textFormat: (byType['text-format'] || []) as Array, textMatch: (byType['text-match'] || []) as Array, From 69bd14b85849b3814fc64000d9f863158e8e18e3 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 10 Sep 2024 23:05:39 -0500 Subject: [PATCH 009/133] Bug Fix: Fix issue where selecting a cell then dragging outside of table would not select entire table (#6579) Co-authored-by: Bob Ippolito --- .../__tests__/e2e/Selection.spec.mjs | 120 +++++++++++++++--- .../__tests__/e2e/Tables.spec.mjs | 5 +- .../__tests__/utils/index.mjs | 33 +++++ .../src/LexicalTableSelectionHelpers.ts | 19 +++ 4 files changed, 158 insertions(+), 19 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index c1999142254..5e30b4f413d 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -26,6 +26,7 @@ import { assertSelection, assertTableSelectionCoordinates, click, + createHumanReadableSelection, evaluate, expect, focusEditor, @@ -671,28 +672,115 @@ test.describe.parallel('Selection', () => { const lastCellText = lastCell.locator('span'); const tripleClickDelay = 50; await lastCellText.click({clickCount: 3, delay: tripleClickDelay}); - const anchorPath = [1, 0, 1, 0]; - // Only the last cell should be selected, and not the entire docuemnt if (browserName === 'firefox') { - // Firefox selects the p > span > #text node - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [...anchorPath, 0, 0], - focusOffset: cellText.length, - focusPath: [...anchorPath, 0, 0], - }); + // Firefox correctly selects the last cell + full text content, unlike Chromium which selects the first cell + const expectedSelection = createHumanReadableSelection( + 'the full text of the last cell in the table', + { + anchorOffset: {desc: 'beginning of cell', value: 0}, + anchorPath: [ + {desc: 'index of table in root', value: 1}, + {desc: 'first table row', value: 0}, + {desc: 'second cell', value: 1}, + {desc: 'first paragraph', value: 0}, + {desc: 'first span', value: 0}, + {desc: 'beginning of text', value: 0}, + ], + focusOffset: {desc: 'full text length', value: cellText.length}, + focusPath: [ + {desc: 'index of table in root', value: 1}, + {desc: 'first table row', value: 0}, + {desc: 'second cell', value: 1}, + {desc: 'first paragraph', value: 0}, + {desc: 'first span', value: 0}, + {desc: 'beginning of text', value: 0}, + ], + }, + ); + await assertSelection(page, expectedSelection); } else { - // Other browsers select the p - await assertSelection(page, { - anchorOffset: 0, - anchorPath, - focusOffset: 1, - focusPath: anchorPath, - }); + // Only the first cell should be selected, and not the entire document + // Ideally the last cell should be selected since that was what was triple-clicked, + // but due to the complex selection logic involved, this was the best that could be done. + // The goal ultimately was to prevent dangerous whole document selection. + // Getting the last cell precisely selected can be done at a later point. + const expectedSelection = createHumanReadableSelection( + 'cursor at beginning of the first cell in table', + { + anchorOffset: {desc: 'beginning of cell', value: 0}, + anchorPath: [ + {desc: 'index of table in root', value: 1}, + {desc: 'first table row', value: 0}, + {desc: 'first cell', value: 0}, + {desc: 'beginning of text', value: 0}, + ], + focusOffset: {desc: 'beginning of text', value: 0}, + focusPath: [ + {desc: 'index of table in root', value: 1}, + {desc: 'first table row', value: 0}, + {desc: 'first cell', value: 0}, + {desc: 'beginning of text', value: 0}, + ], + }, + ); + + await assertSelection(page, expectedSelection); } }); + /** + * Dragging down from a table cell onto paragraph text below the table should select the entire table + * and select the paragraph text below the table. + */ + test('Selecting table cell then dragging to outside of table should select entire table', async ({ + page, + isPlainText, + isCollab, + browserName, + legacyEvents, + }) => { + test.skip(isPlainText || isCollab); + + await focusEditor(page); + await insertTable(page, 1, 2); + await moveToEditorEnd(page); + + const endParagraphText = 'Some text'; + await page.keyboard.type(endParagraphText); + + const lastCell = page.locator( + '.PlaygroundEditorTheme__tableCell:last-child', + ); + await lastCell.click(); + await page.keyboard.type('Foo'); + + // Move the mouse to the last cell + await lastCell.hover(); + await page.mouse.down(); + // Move the mouse to the end of the document + await page.mouse.move(500, 500); + + const expectedSelection = createHumanReadableSelection( + 'the full table from beginning to the end of the text in the last cell', + { + anchorOffset: {desc: 'beginning of cell', value: 0}, + anchorPath: [ + {desc: 'index of table in root', value: 1}, + {desc: 'first table row', value: 0}, + {desc: 'first cell', value: 0}, + ], + focusOffset: {desc: 'full text length', value: endParagraphText.length}, + focusPath: [ + {desc: 'index of last paragraph', value: 2}, + {desc: 'index of first span', value: 0}, + {desc: 'index of text block', value: 0}, + ], + }, + ); + await assertSelection(page, expectedSelection); + }); + test('Can persist the text format from the paragraph', async ({ page, isPlainText, diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 959d3395395..8bff71ea3a6 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1319,7 +1319,7 @@ test.describe.parallel('Tables', () => { await insertTable(page, 2, 2); - await click(page, '.PlaygroundEditorTheme__tableCell'); + await click(page, '.PlaygroundEditorTheme__tableCell:first-child'); await page.keyboard.type('Hello'); await page.keyboard.down('Shift'); @@ -1334,8 +1334,7 @@ test.describe.parallel('Tables', () => { await insertSampleImage(page); await page.keyboard.type(' <- it works!'); - // Wait for Decorator to mount. - await page.waitForTimeout(3000); + await waitForSelector(page, '.editor-image img'); await assertHTML( page, diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index f2755a48583..4d80daad634 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -961,3 +961,36 @@ export async function dragDraggableMenuTo( export async function pressInsertLinkButton(page) { await click(page, '.toolbar-item[aria-label="Insert link"]'); } + +/** + * Creates a selection object to assert against that is human readable and self-describing. + * + * Selections are composed of an anchorPath (the start) and a focusPath (the end). + * Once you traverse each path, you use the respective offsets to find the exact location of the cursor. + * So offsets are relative to their path. For example, if the anchorPath is [0, 1, 2] and the anchorOffset is 3, + * then the cursor is at the 4th character of the 3rd element of the 2nd element of the 1st element. + * + * @example + * const expectedSelection = createHumanReadableSelection('the full text of the last cell', { + * anchorOffset: {desc: 'beginning of cell', value: 0}, + * anchorPath: [ + * {desc: 'index of table in root', value: 1}, + * {desc: 'first table row', value: 0}, + * {desc: 'first cell', value: 0}, + * ], + * focusOffset: {desc: 'full text length', value: 9}, + * focusPath: [ + * {desc: 'index of last paragraph', value: 2}, + * {desc: 'index of first span', value: 0}, + * {desc: 'index of text block', value: 0}, + * ], + * }); + */ +export function createHumanReadableSelection(_overview, dto) { + return { + anchorOffset: dto.anchorOffset.value, + anchorPath: dto.anchorPath.map((p) => p.value), + focusOffset: dto.focusOffset.value, + focusPath: dto.focusPath.map((p) => p.value), + }; +} diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index bb03a8b2525..8034eb18ab7 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -781,6 +781,25 @@ export function applyTableHandlers( : lastCell.getChildrenSize(), 'element', ); + } else if (isAnchorInside) { + const [tableMap] = $computeTableMap( + tableNode, + anchorCellNode, + anchorCellNode, + ); + const firstCell = tableMap[0][0].cell; + const lastCell = tableMap[tableMap.length - 1].at(-1)!.cell; + /** + * If isBackward, set the anchor to be at the end of the table so that when the cursor moves outside of + * the table in the backward direction, the entire table will be selected from its end. + * Otherwise, if forward, set the anchor to be at the start of the table so that when the focus is dragged + * outside th end of the table, it will start from the beginning of the table. + */ + newSelection.anchor.set( + isBackward ? lastCell.getKey() : firstCell.getKey(), + isBackward ? lastCell.getChildrenSize() : 0, + 'element', + ); } $setSelection(newSelection); $addHighlightStyleToTable(editor, tableObserver); From db093f73aa38e1ace186e02e095fbb7e40ce76da Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 11 Sep 2024 15:52:58 +0800 Subject: [PATCH 010/133] address micromatch vulnerability (#6616) --- package-lock.json | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c7a10a01af..d8fb0f29353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25539,11 +25539,12 @@ ] }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -36568,6 +36569,7 @@ "version": "0.17.1", "license": "MIT", "dependencies": { + "@lexical/clipboard": "0.17.1", "@lexical/utils": "0.17.1", "lexical": "0.17.1" } @@ -36626,6 +36628,7 @@ "license": "MIT", "dependencies": { "@lexical/offset": "0.17.1", + "@lexical/selection": "0.17.1", "lexical": "0.17.1" }, "peerDependencies": { @@ -41183,6 +41186,7 @@ "@lexical/table": { "version": "file:packages/lexical-table", "requires": { + "@lexical/clipboard": "0.17.1", "@lexical/utils": "0.17.1", "lexical": "0.17.1" } @@ -41232,6 +41236,7 @@ "version": "file:packages/lexical-yjs", "requires": { "@lexical/offset": "0.17.1", + "@lexical/selection": "0.17.1", "lexical": "0.17.1" } }, @@ -54792,11 +54797,11 @@ "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, From 01e2ae4b9a16f29ee4e78fa52a7f703d1e66020b Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 11 Sep 2024 23:22:20 +0800 Subject: [PATCH 011/133] CI: tag flaky test (#6620) --- .../__tests__/e2e/Links.spec.mjs | 174 +++-- .../__tests__/e2e/Tables.spec.mjs | 731 +++++++++--------- 2 files changed, 460 insertions(+), 445 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index 0523dd0323c..695ee28d8e6 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -426,97 +426,101 @@ test.describe.parallel('Links', () => { ); }); - test(`Can create a link with some text after, insert paragraph, then backspace, it should merge correctly`, async ({ - page, - }) => { - await focusEditor(page); - await page.keyboard.type(' abc def '); - await moveLeft(page, 5); - await selectCharacters(page, 'left', 3); + test( + `Can create a link with some text after, insert paragraph, then backspace, it should merge correctly`, + { + tag: '@flaky', + }, + async ({page}) => { + await focusEditor(page); + await page.keyboard.type(' abc def '); + await moveLeft(page, 5); + await selectCharacters(page, 'left', 3); - // link - await click(page, '.link'); - await click(page, '.link-confirm'); + // link + await click(page, '.link'); + await click(page, '.link-confirm'); - await assertHTML( - page, - html` -

- - - abc - - def -

- `, - ); + await assertHTML( + page, + html` +

+ + + abc + + def +

+ `, + ); - await moveLeft(page, 1); - await moveRight(page, 2); - await page.keyboard.press('Enter'); + await moveLeft(page, 1); + await moveRight(page, 2); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

- - - ab - -

-

- - c - - def -

- `, - ); + await assertHTML( + page, + html` +

+ + + ab + +

+

+ + c + + def +

+ `, + ); - await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); - await assertHTML( - page, - html` -

- - - ab - - - c - - def -

- `, - ); - }); + await assertHTML( + page, + html` +

+ + + ab + + + c + + def +

+ `, + ); + }, + ); test(`Can create a link then replace it with plain text`, async ({page}) => { await focusEditor(page); diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 8bff71ea3a6..52a26449702 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1306,85 +1306,86 @@ test.describe.parallel('Tables', () => { ); }); - test('Grid selection: can select multiple cells and insert an image', async ({ - page, - isPlainText, - isCollab, - browserName, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + test( + 'Grid selection: can select multiple cells and insert an image', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab, browserName}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 2, 2); + await insertTable(page, 2, 2); - await click(page, '.PlaygroundEditorTheme__tableCell:first-child'); - await page.keyboard.type('Hello'); + await click(page, '.PlaygroundEditorTheme__tableCell:first-child'); + await page.keyboard.type('Hello'); - await page.keyboard.down('Shift'); - await page.keyboard.press('ArrowRight'); - // Firefox range selection spans across cells after two arrow key press - if (browserName === 'firefox') { + await page.keyboard.down('Shift'); await page.keyboard.press('ArrowRight'); - } - await page.keyboard.press('ArrowDown'); - await page.keyboard.up('Shift'); + // Firefox range selection spans across cells after two arrow key press + if (browserName === 'firefox') { + await page.keyboard.press('ArrowRight'); + } + await page.keyboard.press('ArrowDown'); + await page.keyboard.up('Shift'); - await insertSampleImage(page); - await page.keyboard.type(' <- it works!'); + await insertSampleImage(page); + await page.keyboard.type(' <- it works!'); - await waitForSelector(page, '.editor-image img'); + await waitForSelector(page, '.editor-image img'); - await assertHTML( - page, - html` -


- - - - - - - - - -
-

- Hello -

-
-


-
-


-
-

- -

- Yellow flower in tilt shift lens -
- - <- it works! -

-
-


- `, - ); - }); + await assertHTML( + page, + html` +


+ + + + + + + + + +
+

+ Hello +

+
+


+
+


+
+

+ +

+ Yellow flower in tilt shift lens +
+ + <- it works! +

+
+


+ `, + ); + }, + ); test('Grid selection: can backspace lines, backspacing empty cell does not destroy it #3278', async ({ page, @@ -2125,267 +2126,271 @@ test.describe.parallel('Tables', () => { ); }); - test('Select multiple merged cells (selection expands to a rectangle)', async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); - - await focusEditor(page); - - await insertTable(page, 3, 3); - - await click(page, '.PlaygroundEditorTheme__tableCell'); - await moveDown(page, 1); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); - await mergeTableCells(page); - - await moveRight(page, 1); - await selectCellsFromTableCords( - page, - {x: 1, y: 0}, - {x: 2, y: 0}, - true, - true, - ); - await mergeTableCells(page); + test( + 'Select multiple merged cells (selection expands to a rectangle)', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 0}, - true, - true, - ); + await focusEditor(page); - await assertHTML( - page, - html` -


- - - - - - - - - - - - - - -
-


-
-


-
-


-
-


-
-


-
-


-
-


-
-


- `, - html` -


- - - - - - - - - - - - - - -
-


-
-


-
-


-
-


-
-


-
-


-
-


-
-


- `, - ); - }); + await insertTable(page, 3, 3); - test('Merge multiple merged cells and then unmerge', async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await moveDown(page, 1); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); - await focusEditor(page); + await moveRight(page, 1); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); - await insertTable(page, 3, 3); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 0}, + true, + true, + ); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await moveDown(page, 1); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); - await mergeTableCells(page); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + html` +


+ + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }, + ); - await moveRight(page, 1); - await selectCellsFromTableCords( - page, - {x: 1, y: 0}, - {x: 2, y: 0}, - true, - true, - ); - await mergeTableCells(page); + test( + 'Merge multiple merged cells and then unmerge', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 0}, - true, - true, - ); - await mergeTableCells(page); + await focusEditor(page); - await assertHTML( - page, - html` -


- - - - -
- - - - - -
-


-
-


-
-


-
-


-
-


- `, - ); + await insertTable(page, 3, 3); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 0}, - true, - true, - ); - await unmergeTableCell(page); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await moveDown(page, 1); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); - await assertHTML( - page, - html` -


- - - - - - - - - - - - - - - - -
-


-
-


-
-


-
-


-
-


-
-


-
-


-
-


-
-


-
-


- `, - ); - }); + await moveRight(page, 1); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 0}, + true, + true, + ); + await mergeTableCells(page); + + await assertHTML( + page, + html` +


+ + + + +
+ + + + + +
+


+
+


+
+


+
+


+
+


+ `, + ); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 0}, + true, + true, + ); + await unmergeTableCell(page); + + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }, + ); test('Insert row above (with conflicting merged cell)', async ({ page, @@ -2759,50 +2764,56 @@ test.describe.parallel('Tables', () => { ); }); - test('Delete columns backward', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); - test.skip(isPlainText); - if (IS_COLLAB) { - // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) - page.setViewportSize({height: 1000, width: 3000}); - } + test( + 'Delete columns backward', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + if (IS_COLLAB) { + // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) + page.setViewportSize({height: 1000, width: 3000}); + } - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 2, 4); + await insertTable(page, 2, 4); - await selectCellsFromTableCords( - page, - {x: 3, y: 1}, - {x: 1, y: 1}, - false, - false, - ); + await selectCellsFromTableCords( + page, + {x: 3, y: 1}, + {x: 1, y: 1}, + false, + false, + ); - await deleteTableColumns(page); + await deleteTableColumns(page); - await assertHTML( - page, - html` -


- - - - - - - -
-


-
-


-
-


- `, - ); - }); + await assertHTML( + page, + html` +


+ + + + + + + +
+


+
+


+
+


+ `, + ); + }, + ); test('Delete columns forward at end of table', async ({ page, From f50f168e8f67af9e13072bb80b63b4543d2f8f00 Mon Sep 17 00:00:00 2001 From: smworld01 <32947380+smworld01@users.noreply.github.com> Date: Thu, 12 Sep 2024 03:28:55 +0900 Subject: [PATCH 012/133] [lexical-react] Refactor: Ensure disconnect is called after connection is established in useYjsCollaboration (#6619) Co-authored-by: Bob Ippolito --- .../lexical-react/src/shared/useYjsCollaboration.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/lexical-react/src/shared/useYjsCollaboration.tsx b/packages/lexical-react/src/shared/useYjsCollaboration.tsx index 7017e96fec3..371a6f4721f 100644 --- a/packages/lexical-react/src/shared/useYjsCollaboration.tsx +++ b/packages/lexical-react/src/shared/useYjsCollaboration.tsx @@ -57,9 +57,7 @@ export function useYjsCollaboration( ): JSX.Element { const isReloadingDoc = useRef(false); - const connect = useCallback(() => { - provider.connect(); - }, [provider]); + const connect = useCallback(() => provider.connect(), [provider]); const disconnect = useCallback(() => { try { @@ -152,11 +150,14 @@ export function useYjsCollaboration( } }, ); - connect(); + + const connectionPromise = connect(); return () => { if (isReloadingDoc.current === false) { - disconnect(); + Promise.resolve(connectionPromise).then(() => { + disconnect(); + }); } provider.off('sync', onSync); From b0c9809dfd75e176b49d66370d3b69dfb75854ee Mon Sep 17 00:00:00 2001 From: Sherry Date: Thu, 12 Sep 2024 18:51:41 +0800 Subject: [PATCH 013/133] Revert "[lexical-markdown] Fix: normalize markdown in $convertFromMarkdownString to comply with CommonMark spec (#6608)" (#6627) --- .../src/MarkdownTransformers.ts | 66 ++-------------- .../__tests__/unit/LexicalMarkdown.test.ts | 79 ++----------------- packages/lexical-markdown/src/index.ts | 4 +- .../__tests__/e2e/Markdown.spec.mjs | 11 ++- 4 files changed, 22 insertions(+), 138 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index 1fc738a3f79..6afeebd1fbb 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -153,14 +153,6 @@ export type TextMatchTransformer = Readonly<{ type: 'text-match'; }>; -const ORDERED_LIST_REGEX = /^(\s*)(\d{1,})\.\s/; -const UNORDERED_LIST_REGEX = /^(\s*)[-*+]\s/; -const CHECK_LIST_REGEX = /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i; -const HEADING_REGEX = /^(#{1,6})\s/; -const QUOTE_REGEX = /^>\s/; -const CODE_START_REGEX = /^[ \t]*```(\w+)?/; -const CODE_END_REGEX = /[ \t]*```$/; - const createBlockNode = ( createNode: (match: Array) => ElementNode, ): ElementTransformer['replace'] => { @@ -274,7 +266,7 @@ export const HEADING: ElementTransformer = { const level = Number(node.getTag().slice(1)); return '#'.repeat(level) + ' ' + exportChildren(node); }, - regExp: HEADING_REGEX, + regExp: /^(#{1,6})\s/, replace: createBlockNode((match) => { const tag = ('h' + match[1].length) as HeadingTagType; return $createHeadingNode(tag); @@ -296,7 +288,7 @@ export const QUOTE: ElementTransformer = { } return output.join('\n'); }, - regExp: QUOTE_REGEX, + regExp: /^>\s/, replace: (parentNode, children, _match, isImport) => { if (isImport) { const previousNode = parentNode.getPreviousSibling(); @@ -336,9 +328,9 @@ export const CODE: MultilineElementTransformer = { }, regExpEnd: { optional: true, - regExp: CODE_END_REGEX, + regExp: /[ \t]*```$/, }, - regExpStart: CODE_START_REGEX, + regExpStart: /^[ \t]*```(\w+)?/, replace: ( rootNode, children, @@ -407,7 +399,7 @@ export const UNORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: UNORDERED_LIST_REGEX, + regExp: /^(\s*)[-*+]\s/, replace: listReplace('bullet'), type: 'element', }; @@ -417,7 +409,7 @@ export const CHECK_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: CHECK_LIST_REGEX, + regExp: /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i, replace: listReplace('check'), type: 'element', }; @@ -427,7 +419,7 @@ export const ORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: ORDERED_LIST_REGEX, + regExp: /^(\s*)(\d{1,})\.\s/, replace: listReplace('number'), type: 'element', }; @@ -527,47 +519,3 @@ export const LINK: TextMatchTransformer = { trigger: ')', type: 'text-match', }; - -export function normalizeMarkdown(input: string): string { - const lines = input.split('\n'); - let inCodeBlock = false; - const sanitizedLines: string[] = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const lastLine = sanitizedLines[sanitizedLines.length - 1]; - - // Detect the start or end of a code block - if (CODE_START_REGEX.test(line) || CODE_END_REGEX.test(line)) { - inCodeBlock = !inCodeBlock; - sanitizedLines.push(line); - continue; - } - - // If we are inside a code block, keep the line unchanged - if (inCodeBlock) { - sanitizedLines.push(line); - continue; - } - - // In markdown the concept of "empty paragraphs" does not exist. - // Blocks must be separated by an empty line. Non-empty adjacent lines must be merged. - if ( - line === '' || - lastLine === '' || - !lastLine || - HEADING_REGEX.test(lastLine) || - HEADING_REGEX.test(line) || - QUOTE_REGEX.test(line) || - ORDERED_LIST_REGEX.test(line) || - UNORDERED_LIST_REGEX.test(line) || - CHECK_LIST_REGEX.test(line) - ) { - sanitizedLines.push(line); - } else { - sanitizedLines[sanitizedLines.length - 1] = lastLine + line; - } - } - - return sanitizedLines.join('\n'); -} diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index 5c1f8fef11d..beab8779f28 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -22,10 +22,7 @@ import { Transformer, TRANSFORMERS, } from '../..'; -import { - MultilineElementTransformer, - normalizeMarkdown, -} from '../../MarkdownTransformers'; +import {MultilineElementTransformer} from '../../MarkdownTransformers'; // Matches html within a mdx file const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { @@ -95,36 +92,19 @@ describe('Markdown', () => { html: '
Hello world
', md: '###### Hello world', }, - { - // Multiline paragraphs: https://spec.commonmark.org/dingus/?text=Hello%0Aworld%0A! - html: '

Helloworld!

', - md: ['Hello', 'world', '!'].join('\n'), - skipExport: true, - }, { // Multiline paragraphs - // TO-DO: It would be nice to support also hard line breaks (
) as \ or double spaces - // See https://spec.commonmark.org/0.31.2/#hard-line-breaks. - // Example: '

Hello\\\nworld\\\n!

', - html: '

Hello
world
!

', + html: '

Hello
world
!

', md: ['Hello', 'world', '!'].join('\n'), - skipImport: true, }, { html: '
Hello
world!
', md: '> Hello\n> world!', }, - // TO-DO:
should be preserved - // { - // html: '
  • Hello
  • world
    !
    !
', - // md: '- Hello\n- world
!
!', - // skipImport: true, - // }, { - // Multiline list items: https://spec.commonmark.org/dingus/?text=-%20Hello%0A-%20world%0A!%0A! - html: '
  • Hello
  • world!!
', + // Multiline list items + html: '
  • Hello
  • world
    !
    !
', md: '- Hello\n- world\n!\n!', - skipExport: true, }, { html: '
  • Hello
  • world
', @@ -294,8 +274,8 @@ describe('Markdown', () => { skipExport: true, }, { - // https://spec.commonmark.org/dingus/?text=%3E%20Hello%0Aworld%0A! - html: '
Helloworld!
', + // Import only: multiline quote will be prefixed with ">" on each line during export + html: '
Hello
world
!
', md: '> Hello\nworld\n!', skipExport: true, }, @@ -318,9 +298,8 @@ describe('Markdown', () => { }, { customTransformers: [MDX_HTML_TRANSFORMER], - html: '

Some HTML in mdx:

From HTML: Line 1Some Text
', + html: '

Some HTML in mdx:

From HTML: Line 1\nSome Text
', md: 'Some HTML in mdx:\n\nLine 1\nSome Text', - skipExport: true, }, ]; @@ -428,47 +407,3 @@ describe('Markdown', () => { }); } }); - -describe('normalizeMarkdown', () => { - it('should combine lines separated by a single \n unless they are in a codeblock', () => { - const markdown = ` -1 -2 - -3 - -\`\`\`md -1 -2 - -3 -\`\`\` - -\`\`\`js -1 -2 - -3 -\`\`\` -`; - expect(normalizeMarkdown(markdown)).toBe(` -12 - -3 - -\`\`\`md -1 -2 - -3 -\`\`\` - -\`\`\`js -1 -2 - -3 -\`\`\` -`); - }); -}); diff --git a/packages/lexical-markdown/src/index.ts b/packages/lexical-markdown/src/index.ts index 3fc2f21da17..dac5b260478 100644 --- a/packages/lexical-markdown/src/index.ts +++ b/packages/lexical-markdown/src/index.ts @@ -31,7 +31,6 @@ import { ITALIC_STAR, ITALIC_UNDERSCORE, LINK, - normalizeMarkdown, ORDERED_LIST, QUOTE, STRIKETHROUGH, @@ -83,12 +82,11 @@ function $convertFromMarkdownString( node?: ElementNode, shouldPreserveNewLines = false, ): void { - const sanitizedMarkdown = normalizeMarkdown(markdown); const importMarkdown = createMarkdownImport( transformers, shouldPreserveNewLines, ); - return importMarkdown(sanitizedMarkdown, node); + return importMarkdown(markdown, node); } /** diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index 505b038ce28..b434d62be55 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -1310,6 +1310,7 @@ const IMPORTED_MARKDOWN_HTML = html` bold italic strikethrough
text, +
@@ -1407,7 +1408,9 @@ const IMPORTED_MARKDOWN_HTML = html` dir="ltr"> Blockquotes text goes here
- And secondline after + And second +
+ line after
- - And can be nested and multiline as well - + And can be nested +
+ and multiline as well From c0f1e207283eb96473b06b3c8c1476b3a77807f9 Mon Sep 17 00:00:00 2001 From: Patrick Moody <166345262+patrick-atticus@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:13:51 +1000 Subject: [PATCH 014/133] [lexical-playground] Bug Fix: Fix table row/column index when resizing merged cells (#6630) --- .../lexical-playground/__tests__/e2e/Tables.spec.mjs | 12 ++++++------ .../src/plugins/TableCellResizer/index.tsx | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 52a26449702..985b1cf8c34 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1690,7 +1690,8 @@ test.describe.parallel('Tables', () => { + rowspan="2" + style="width: 217px">


{ + class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader">


- +


@@ -1766,7 +1766,7 @@ test.describe.parallel('Tables', () => { html`


- + - + diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 0eb04da82ae..0f68199f69f 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -173,7 +173,9 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); const tableRowIndex = - $getTableRowIndexFromTableCellNode(tableCellNode); + $getTableRowIndexFromTableCellNode(tableCellNode) + + tableCellNode.getRowSpan() - + 1; const tableRows = tableNode.getChildren(); @@ -242,7 +244,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { for (let row = 0; row < tableMap.length; row++) { for (let column = 0; column < tableMap[row].length; column++) { if (tableMap[row][column].cell === tableCellNode) { - return column; + return column + tableCellNode.getColSpan() - 1; } } } From 6e10210fd1e113ccfafdc999b1d896733c5c5bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Fri, 13 Sep 2024 05:45:18 -0300 Subject: [PATCH 015/133] [lexical-markdown] Fix: normalize markdown in $convertFromMarkdownString to comply with CommonMark spec (2nd try) (#6629) Co-authored-by: Bob Ippolito Co-authored-by: Sherry Co-authored-by: Sherry Wong --- .../src/MarkdownTransformers.ts | 78 ++++++++++-- .../__tests__/unit/LexicalMarkdown.test.ts | 115 ++++++++++++++++-- packages/lexical-markdown/src/index.ts | 6 +- .../__tests__/e2e/Markdown.spec.mjs | 11 +- 4 files changed, 188 insertions(+), 22 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index 6afeebd1fbb..37e3d4b8f6d 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -153,6 +153,18 @@ export type TextMatchTransformer = Readonly<{ type: 'text-match'; }>; +const ORDERED_LIST_REGEX = /^(\s*)(\d{1,})\.\s/; +const UNORDERED_LIST_REGEX = /^(\s*)[-*+]\s/; +const CHECK_LIST_REGEX = /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i; +const HEADING_REGEX = /^(#{1,6})\s/; +const QUOTE_REGEX = /^>\s/; +const CODE_START_REGEX = /^[ \t]*```(\w+)?/; +const CODE_END_REGEX = /[ \t]*```$/; +const CODE_SINGLE_LINE_REGEX = + /^[ \t]*```[^`]+(?:(?:`{1,2}|`{4,})[^`]+)*```(?:[^`]|$)/; +const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/; +const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/; + const createBlockNode = ( createNode: (match: Array) => ElementNode, ): ElementTransformer['replace'] => { @@ -266,7 +278,7 @@ export const HEADING: ElementTransformer = { const level = Number(node.getTag().slice(1)); return '#'.repeat(level) + ' ' + exportChildren(node); }, - regExp: /^(#{1,6})\s/, + regExp: HEADING_REGEX, replace: createBlockNode((match) => { const tag = ('h' + match[1].length) as HeadingTagType; return $createHeadingNode(tag); @@ -288,7 +300,7 @@ export const QUOTE: ElementTransformer = { } return output.join('\n'); }, - regExp: /^>\s/, + regExp: QUOTE_REGEX, replace: (parentNode, children, _match, isImport) => { if (isImport) { const previousNode = parentNode.getPreviousSibling(); @@ -328,9 +340,9 @@ export const CODE: MultilineElementTransformer = { }, regExpEnd: { optional: true, - regExp: /[ \t]*```$/, + regExp: CODE_END_REGEX, }, - regExpStart: /^[ \t]*```(\w+)?/, + regExpStart: CODE_START_REGEX, replace: ( rootNode, children, @@ -399,7 +411,7 @@ export const UNORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)[-*+]\s/, + regExp: UNORDERED_LIST_REGEX, replace: listReplace('bullet'), type: 'element', }; @@ -409,7 +421,7 @@ export const CHECK_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)(?:-\s)?\s?(\[(\s|x)?\])\s/i, + regExp: CHECK_LIST_REGEX, replace: listReplace('check'), type: 'element', }; @@ -419,7 +431,7 @@ export const ORDERED_LIST: ElementTransformer = { export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, - regExp: /^(\s*)(\d{1,})\.\s/, + regExp: ORDERED_LIST_REGEX, replace: listReplace('number'), type: 'element', }; @@ -519,3 +531,55 @@ export const LINK: TextMatchTransformer = { trigger: ')', type: 'text-match', }; + +export function normalizeMarkdown(input: string): string { + const lines = input.split('\n'); + let inCodeBlock = false; + const sanitizedLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lastLine = sanitizedLines[sanitizedLines.length - 1]; + + // Code blocks of ```single line``` don't toggle the inCodeBlock flag + if (CODE_SINGLE_LINE_REGEX.test(line)) { + sanitizedLines.push(line); + continue; + } + + // Detect the start or end of a code block + if (CODE_START_REGEX.test(line) || CODE_END_REGEX.test(line)) { + inCodeBlock = !inCodeBlock; + sanitizedLines.push(line); + continue; + } + + // If we are inside a code block, keep the line unchanged + if (inCodeBlock) { + sanitizedLines.push(line); + continue; + } + + // In markdown the concept of "empty paragraphs" does not exist. + // Blocks must be separated by an empty line. Non-empty adjacent lines must be merged. + if ( + line === '' || + lastLine === '' || + !lastLine || + HEADING_REGEX.test(lastLine) || + HEADING_REGEX.test(line) || + QUOTE_REGEX.test(line) || + ORDERED_LIST_REGEX.test(line) || + UNORDERED_LIST_REGEX.test(line) || + CHECK_LIST_REGEX.test(line) || + TABLE_ROW_REG_EXP.test(line) || + TABLE_ROW_DIVIDER_REG_EXP.test(line) + ) { + sanitizedLines.push(line); + } else { + sanitizedLines[sanitizedLines.length - 1] = lastLine + line; + } + } + + return sanitizedLines.join('\n'); +} diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index beab8779f28..a18318f0ae6 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -22,7 +22,10 @@ import { Transformer, TRANSFORMERS, } from '../..'; -import {MultilineElementTransformer} from '../../MarkdownTransformers'; +import { + MultilineElementTransformer, + normalizeMarkdown, +} from '../../MarkdownTransformers'; // Matches html within a mdx file const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { @@ -92,19 +95,36 @@ describe('Markdown', () => { html: '
Hello world
', md: '###### Hello world', }, + { + // Multiline paragraphs: https://spec.commonmark.org/dingus/?text=Hello%0Aworld%0A! + html: '

Helloworld!

', + md: ['Hello', 'world', '!'].join('\n'), + skipExport: true, + }, { // Multiline paragraphs - html: '

Hello
world
!

', + // TO-DO: It would be nice to support also hard line breaks (
) as \ or double spaces + // See https://spec.commonmark.org/0.31.2/#hard-line-breaks. + // Example: '

Hello\\\nworld\\\n!

', + html: '

Hello
world
!

', md: ['Hello', 'world', '!'].join('\n'), + skipImport: true, }, { html: '
Hello
world!
', md: '> Hello\n> world!', }, + // TO-DO:
should be preserved + // { + // html: '
  • Hello
  • world
    !
    !
', + // md: '- Hello\n- world
!
!', + // skipImport: true, + // }, { - // Multiline list items - html: '
  • Hello
  • world
    !
    !
', + // Multiline list items: https://spec.commonmark.org/dingus/?text=-%20Hello%0A-%20world%0A!%0A! + html: '
  • Hello
  • world!!
', md: '- Hello\n- world\n!\n!', + skipExport: true, }, { html: '
  • Hello
  • world
', @@ -182,6 +202,11 @@ describe('Markdown', () => { html: '

Hello world!

', md: '*Hello **world**!*', }, + { + html: '

hello
world

', + md: 'hello\nworld', + shouldPreserveNewLines: true, + }, { html: '

Hello




world!

', md: '# Hello\n\n\n\n**world**!', @@ -274,8 +299,8 @@ describe('Markdown', () => { skipExport: true, }, { - // Import only: multiline quote will be prefixed with ">" on each line during export - html: '
Hello
world
!
', + // https://spec.commonmark.org/dingus/?text=%3E%20Hello%0Aworld%0A! + html: '
Helloworld!
', md: '> Hello\nworld\n!', skipExport: true, }, @@ -298,8 +323,9 @@ describe('Markdown', () => { }, { customTransformers: [MDX_HTML_TRANSFORMER], - html: '

Some HTML in mdx:

From HTML: Line 1\nSome Text
', + html: '

Some HTML in mdx:

From HTML: Line 1Some Text
', md: 'Some HTML in mdx:\n\nLine 1\nSome Text', + skipExport: true, }, ]; @@ -407,3 +433,78 @@ describe('Markdown', () => { }); } }); + +describe('normalizeMarkdown', () => { + it('should combine lines separated by a single \n unless they are in a codeblock', () => { + const markdown = ` +A1 +A2 + +A3 + +\`\`\`md +B1 +B2 + +B3 +\`\`\` + +C1 +C2 + +C3 + +\`\`\`js +D1 +D2 + +D3 +\`\`\` + +\`\`\`single line code\`\`\` + +E1 +E2 + +E3 +`; + expect(normalizeMarkdown(markdown)).toBe(` +A1A2 + +A3 + +\`\`\`md +B1 +B2 + +B3 +\`\`\` + +C1C2 + +C3 + +\`\`\`js +D1 +D2 + +D3 +\`\`\` + +\`\`\`single line code\`\`\` + +E1E2 + +E3 +`); + }); + + it('tables', () => { + const markdown = ` +| a | b | +| --- | --- | +| c | d | +`; + expect(normalizeMarkdown(markdown)).toBe(markdown); + }); +}); diff --git a/packages/lexical-markdown/src/index.ts b/packages/lexical-markdown/src/index.ts index dac5b260478..ec32900fce5 100644 --- a/packages/lexical-markdown/src/index.ts +++ b/packages/lexical-markdown/src/index.ts @@ -31,6 +31,7 @@ import { ITALIC_STAR, ITALIC_UNDERSCORE, LINK, + normalizeMarkdown, ORDERED_LIST, QUOTE, STRIKETHROUGH, @@ -82,11 +83,14 @@ function $convertFromMarkdownString( node?: ElementNode, shouldPreserveNewLines = false, ): void { + const sanitizedMarkdown = shouldPreserveNewLines + ? markdown + : normalizeMarkdown(markdown); const importMarkdown = createMarkdownImport( transformers, shouldPreserveNewLines, ); - return importMarkdown(markdown, node); + return importMarkdown(sanitizedMarkdown, node); } /** diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index b434d62be55..505b038ce28 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -1310,7 +1310,6 @@ const IMPORTED_MARKDOWN_HTML = html` bold italic strikethrough text, -
@@ -1408,9 +1407,7 @@ const IMPORTED_MARKDOWN_HTML = html` dir="ltr"> Blockquotes text goes here
- And second -
- line after + And secondline after
- And can be nested -
- and multiline as well + + And can be nested and multiline as well + From c8c201486bc45912a087e18830c4f431621a7e5c Mon Sep 17 00:00:00 2001 From: Katsia <47710336+KatsiarynaDzibrova@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:48:56 +0100 Subject: [PATCH 016/133] [lexical-table] Bug: Table formatting and styling not persisting for empty cells (#6626) Co-authored-by: Bob Ippolito --- .../lexical-table/src/LexicalTableObserver.ts | 19 ++++++++++++------- packages/lexical/src/LexicalSelection.ts | 18 ++++++++++++++++-- .../lexical/src/nodes/LexicalParagraphNode.ts | 12 ++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index a1114107d43..03030509a52 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -21,12 +21,13 @@ import { $getRoot, $getSelection, $isElementNode, + $isParagraphNode, $setSelection, SELECTION_CHANGE_COMMAND, } from 'lexical'; import invariant from 'shared/invariant'; -import {$isTableCellNode} from './LexicalTableCellNode'; +import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; import {$isTableNode} from './LexicalTableNode'; import { $createTableSelection, @@ -356,12 +357,16 @@ export class TableObserver { const anchor = formatSelection.anchor; const focus = formatSelection.focus; - selection.getNodes().forEach((cellNode) => { - if ($isTableCellNode(cellNode) && cellNode.getTextContentSize() !== 0) { - anchor.set(cellNode.getKey(), 0, 'element'); - focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element'); - formatSelection.formatText(type); - } + const cellNodes = selection.getNodes().filter($isTableCellNode); + const paragraph = cellNodes[0].getFirstChild(); + const alignFormatWith = $isParagraphNode(paragraph) + ? paragraph.getFormatFlags(type, null) + : null; + + cellNodes.forEach((cellNode: TableCellNode) => { + anchor.set(cellNode.getKey(), 0, 'element'); + focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element'); + formatSelection.formatText(type, alignFormatWith); }); $setSelection(selection); diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index e1486865c81..3a4e9ac0456 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -21,6 +21,7 @@ import { $isDecoratorNode, $isElementNode, $isLineBreakNode, + $isParagraphNode, $isRootNode, $isTextNode, $setSelection, @@ -1173,8 +1174,12 @@ export class RangeSelection implements BaseSelection { * merging nodes as necessary. * * @param formatType the format type to apply to the nodes in the Selection. + * @param alignWithFormat a 32-bit integer representing formatting flags to align with. */ - formatText(formatType: TextFormatType): void { + formatText( + formatType: TextFormatType, + alignWithFormat: number | null = null, + ): void { if (this.isCollapsed()) { this.toggleFormat(formatType); // When changing format, we should stop composition @@ -1222,7 +1227,16 @@ export class RangeSelection implements BaseSelection { return; } - const firstNextFormat = firstNode.getFormatFlags(formatType, null); + const firstNextFormat = firstNode.getFormatFlags( + formatType, + alignWithFormat, + ); + selectedNodes.forEach((node) => { + if ($isParagraphNode(node)) { + const newFormat = node.getFormatFlags(formatType, firstNextFormat); + node.setTextFormat(newFormat); + } + }); const lastIndex = selectedTextNodesLength - 1; let lastNode = selectedTextNodes[lastIndex]; diff --git a/packages/lexical/src/nodes/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index deab3a2cc13..ebbf9814581 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -30,6 +30,7 @@ import { $applyNodeReplacement, getCachedClassNameArray, isHTMLElement, + toggleTextFormatType, } from '../LexicalUtils'; import {ElementNode} from './LexicalElementNode'; import {$isTextNode, TextFormatType} from './LexicalTextNode'; @@ -75,6 +76,17 @@ export class ParagraphNode extends ElementNode { return (this.getTextFormat() & formatFlag) !== 0; } + /** + * Returns the format flags applied to the node as a 32-bit integer. + * + * @returns a number representing the TextFormatTypes applied to the node. + */ + getFormatFlags(type: TextFormatType, alignWithFormat: null | number): number { + const self = this.getLatest(); + const format = self.__textFormat; + return toggleTextFormatType(format, type, alignWithFormat); + } + getTextStyle(): string { const self = this.getLatest(); return self.__textStyle; From 3131ff89e746b640dacbfc9314b9d718236f6a7d Mon Sep 17 00:00:00 2001 From: neysanfoo Date: Mon, 16 Sep 2024 09:00:25 +0800 Subject: [PATCH 017/133] [lexical-playground][ExcalidrawNode] Bug Fix: Preserve Excalidraw image dimensions after resizing (#6634) --- .../ExcalidrawNode/ExcalidrawComponent.tsx | 39 ++++++++++------ .../nodes/ExcalidrawNode/ExcalidrawImage.tsx | 29 +++++++++--- .../src/nodes/ExcalidrawNode/index.tsx | 44 +++++++++++++------ 3 files changed, 80 insertions(+), 32 deletions(-) diff --git a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx index 903fe0f1e71..fcf2186ee19 100644 --- a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +++ b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx @@ -15,8 +15,6 @@ import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; import {mergeRegister} from '@lexical/utils'; import { $getNodeByKey, - $getSelection, - $isNodeSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, @@ -41,7 +39,7 @@ export default function ExcalidrawComponent({ const [isModalOpen, setModalOpen] = useState( data === '[]' && editor.isEditable(), ); - const imageContainerRef = useRef(null); + const imageContainerRef = useRef(null); const buttonRef = useRef(null); const captionButtonRef = useRef(null); const [isSelected, setSelected, clearSelection] = @@ -50,23 +48,21 @@ export default function ExcalidrawComponent({ const $onDelete = useCallback( (event: KeyboardEvent) => { - const deleteSelection = $getSelection(); - if (isSelected && $isNodeSelection(deleteSelection)) { + if (isSelected) { event.preventDefault(); editor.update(() => { - deleteSelection.getNodes().forEach((node) => { - if ($isExcalidrawNode(node)) { - node.remove(); - } - }); + const node = $getNodeByKey(nodeKey); + if (node) { + node.remove(); + } }); } return false; }, - [editor, isSelected], + [editor, isSelected, nodeKey], ); - // Set editor to readOnly if excalidraw is open to prevent unwanted changes + // Set editor to readOnly if Excalidraw is open to prevent unwanted changes useEffect(() => { if (isModalOpen) { editor.setEditable(false); @@ -119,7 +115,7 @@ export default function ExcalidrawComponent({ setModalOpen(false); return editor.update(() => { const node = $getNodeByKey(nodeKey); - if ($isExcalidrawNode(node)) { + if (node) { node.remove(); } }); @@ -184,6 +180,21 @@ export default function ExcalidrawComponent({ appState = {}, } = useMemo(() => JSON.parse(data), [data]); + const [initialWidth, initialHeight] = useMemo(() => { + let nodeWidth: 'inherit' | number = 'inherit'; + let nodeHeight: 'inherit' | number = 'inherit'; + + editor.getEditorState().read(() => { + const node = $getNodeByKey(nodeKey); + if ($isExcalidrawNode(node)) { + nodeWidth = node.getWidth(); + nodeHeight = node.getHeight(); + } + }); + + return [nodeWidth, nodeHeight]; + }, [editor, nodeKey]); + return ( <> {isSelected && (
[]; /** - * The Excalidraw elements to be rendered as an image + * The Excalidraw files associated with the elements */ files: BinaryFiles; /** * The height of the image to be rendered */ - height?: number | null; + height?: Dimension; /** * The ref object to be used to render the image */ - imageContainerRef: {current: null | HTMLDivElement}; + imageContainerRef: React.MutableRefObject; /** * The type of image to be rendered */ @@ -53,7 +55,7 @@ type Props = { /** * The width of the image to be rendered */ - width?: number | null; + width?: Dimension; }; // exportToSvg has fonts from excalidraw.com @@ -85,6 +87,8 @@ export default function ExcalidrawImage({ imageContainerRef, appState, rootClassName = null, + width = 'inherit', + height = 'inherit', }: Props): JSX.Element { const [Svg, setSvg] = useState(null); @@ -106,10 +110,25 @@ export default function ExcalidrawImage({ setContent(); }, [elements, files, appState]); + const containerStyle: React.CSSProperties = {}; + if (width !== 'inherit') { + containerStyle.width = `${width}px`; + } + if (height !== 'inherit') { + containerStyle.height = `${height}px`; + } + return (
{ + if (node) { + if (imageContainerRef) { + imageContainerRef.current = node; + } + } + }} className={rootClassName ?? ''} + style={containerStyle} dangerouslySetInnerHTML={{__html: Svg?.outerHTML ?? ''}} /> ); diff --git a/packages/lexical-playground/src/nodes/ExcalidrawNode/index.tsx b/packages/lexical-playground/src/nodes/ExcalidrawNode/index.tsx index 693a7418cc1..8b4c5796024 100644 --- a/packages/lexical-playground/src/nodes/ExcalidrawNode/index.tsx +++ b/packages/lexical-playground/src/nodes/ExcalidrawNode/index.tsx @@ -29,8 +29,8 @@ const ExcalidrawComponent = React.lazy(() => import('./ExcalidrawComponent')); export type SerializedExcalidrawNode = Spread< { data: string; - width: Dimension; - height: Dimension; + width?: Dimension; + height?: Dimension; }, SerializedLexicalNode >; @@ -48,10 +48,7 @@ function $convertExcalidrawElement( !widthStr || widthStr === 'inherit' ? 'inherit' : parseInt(widthStr, 10); if (excalidrawData) { - const node = $createExcalidrawNode(); - node.__data = excalidrawData; - node.__height = height; - node.__width = width; + const node = $createExcalidrawNode(excalidrawData, width, height); return { node, }; @@ -80,18 +77,19 @@ export class ExcalidrawNode extends DecoratorNode { static importJSON(serializedNode: SerializedExcalidrawNode): ExcalidrawNode { return new ExcalidrawNode( serializedNode.data, - serializedNode.width, - serializedNode.height, + serializedNode.width ?? 'inherit', + serializedNode.height ?? 'inherit', ); } exportJSON(): SerializedExcalidrawNode { return { + ...super.exportJSON(), data: this.__data, - height: this.__height, + height: this.__height === 'inherit' ? undefined : this.__height, type: 'excalidraw', version: 1, - width: this.__width, + width: this.__width === 'inherit' ? undefined : this.__width, }; } @@ -113,6 +111,8 @@ export class ExcalidrawNode extends DecoratorNode { const theme = config.theme; const className = theme.image; + span.style.display = 'inline-block'; + span.style.width = this.__width === 'inherit' ? 'inherit' : `${this.__width}px`; span.style.height = @@ -124,7 +124,11 @@ export class ExcalidrawNode extends DecoratorNode { return span; } - updateDOM(): false { + updateDOM(prevNode: ExcalidrawNode, dom: HTMLElement): boolean { + dom.style.width = + this.__width === 'inherit' ? 'inherit' : `${this.__width}px`; + dom.style.height = + this.__height === 'inherit' ? 'inherit' : `${this.__height}px`; return false; } @@ -169,11 +173,19 @@ export class ExcalidrawNode extends DecoratorNode { self.__data = data; } + getWidth(): Dimension { + return this.getLatest().__width; + } + setWidth(width: Dimension): void { const self = this.getWritable(); self.__width = width; } + getHeight(): Dimension { + return this.getLatest().__height; + } + setHeight(height: Dimension): void { const self = this.getWritable(); self.__height = height; @@ -188,12 +200,16 @@ export class ExcalidrawNode extends DecoratorNode { } } -export function $createExcalidrawNode(): ExcalidrawNode { - return new ExcalidrawNode(); +export function $createExcalidrawNode( + data: string = '[]', + width: Dimension = 'inherit', + height: Dimension = 'inherit', +): ExcalidrawNode { + return new ExcalidrawNode(data, width, height); } export function $isExcalidrawNode( - node: LexicalNode | null, + node: LexicalNode | null | undefined, ): node is ExcalidrawNode { return node instanceof ExcalidrawNode; } From da405bba0511ba26191e56ec8d7c7770b36c59f0 Mon Sep 17 00:00:00 2001 From: Patrick Moody <166345262+patrick-atticus@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:56:55 +1000 Subject: [PATCH 018/133] [lexical-table][lexical-playground] Feature: Add column widths to TableNode (#6625) --- .../html/TablesHTMLCopyAndPaste.spec.mjs | 38 ++ .../__tests__/e2e/Indentation.spec.mjs | 15 + .../__tests__/e2e/Selection.spec.mjs | 18 +- .../__tests__/e2e/Tables.spec.mjs | 346 +++++++++++++++--- .../__tests__/e2e/Toolbar.spec.mjs | 7 + .../4661-insert-column-selection.spec.mjs | 13 + .../__tests__/utils/index.mjs | 4 +- .../src/plugins/TableCellResizer/index.tsx | 73 ++-- .../src/themes/PlaygroundEditorTheme.css | 3 +- .../lexical-table/src/LexicalTableNode.ts | 83 ++++- .../src/LexicalTableSelectionHelpers.ts | 2 +- .../lexical-table/src/LexicalTableUtils.ts | 14 + .../__tests__/unit/LexicalTableNode.test.tsx | 72 +++- 13 files changed, 558 insertions(+), 130 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs index 28d18df4ce5..efe46564db1 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs @@ -43,6 +43,11 @@ test.describe('HTML Tables CopyAndPaste', () => { page, html`
{



+ + + + +

{ page, html` + + + + +

{ page, html` + + + + +

{


+ + + + + +
{ html`


+ + + + + +
@@ -531,6 +558,10 @@ test.describe('HTML Tables CopyAndPaste', () => { 123

+ + + +

@@ -646,6 +677,13 @@ test.describe('HTML Tables CopyAndPaste', () => { 123

+ + + + + + +

diff --git a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs index a0b460c8225..3a856bd19f3 100644 --- a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs @@ -106,6 +106,9 @@ test.describe('Identation', () => {


+ + +
{

+ + +
{

+ + +
{

+ + +
{


+ + +
{ abc

+ + + +
@@ -681,7 +685,7 @@ test.describe.parallel('Selection', () => { anchorOffset: {desc: 'beginning of cell', value: 0}, anchorPath: [ {desc: 'index of table in root', value: 1}, - {desc: 'first table row', value: 0}, + {desc: 'first table row', value: 1}, {desc: 'second cell', value: 1}, {desc: 'first paragraph', value: 0}, {desc: 'first span', value: 0}, @@ -690,7 +694,7 @@ test.describe.parallel('Selection', () => { focusOffset: {desc: 'full text length', value: cellText.length}, focusPath: [ {desc: 'index of table in root', value: 1}, - {desc: 'first table row', value: 0}, + {desc: 'first table row', value: 1}, {desc: 'second cell', value: 1}, {desc: 'first paragraph', value: 0}, {desc: 'first span', value: 0}, @@ -711,14 +715,14 @@ test.describe.parallel('Selection', () => { anchorOffset: {desc: 'beginning of cell', value: 0}, anchorPath: [ {desc: 'index of table in root', value: 1}, - {desc: 'first table row', value: 0}, + {desc: 'first table row', value: 1}, {desc: 'first cell', value: 0}, {desc: 'beginning of text', value: 0}, ], focusOffset: {desc: 'beginning of text', value: 0}, focusPath: [ {desc: 'index of table in root', value: 1}, - {desc: 'first table row', value: 0}, + {desc: 'first table row', value: 1}, {desc: 'first cell', value: 0}, {desc: 'beginning of text', value: 0}, ], @@ -767,7 +771,7 @@ test.describe.parallel('Selection', () => { anchorOffset: {desc: 'beginning of cell', value: 0}, anchorPath: [ {desc: 'index of table in root', value: 1}, - {desc: 'first table row', value: 0}, + {desc: 'first table row', value: 1}, {desc: 'first cell', value: 0}, ], focusOffset: {desc: 'full text length', value: endParagraphText.length}, @@ -1025,7 +1029,7 @@ test.describe.parallel('Selection', () => { anchorOffset: 0, anchorPath: [0], focusOffset: 1, - focusPath: [1, 1, 1], + focusPath: [1, 2, 1], }); }, ); @@ -1048,7 +1052,7 @@ test.describe.parallel('Selection', () => { anchorOffset: 0, anchorPath: [1], focusOffset: 1, - focusPath: [0, 0, 0], + focusPath: [0, 1, 0], }); }, ); diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 985b1cf8c34..75bda0f018b 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -90,6 +90,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +


@@ -132,6 +136,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +

abc

@@ -171,9 +179,9 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); await moveLeft(page, 1); @@ -188,17 +196,17 @@ test.describe.parallel('Tables', () => { await page.keyboard.type('ab'); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 0, 0, 0, 0, 0], + anchorPath: [1, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 0, 0, 0, 0, 0], + focusPath: [1, 1, 0, 0, 0, 0], }); await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, 2, 1, 0], }); }); @@ -216,9 +224,9 @@ test.describe.parallel('Tables', () => { await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, 2, 1, 0], }); await moveRight(page, 1); @@ -233,9 +241,9 @@ test.describe.parallel('Tables', () => { await page.keyboard.type('ab'); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 1, 1, 0, 0, 0], + anchorPath: [1, 2, 1, 0, 0, 0], focusOffset: 2, - focusPath: [1, 1, 1, 0, 0, 0], + focusPath: [1, 2, 1, 0, 0, 0], }); await moveRight(page, 3); @@ -261,17 +269,17 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 1, 0, 0, 0], + anchorPath: [1, 1, 0, 1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 1, 0, 0, 0], + focusPath: [1, 1, 0, 1, 1, 0, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); }); @@ -290,17 +298,17 @@ test.describe.parallel('Tables', () => { await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 1, 1, 1, 0], + anchorPath: [1, 1, 0, 1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 0, 0, 1, 1, 1, 0], + focusPath: [1, 1, 0, 1, 2, 1, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 2], + anchorPath: [1, 1, 0, 2], focusOffset: 0, - focusPath: [1, 0, 0, 2], + focusPath: [1, 1, 0, 2], }); }); }); @@ -335,15 +343,19 @@ test.describe.parallel('Tables', () => { await deleteBackward(page); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, 2, 1, 0], }); await assertHTML( page, html`


+ + + +


@@ -389,6 +401,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +


@@ -446,6 +462,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +


@@ -492,9 +512,9 @@ test.describe.parallel('Tables', () => { await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, 2, 1, 0], }); }); @@ -515,6 +535,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +


@@ -540,57 +564,57 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 1, 0], + anchorPath: [1, 1, 1, 0], focusOffset: 0, - focusPath: [1, 0, 1, 0], + focusPath: [1, 1, 1, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, 2, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, 2, 0, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, 2, 1, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, 2, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, 2, 0, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 1, 0], + anchorPath: [1, 1, 1, 0], focusOffset: 0, - focusPath: [1, 0, 1, 0], + focusPath: [1, 1, 1, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); }); @@ -607,25 +631,25 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); await moveDown(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, 2, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, 2, 0, 0], }); await moveUp(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 0, 0, 0], + anchorPath: [1, 1, 0, 0], focusOffset: 0, - focusPath: [1, 0, 0, 0], + focusPath: [1, 1, 0, 0], }); }); @@ -643,9 +667,9 @@ test.describe.parallel('Tables', () => { await page.keyboard.type('@A'); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 0, 0, 0, 0, 0], + anchorPath: [1, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 0, 0, 0, 0, 0], + focusPath: [1, 1, 0, 0, 0, 0], }); await waitForSelector(page, `#typeahead-menu ul li:first-child.selected`); @@ -653,9 +677,9 @@ test.describe.parallel('Tables', () => { await moveDown(page, 1); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 0, 0, 0, 0, 0], + anchorPath: [1, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 0, 0, 0, 0, 0], + focusPath: [1, 1, 0, 0, 0, 0], }); await waitForSelector( @@ -691,6 +715,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -723,6 +752,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +

a

@@ -775,7 +809,7 @@ test.describe.parallel('Tables', () => { } const firstRowFirstColumnCellBoundingBox = await p.locator( - 'table:first-of-type > tr:nth-child(1) > th:nth-child(1)', + 'table:first-of-type > :nth-match(tr, 1) > th:nth-child(1)', ); // Focus on inside the iFrame or the boundingBox() below returns null. @@ -795,6 +829,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -838,6 +877,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +

a

@@ -909,6 +953,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -941,6 +990,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +

a

@@ -1006,6 +1060,10 @@ test.describe.parallel('Tables', () => { page, html` + + + +

a

@@ -1024,6 +1082,11 @@ test.describe.parallel('Tables', () => {
+ + + + +

a

@@ -1084,6 +1147,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +


@@ -1136,6 +1204,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1152,6 +1224,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +


@@ -1187,6 +1263,11 @@ test.describe.parallel('Tables', () => { html`

Hello World

+ + + + +
@@ -1221,6 +1302,11 @@ test.describe.parallel('Tables', () => { html`

Hello World

+ + + + +


@@ -1288,6 +1374,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +

123

@@ -1341,6 +1431,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1411,6 +1505,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1445,6 +1543,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1498,6 +1600,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1535,6 +1641,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -1608,6 +1718,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + + - @@ -1634,7 +1748,7 @@ test.describe.parallel('Tables', () => { - @@ -1686,12 +1800,16 @@ test.describe.parallel('Tables', () => { html`


{


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader">


+



+


+ + + + + -
+ rowspan="2">


{ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader">


+


@@ -1766,6 +1884,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
{ html`


+ + + + +
@@ -1873,6 +2001,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -1932,6 +2065,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -1983,6 +2121,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -2069,6 +2212,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -2174,6 +2322,11 @@ test.describe.parallel('Tables', () => {


+ + + + +
{ html`


+ + + + +
{ html`


+ + + + +
{ html`


+ + + + +
@@ -2426,6 +2594,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -2490,6 +2662,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
{ html`


+ + + +
@@ -2601,6 +2782,12 @@ test.describe.parallel('Tables', () => { html`


+ + + + + +
@@ -2681,6 +2868,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -2743,6 +2934,10 @@ test.describe.parallel('Tables', () => { html`


+ + + +
@@ -2796,6 +2991,9 @@ test.describe.parallel('Tables', () => { html`


+ + +
@@ -2846,6 +3044,9 @@ test.describe.parallel('Tables', () => { html`


+ + +
@@ -2912,6 +3113,9 @@ test.describe.parallel('Tables', () => { html`


+ + +
{ page, html` + + + + +

{ page, html` + + +

{ html`


+ + + + + +
@@ -3190,6 +3408,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +
@@ -3230,6 +3453,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + +

@@ -3297,6 +3525,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + + + - - + +
@@ -3374,6 +3607,11 @@ test.describe.parallel('Tables', () => { html`


+ + + + + diff --git a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs index 58a0b41af91..aee91bc356b 100644 --- a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs @@ -113,8 +113,7 @@ test.describe('Identation', () => { - - - - - - - - - - - - - - @@ -1508,30 +1494,24 @@ test.describe.parallel('Tables', () => { - - - - - - @@ -2608,27 +2588,23 @@ test.describe.parallel('Tables', () => { @@ -3690,13 +3666,15 @@ test.describe.parallel('Tables', () => { for rows that are in the output + const cols = Array.from( + tableElement.querySelectorAll(':scope > colgroup > col'), + ).filter((dom, i) => knownColumns.has(i)); + colGroup.replaceChildren(...cols); + } + // Wrap direct descendant rows in a tbody for export const rows = tableElement.querySelectorAll(':scope > tr'); if (rows.length > 0) { const tBody = document.createElement('tbody'); - tBody.append(...rows); + for (const row of rows) { + tBody.appendChild(row); + } tableElement.append(tBody); } return tableElement; diff --git a/packages/lexical-table/src/LexicalTableRowNode.ts b/packages/lexical-table/src/LexicalTableRowNode.ts index eddea69a27e..fd8bcb8fa0a 100644 --- a/packages/lexical-table/src/LexicalTableRowNode.ts +++ b/packages/lexical-table/src/LexicalTableRowNode.ts @@ -6,7 +6,7 @@ * */ -import type {Spread} from 'lexical'; +import type {BaseSelection, Spread} from 'lexical'; import {addClassNamesToElement} from '@lexical/utils'; import { @@ -81,6 +81,14 @@ export class TableRowNode extends ElementNode { return element; } + extractWithChild( + child: LexicalNode, + selection: BaseSelection | null, + destination: 'clone' | 'html', + ): boolean { + return destination === 'html'; + } + isShadowRoot(): boolean { return true; } diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index df23bdcf843..96ca3c7e426 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -7,13 +7,16 @@ */ import {$insertDataTransferForRichText} from '@lexical/clipboard'; +import {$generateHtmlFromNodes} from '@lexical/html'; import {TablePlugin} from '@lexical/react/LexicalTablePlugin'; import { $createTableNode, $createTableNodeWithDimensions, $createTableSelection, $insertTableColumn__EXPERIMENTAL, + $isTableCellNode, } from '@lexical/table'; +import {$dfs} from '@lexical/utils'; import { $createParagraphNode, $createTextNode, @@ -136,6 +139,133 @@ describe('LexicalTableNode tests', () => { }); }); + test('TableNode.exportDOM() with range selection', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const tableNode = $createTableNodeWithDimensions( + 2, + 2, + ).setColWidths([100, 200]); + tableNode + .getAllTextNodes() + .forEach((node, i) => node.setTextContent(String(i))); + $getRoot().clear().append(tableNode); + expectHtmlToBeEqual( + $generateHtmlFromNodes(editor, $getRoot().select(0)), + html` +
diff --git a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs index eb039765480..12cec473d34 100644 --- a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs @@ -157,6 +157,13 @@ test.describe('Toolbar', () => {

+ + + + + + + '; + expectTableHtmlToBeEqual( + testEnv.innerHTML, + html` +


diff --git a/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs b/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs index 8a1988cc2d1..abcd0492817 100644 --- a/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs @@ -48,6 +48,12 @@ test.describe('Regression test #4661', () => { html`


+ + + + + + '; - expect(testEnv.innerHTML).toBe( - `
@@ -86,6 +92,7 @@ test.describe('Regression test #4661', () => { `, ); }); + test('inserting 2 columns after inserts after selection', async ({ page, isPlainText, @@ -113,6 +120,12 @@ test.describe('Regression test #4661', () => { html`


+ + + + + + '; expect(testEnv.innerHTML).toBe( - `
diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 4d80daad634..907db4b110a 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -826,12 +826,12 @@ export async function selectCellsFromTableCords( } const firstRowFirstColumnCell = await leftFrame.locator( - `table:first-of-type > tr:nth-child(${firstCords.y + 1}) > ${ + `table:first-of-type > :nth-match(tr, ${firstCords.y + 1}) > ${ isFirstHeader ? 'th' : 'td' }:nth-child(${firstCords.x + 1})`, ); const secondRowSecondCell = await leftFrame.locator( - `table:first-of-type > tr:nth-child(${secondCords.y + 1}) > ${ + `table:first-of-type > :nth-match(tr, ${secondCords.y + 1}) > ${ isSecondHeader ? 'th' : 'td' }:nth-child(${secondCords.x + 1})`, ); diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 0f68199f69f..f0446ee6b1f 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -5,12 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ -import type { - TableCellNode, - TableDOMCell, - TableMapType, - TableMapValueType, -} from '@lexical/table'; +import type {TableCellNode, TableDOMCell, TableMapType} from '@lexical/table'; import type {LexicalEditor} from 'lexical'; import './index.css'; @@ -24,6 +19,7 @@ import { $isTableCellNode, $isTableRowNode, getDOMCellFromTarget, + TableNode, } from '@lexical/table'; import {calculateZoomLevel} from '@lexical/utils'; import {$getNearestNodeFromDOMNode} from 'lexical'; @@ -47,7 +43,7 @@ type MousePosition = { type MouseDraggingDirection = 'right' | 'bottom'; const MIN_ROW_HEIGHT = 33; -const MIN_COLUMN_WIDTH = 50; +const MIN_COLUMN_WIDTH = 92; function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { const targetRef = useRef(null); @@ -75,6 +71,20 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { return (event.buttons & 1) === 1; }; + useEffect(() => { + return editor.registerNodeTransform(TableNode, (tableNode) => { + if (tableNode.getColWidths()) { + return tableNode; + } + + const numColumns = tableNode.getColumnCount(); + const columnWidth = MIN_COLUMN_WIDTH; + + tableNode.setColWidths(Array(numColumns).fill(columnWidth)); + return tableNode; + }); + }, [editor]); + useEffect(() => { const onMouseMove = (event: MouseEvent) => { setTimeout(() => { @@ -208,27 +218,6 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { [activeCell, editor], ); - const getCellNodeWidth = ( - cell: TableCellNode, - activeEditor: LexicalEditor, - ): number | undefined => { - const width = cell.getWidth(); - if (width !== undefined) { - return width; - } - - const domCellNode = activeEditor.getElementByKey(cell.getKey()); - if (domCellNode == null) { - return undefined; - } - const computedStyle = getComputedStyle(domCellNode); - return ( - domCellNode.clientWidth - - parseFloat(computedStyle.paddingLeft) - - parseFloat(computedStyle.paddingRight) - ); - }; - const getCellNodeHeight = ( cell: TableCellNode, activeEditor: LexicalEditor, @@ -244,7 +233,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { for (let row = 0; row < tableMap.length; row++) { for (let column = 0; column < tableMap[row].length; column++) { if (tableMap[row][column].cell === tableCellNode) { - return column + tableCellNode.getColSpan() - 1; + return column; } } } @@ -273,22 +262,18 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { throw new Error('TableCellResizer: Table column not found.'); } - for (let row = 0; row < tableMap.length; row++) { - const cell: TableMapValueType = tableMap[row][columnIndex]; - if ( - cell.startRow === row && - (columnIndex === tableMap[row].length - 1 || - tableMap[row][columnIndex].cell !== - tableMap[row][columnIndex + 1].cell) - ) { - const width = getCellNodeWidth(cell.cell, editor); - if (width === undefined) { - continue; - } - const newWidth = Math.max(width + widthChange, MIN_COLUMN_WIDTH); - cell.cell.setWidth(newWidth); - } + const colWidths = tableNode.getColWidths(); + if (!colWidths) { + return; + } + const width = colWidths[columnIndex]; + if (width === undefined) { + return; } + const newColWidths = [...colWidths]; + const newWidth = Math.max(width + widthChange, MIN_COLUMN_WIDTH); + newColWidths[columnIndex] = newWidth; + tableNode.setColWidths(newColWidths); }, {tag: 'skip-scroll-into-view'}, ); diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index f79fb7d3a86..22d27e4145e 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -122,7 +122,7 @@ overflow-y: scroll; overflow-x: scroll; table-layout: fixed; - width: max-content; + width: fit-content; margin: 0px 25px 30px 0px; } .PlaygroundEditorTheme__tableRowStriping tr:nth-child(even) { @@ -137,7 +137,6 @@ .PlaygroundEditorTheme__tableCell { border: 1px solid #bbb; width: 75px; - min-width: 75px; vertical-align: top; text-align: start; padding: 6px 8px; diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 938d8c61c63..90031636d26 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -31,16 +31,39 @@ import { import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; -import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode'; +import {TableRowNode} from './LexicalTableRowNode'; import {getTable} from './LexicalTableSelectionHelpers'; export type SerializedTableNode = Spread< { + colWidths?: readonly number[]; rowStriping?: boolean; }, SerializedElementNode >; +function updateColgroup( + dom: HTMLElement, + config: EditorConfig, + colCount: number, + colWidths?: number[] | readonly number[], +) { + const colGroup = dom.querySelector('colgroup'); + if (!colGroup) { + return; + } + const cols = []; + for (let i = 0; i < colCount; i++) { + const col = document.createElement('col'); + const width = colWidths && colWidths[i]; + if (width) { + col.style.width = `${width}px`; + } + cols.push(col); + } + colGroup.replaceChildren(...cols); +} + function setRowStriping( dom: HTMLElement, config: EditorConfig, @@ -59,17 +82,31 @@ function setRowStriping( export class TableNode extends ElementNode { /** @internal */ __rowStriping: boolean; + __colWidths?: number[] | readonly number[]; static getType(): string { return 'table'; } + getColWidths(): number[] | readonly number[] | undefined { + const self = this.getLatest(); + return self.__colWidths; + } + + setColWidths(colWidths: readonly number[]): this { + const self = this.getWritable(); + // NOTE: Node properties should be immutable. Freeze to prevent accidental mutation. + self.__colWidths = __DEV__ ? Object.freeze(colWidths) : colWidths; + return self; + } + static clone(node: TableNode): TableNode { return new TableNode(node.__key); } afterCloneFrom(prevNode: this) { super.afterCloneFrom(prevNode); + this.__colWidths = prevNode.__colWidths; this.__rowStriping = prevNode.__rowStriping; } @@ -85,6 +122,7 @@ export class TableNode extends ElementNode { static importJSON(serializedNode: SerializedTableNode): TableNode { const tableNode = $createTableNode(); tableNode.__rowStriping = serializedNode.rowStriping || false; + tableNode.__colWidths = serializedNode.colWidths; return tableNode; } @@ -96,6 +134,7 @@ export class TableNode extends ElementNode { exportJSON(): SerializedTableNode { return { ...super.exportJSON(), + colWidths: this.getColWidths(), rowStriping: this.__rowStriping ? this.__rowStriping : undefined, type: 'table', version: 1, @@ -104,6 +143,14 @@ export class TableNode extends ElementNode { createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement { const tableElement = document.createElement('table'); + const colGroup = document.createElement('colgroup'); + tableElement.appendChild(colGroup); + updateColgroup( + tableElement, + config, + this.getColumnCount(), + this.getColWidths(), + ); addClassNamesToElement(tableElement, config.theme.table); if (this.__rowStriping) { @@ -121,6 +168,7 @@ export class TableNode extends ElementNode { if (prevNode.__rowStriping !== this.__rowStriping) { setRowStriping(dom, config, this.__rowStriping); } + updateColgroup(dom, config, this.getColumnCount(), this.getColWidths()); return false; } @@ -133,19 +181,10 @@ export class TableNode extends ElementNode { const colGroup = document.createElement('colgroup'); const tBody = document.createElement('tbody'); if (isHTMLElement(tableElement)) { - tBody.append(...tableElement.children); - } - const firstRow = this.getFirstChildOrThrow(); - - if (!$isTableRowNode(firstRow)) { - throw new Error('Expected to find row node.'); - } - - const colCount = firstRow.getChildrenSize(); - - for (let i = 0; i < colCount; i++) { - const col = document.createElement('col'); - colGroup.append(col); + const cols = tableElement.querySelectorAll('col'); + colGroup.append(...cols); + const rows = tableElement.querySelectorAll('tr'); + tBody.append(...rows); } newElement.replaceChildren(colGroup, tBody); @@ -281,6 +320,22 @@ export class TableNode extends ElementNode { canIndent(): false { return false; } + + getColumnCount(): number { + const firstRow = this.getFirstChild(); + if (!firstRow) { + return 0; + } + + let columnCount = 0; + firstRow.getChildren().forEach((cell) => { + if ($isTableCellNode(cell)) { + columnCount += cell.getColSpan(); + } + }); + + return columnCount; + } } export function $getElementForTableNode( diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 8034eb18ab7..4353604567e 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -1014,7 +1014,7 @@ export function getTable(tableElement: HTMLElement): TableDOMTable { domRows, rows: 0, }; - let currentNode = tableElement.firstChild; + let currentNode = tableElement.querySelector('tr') as ChildNode | null; let x = 0; let y = 0; domRows.length = 0; diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index d3e7d5ab888..c674c4a8b88 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -471,6 +471,14 @@ export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void { if (firstInsertedCell !== null) { $moveSelectionToCell(firstInsertedCell); } + const colWidths = grid.getColWidths(); + if (colWidths) { + const newColWidths = [...colWidths]; + const columnIndex = insertAfterColumn < 0 ? 0 : insertAfterColumn; + const newWidth = newColWidths[columnIndex]; + newColWidths.splice(columnIndex, 0, newWidth); + grid.setColWidths(newColWidths); + } } export function $deleteTableColumn( @@ -643,6 +651,12 @@ export function $deleteTableColumn__EXPERIMENTAL(): void { const {cell} = previousRow; $moveSelectionToCell(cell); } + const colWidths = grid.getColWidths(); + if (colWidths) { + const newColWidths = [...colWidths]; + newColWidths.splice(startColumn, selectedColumnCount); + grid.setColWidths(newColWidths); + } } function $moveSelectionToCell(cell: TableCellNode): void { diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 161def23622..fb1d9af2332 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -12,6 +12,7 @@ import { $createTableNode, $createTableNodeWithDimensions, $createTableSelection, + $insertTableColumn__EXPERIMENTAL, } from '@lexical/table'; import { $createParagraphNode, @@ -102,7 +103,7 @@ describe('LexicalTableNode tests', () => { const tableNode = $createTableNode(); expect(tableNode.createDOM(editorConfig).outerHTML).toBe( - `
`, + `
`, ); }); }); @@ -126,7 +127,7 @@ describe('LexicalTableNode tests', () => { // Make sure paragraph is inserted inside empty cells const emptyCell = '


${emptyCell}

Hello there

General Kenobi!

Lexical is nice

`, + `${emptyCell}

Hello there

General Kenobi!

Lexical is nice

`, ); }); @@ -147,7 +148,7 @@ describe('LexicalTableNode tests', () => { $insertDataTransferForRichText(dataTransfer, selection, editor); }); expect(testEnv.innerHTML).toBe( - `

Surface

MWP_WORK_LS_COMPOSER

77349

Lexical

XDS_RICH_TEXT_AREA

sdvd sdfvsfs

`, + `

Surface

MWP_WORK_LS_COMPOSER

77349

Lexical

XDS_RICH_TEXT_AREA

sdvd sdfvsfs

`, ); }); @@ -292,7 +293,7 @@ describe('LexicalTableNode tests', () => { }); expect(testEnv.innerHTML).toBe( - `


















`, + `


















`, ); }); @@ -366,7 +367,7 @@ describe('LexicalTableNode tests', () => { const root = $getRoot(); const table = root.getLastChild(); expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, + `
`, ); }); @@ -382,10 +383,69 @@ describe('LexicalTableNode tests', () => { const root = $getRoot(); const table = root.getLastChild(); expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, + `
`, ); }); }); + + test('Update column widths', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 2, true); + root.append(table); + }); + + // Set widths + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + table!.setColWidths([50, 50]); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expect(table!.createDOM(editorConfig).outerHTML).toBe( + `
`, + ); + const colWidths = table!.getColWidths(); + + // colwidths should be immutable in DEV + expect(() => { + (colWidths as number[]).push(100); + }).toThrow(); + expect(table!.getColWidths()).toStrictEqual([50, 50]); + expect(table!.getColumnCount()).toBe(2); + }); + + // Add a column + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + const DOMTable = $getElementForTableNode(editor, table!); + const selection = $createTableSelection(); + selection.set( + table!.__key, + table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + ); + $setSelection(selection); + $insertTableColumn__EXPERIMENTAL(); + table!.setColWidths([50, 50, 100]); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expect(table!.createDOM(editorConfig).outerHTML).toBe( + `
`, + ); + expect(table!.getColWidths()).toStrictEqual([50, 50, 100]); + expect(table!.getColumnCount()).toBe(3); + }); + }); }, undefined, , From 8fb96f98a34925699a5c73a03ed21f5c4de34d8a Mon Sep 17 00:00:00 2001 From: Adam Pugh Date: Wed, 18 Sep 2024 10:46:41 -0500 Subject: [PATCH 019/133] Grammar Issue - Repeated Word (#6643) --- packages/lexical-website/docs/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-website/docs/intro.md b/packages/lexical-website/docs/intro.md index 825fc2e5d65..5b4d62966c4 100644 --- a/packages/lexical-website/docs/intro.md +++ b/packages/lexical-website/docs/intro.md @@ -81,7 +81,7 @@ Node Transforms and Command Listeners are called with an implicit `editor.update It is permitted to do nested updates, or nested reads, but an update should not be nested in a read or vice versa. For example, `editor.update(() => editor.update(() => {...}))` is allowed. It is permitted -to nest nest an `editor.read` at the end of an `editor.update`, but this will immediately flush the update +to nest an `editor.read` at the end of an `editor.update`, but this will immediately flush the update and any additional update in that callback will throw an error. All Lexical Nodes are dependent on the associated Editor State. With few exceptions, you should only call methods From fc8cf099e3a15125652716ab94fc7d2c60f8bf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kub=C3=A1t?= <36794259+skopz356@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:28:21 +0200 Subject: [PATCH 020/133] Allow exporting a document fragment from the exportDOM function (#6641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomáš Kubát --- .../src/__tests__/unit/LexicalHtml.test.ts | 55 ++++++++++++++++++- packages/lexical-html/src/index.ts | 9 ++- packages/lexical/src/LexicalNode.ts | 4 +- packages/lexical/src/LexicalUtils.ts | 11 ++++ packages/lexical/src/index.ts | 1 + 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/packages/lexical-html/src/__tests__/unit/LexicalHtml.test.ts b/packages/lexical-html/src/__tests__/unit/LexicalHtml.test.ts index 5583cf94f5a..8759603419a 100644 --- a/packages/lexical-html/src/__tests__/unit/LexicalHtml.test.ts +++ b/packages/lexical-html/src/__tests__/unit/LexicalHtml.test.ts @@ -6,9 +6,6 @@ * */ -//@ts-ignore-next-line -import type {RangeSelection} from 'lexical'; - import {CodeNode} from '@lexical/code'; import {createHeadlessEditor} from '@lexical/headless'; import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; @@ -20,6 +17,8 @@ import { $createRangeSelection, $createTextNode, $getRoot, + ParagraphNode, + RangeSelection, } from 'lexical'; describe('HTML', () => { @@ -212,4 +211,54 @@ describe('HTML', () => { '

Hello world!

', ); }); + + test('It should output correctly nodes whose export is DocumentFragment', () => { + const editor = createHeadlessEditor({ + html: { + export: new Map([ + [ + ParagraphNode, + () => { + const element = document.createDocumentFragment(); + return { + element, + }; + }, + ], + ]), + }, + nodes: [], + }); + + editor.update( + () => { + const root = $getRoot(); + const p1 = $createParagraphNode(); + const text1 = $createTextNode('Hello'); + p1.append(text1); + const p2 = $createParagraphNode(); + const text2 = $createTextNode('World'); + p2.append(text2); + root.append(p1).append(p2); + // Root + // - ParagraphNode + // -- TextNode "Hello" + // - ParagraphNode + // -- TextNode "World" + }, + { + discrete: true, + }, + ); + + let html = ''; + + editor.update(() => { + html = $generateHtmlFromNodes(editor); + }); + + expect(html).toBe( + 'HelloWorld', + ); + }); }); diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 2975315cc35..732bab44048 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -29,6 +29,7 @@ import { $isTextNode, ArtificialNode__DO_NOT_USE, ElementNode, + isDocumentFragment, isInlineDomNode, } from 'lexical'; @@ -147,7 +148,7 @@ function $appendNodesToHTML( } if (shouldInclude && !shouldExclude) { - if (isHTMLElement(element)) { + if (isHTMLElement(element) || isDocumentFragment(element)) { element.append(fragment); } parentElement.append(element); @@ -155,7 +156,11 @@ function $appendNodesToHTML( if (after) { const newElement = after.call(target, element); if (newElement) { - element.replaceWith(newElement); + if (isDocumentFragment(element)) { + element.replaceChildren(newElement); + } else { + element.replaceWith(newElement); + } } } } else { diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 5f70d3e36df..564989cdc2e 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -155,9 +155,9 @@ export type DOMExportOutputMap = Map< export type DOMExportOutput = { after?: ( - generatedElement: HTMLElement | Text | null | undefined, + generatedElement: HTMLElement | DocumentFragment | Text | null | undefined, ) => HTMLElement | Text | null | undefined; - element: HTMLElement | Text | null; + element: HTMLElement | DocumentFragment | Text | null; }; export type NodeKey = string; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 65b45cf7362..bf7904d3b90 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1668,6 +1668,17 @@ export function isHTMLElement(x: Node | EventTarget): x is HTMLElement { return x.nodeType === 1; } +/** + * @param x - The element being testing + * @returns Returns true if x is a document fragment, false otherwise. + */ +export function isDocumentFragment( + x: Node | EventTarget, +): x is DocumentFragment { + // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors + return x.nodeType === 11; +} + /** * * @param node - the Dom Node to check diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 5ef926b5afc..b3f5013cdd7 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -179,6 +179,7 @@ export { getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, isBlockDomNode, + isDocumentFragment, isHTMLAnchorElement, isHTMLElement, isInlineDomNode, From deb58949b505c08d81b2b0bce7ddbffdbd779ea2 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 18 Sep 2024 21:26:57 -0700 Subject: [PATCH 021/133] [lexical-react][lexical-playground] Bug Fix: Workaround for yjs disconnect race in React StrictMode (#6644) --- .../src/shared/useYjsCollaboration.tsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/lexical-react/src/shared/useYjsCollaboration.tsx b/packages/lexical-react/src/shared/useYjsCollaboration.tsx index 371a6f4721f..a8f4e49da17 100644 --- a/packages/lexical-react/src/shared/useYjsCollaboration.tsx +++ b/packages/lexical-react/src/shared/useYjsCollaboration.tsx @@ -155,9 +155,18 @@ export function useYjsCollaboration( return () => { if (isReloadingDoc.current === false) { - Promise.resolve(connectionPromise).then(() => { + if (connectionPromise) { + connectionPromise.then(disconnect); + } else { + // Workaround for race condition in StrictMode. It's possible there + // is a different race for the above case where connect returns a + // promise, but we don't have an example of that in-repo. + // It's possible that there is a similar issue with + // TOGGLE_CONNECT_COMMAND below when the provider connect returns a + // promise. + // https://github.com/facebook/lexical/issues/6640 disconnect(); - }); + } } provider.off('sync', onSync); @@ -198,18 +207,16 @@ export function useYjsCollaboration( return editor.registerCommand( TOGGLE_CONNECT_COMMAND, (payload) => { - if (connect !== undefined && disconnect !== undefined) { - const shouldConnect = payload; - - if (shouldConnect) { - // eslint-disable-next-line no-console - console.log('Collaboration connected!'); - connect(); - } else { - // eslint-disable-next-line no-console - console.log('Collaboration disconnected!'); - disconnect(); - } + const shouldConnect = payload; + + if (shouldConnect) { + // eslint-disable-next-line no-console + console.log('Collaboration connected!'); + connect(); + } else { + // eslint-disable-next-line no-console + console.log('Collaboration disconnected!'); + disconnect(); } return true; From 4cf0a5372f0b76888a5f3dee31fd026db1bdfc6c Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 19 Sep 2024 20:03:15 -0700 Subject: [PATCH 022/133] [lexical-code] Bug Fix: Annotate @lexical/code as having side-effects for Prism (#6652) --- packages/lexical-code/package.json | 2 +- .../lexical-code/src/CodeHighlightNode.ts | 7 +- packages/lexical-code/src/CodeHighlighter.ts | 8 +- .../lexical-code/src/CodeHighlighterPrism.ts | 11 +- .../lexical-esm-astro-react/package-lock.json | 800 +++++++++++------- .../lexical-esm-astro-react/package.json | 5 +- .../lexical-esm-astro-react/src/env.d.ts | 1 + 7 files changed, 499 insertions(+), 335 deletions(-) diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index 51b999e6a12..89dd18cadee 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -25,7 +25,7 @@ "@types/prismjs": "^1.26.0" }, "module": "LexicalCode.mjs", - "sideEffects": false, + "sideEffects": true, "exports": { ".": { "import": { diff --git a/packages/lexical-code/src/CodeHighlightNode.ts b/packages/lexical-code/src/CodeHighlightNode.ts index 0e60950c248..15f08d207ab 100644 --- a/packages/lexical-code/src/CodeHighlightNode.ts +++ b/packages/lexical-code/src/CodeHighlightNode.ts @@ -17,8 +17,6 @@ import type { TabNode, } from 'lexical'; -import './CodeHighlighterPrism'; - import { addClassNamesToElement, removeClassNamesFromElement, @@ -30,6 +28,7 @@ import { TextNode, } from 'lexical'; +import {Prism} from './CodeHighlighterPrism'; import {$createCodeNode} from './CodeNode'; export const DEFAULT_CODE_LANGUAGE = 'javascript'; @@ -84,11 +83,11 @@ export function getLanguageFriendlyName(lang: string) { export const getDefaultCodeLanguage = (): string => DEFAULT_CODE_LANGUAGE; export const getCodeLanguages = (): Array => - Object.keys(window.Prism.languages) + Object.keys(Prism.languages) .filter( // Prism has several language helpers mixed into languages object // so filtering them out here to get langs list - (language) => typeof window.Prism.languages[language] !== 'function', + (language) => typeof Prism.languages[language] !== 'function', ) .sort(); diff --git a/packages/lexical-code/src/CodeHighlighter.ts b/packages/lexical-code/src/CodeHighlighter.ts index dd721b03e9a..c023b80b760 100644 --- a/packages/lexical-code/src/CodeHighlighter.ts +++ b/packages/lexical-code/src/CodeHighlighter.ts @@ -16,8 +16,6 @@ import type { RangeSelection, } from 'lexical'; -import './CodeHighlighterPrism'; - import {mergeRegister} from '@lexical/utils'; import { $createLineBreakNode, @@ -44,6 +42,7 @@ import { } from 'lexical'; import invariant from 'shared/invariant'; +import {Prism} from './CodeHighlighterPrism'; import { $createCodeHighlightNode, $isCodeHighlightNode, @@ -69,10 +68,9 @@ export interface Tokenizer { export const PrismTokenizer: Tokenizer = { defaultLanguage: DEFAULT_CODE_LANGUAGE, tokenize(code: string, language?: string): (string | Token)[] { - return window.Prism.tokenize( + return Prism.tokenize( code, - window.Prism.languages[language || ''] || - window.Prism.languages[this.defaultLanguage], + Prism.languages[language || ''] || Prism.languages[this.defaultLanguage], ); }, }; diff --git a/packages/lexical-code/src/CodeHighlighterPrism.ts b/packages/lexical-code/src/CodeHighlighterPrism.ts index 9ec68588456..98b17d8f43c 100644 --- a/packages/lexical-code/src/CodeHighlighterPrism.ts +++ b/packages/lexical-code/src/CodeHighlighterPrism.ts @@ -24,14 +24,5 @@ import 'prismjs/components/prism-swift'; import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-java'; import 'prismjs/components/prism-cpp'; -import {CAN_USE_DOM} from 'shared/canUseDOM'; -declare global { - interface Window { - Prism: typeof import('prismjs'); - } -} - -export const Prism: typeof import('prismjs') = CAN_USE_DOM - ? window.Prism - : (global as unknown as {Prism: typeof import('prismjs')}).Prism; +export const Prism: typeof import('prismjs') = globalThis.Prism || window.Prism; diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json index 3d5cb5e1953..26ccfd3cdc4 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json @@ -1,21 +1,21 @@ { "name": "lexical-esm-astro-react", - "version": "0.0.1", + "version": "0.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lexical-esm-astro-react", - "version": "0.0.1", + "version": "0.17.1", "dependencies": { - "@astrojs/check": "^0.5.9", + "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "^0.14.3", - "@lexical/utils": "^0.14.3", + "@lexical/react": "0.17.1", + "@lexical/utils": "0.17.1", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "^0.14.3", + "lexical": "0.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" @@ -37,11 +37,11 @@ } }, "node_modules/@astrojs/check": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.5.10.tgz", - "integrity": "sha512-vliHXM9cu/viGeKiksUM4mXfO816ohWtawTl2ADPgTsd4nUMjFiyAl7xFZhF34yy4hq4qf7jvK1F2PlR3b5I5w==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.3.tgz", + "integrity": "sha512-I6Dz45bMI5YRbp4yK2LKWsHH3/kkHRGdPGruGkLap6pqxhdcNh7oCgN04Ac+haDfc9ow5BYPGPmEhkwef15GQQ==", "dependencies": { - "@astrojs/language-server": "^2.8.4", + "@astrojs/language-server": "^2.14.1", "chokidar": "^3.5.3", "fast-glob": "^3.3.1", "kleur": "^4.1.5", @@ -55,9 +55,9 @@ } }, "node_modules/@astrojs/compiler": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.7.1.tgz", - "integrity": "sha512-/POejAYuj8WEw7ZI0J8JBvevjfp9jQ9Wmu/Bg52RiNwGXkMV7JnYpsenVfHvvf1G7R5sXHGKlTcxlQWhoUTiGQ==" + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.10.3.tgz", + "integrity": "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==" }, "node_modules/@astrojs/internal-helpers": { "version": "0.4.0", @@ -65,25 +65,28 @@ "integrity": "sha512-6B13lz5n6BrbTqCTwhXjJXuR1sqiX/H6rTxzlXx+lN1NnV4jgnq/KJldCQaUWJzPL5SiWahQyinxAbxQtwgPHA==" }, "node_modules/@astrojs/language-server": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.8.4.tgz", - "integrity": "sha512-sJH5vGTBkhgA8+hdhzX78UUp4cFz4Mt7xkEkevD188OS5bDMkaue6hK+dtXWM47mnrXFveXA2u38K7S+5+IRjA==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.14.2.tgz", + "integrity": "sha512-daUJ/+/2pPF3eGG4tVdXKyw0tabUDrJKwLzU8VTuNhEHIn3VZAIES6VT3+mX0lmKcMiKM8/bjZdfY+fPfmnsMA==", "dependencies": { - "@astrojs/compiler": "^2.7.0", + "@astrojs/compiler": "^2.10.3", + "@astrojs/yaml2ts": "^0.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", - "@volar/kit": "~2.1.5", - "@volar/language-core": "~2.1.5", - "@volar/language-server": "~2.1.5", - "@volar/language-service": "~2.1.5", - "@volar/typescript": "~2.1.5", + "@volar/kit": "~2.4.0", + "@volar/language-core": "~2.4.0", + "@volar/language-server": "~2.4.0", + "@volar/language-service": "~2.4.0", + "@volar/typescript": "~2.4.0", "fast-glob": "^3.2.12", - "volar-service-css": "0.0.34", - "volar-service-emmet": "0.0.34", - "volar-service-html": "0.0.34", - "volar-service-prettier": "0.0.34", - "volar-service-typescript": "0.0.34", - "volar-service-typescript-twoslash-queries": "0.0.34", - "vscode-html-languageservice": "^5.1.2", + "muggle-string": "^0.4.1", + "volar-service-css": "0.0.61", + "volar-service-emmet": "0.0.61", + "volar-service-html": "0.0.61", + "volar-service-prettier": "0.0.61", + "volar-service-typescript": "0.0.61", + "volar-service-typescript-twoslash-queries": "0.0.61", + "volar-service-yaml": "0.0.61", + "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "bin": { @@ -187,6 +190,14 @@ "node": ">=8" } }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.1.tgz", + "integrity": "sha512-CBaNwDQJz20E5WxzQh4thLVfhB3JEEGz72wRA+oJp6fQR37QLAqXZJU0mHC+yqMOQ6oj0GfRPJrz6hjf+zm6zA==", + "dependencies": { + "yaml": "^2.5.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -561,11 +572,38 @@ "@emmetio/scanner": "^1.0.4" } }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.0.tgz", + "integrity": "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, "node_modules/@emmetio/scanner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==" }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -955,154 +993,172 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.14.3.tgz", - "integrity": "sha512-kMasHJQCNSSdD6US8XF/GJEZAgdmIUIoqwcV/7Q8jVUICYT53bcr+Rh7RxL+1c7ZpJE2rXg5KTELsUPGjs0uwA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.17.1.tgz", + "integrity": "sha512-OVqnEfWX8XN5xxuMPo6BfgGKHREbz++D5V5ISOiml0Z8fV/TQkdgwqbBJcUdJHGRHWSUwdK7CWGs/VALvVvZyw==", "dependencies": { - "@lexical/html": "0.14.3", - "@lexical/list": "0.14.3", - "@lexical/selection": "0.14.3", - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/html": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/code": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.14.3.tgz", - "integrity": "sha512-eBhs+TsJ5z7Vg/0e77bau86lN7R5nqO7effkPNNndn0XV2VSDpjMF+PTj4Cd1peenFlfqVivBr9gdewDrvPQng==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.17.1.tgz", + "integrity": "sha512-ZspfTm6g6dN3nAb4G5bPp3SqxzdkB/bjGfa0uRKMU6/eBKtrMUgZsGxt0a8JRZ1eq2TZrQhx+l1ceRoLXii/bQ==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1", "prismjs": "^1.27.0" } }, + "node_modules/@lexical/devtools-core": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.17.1.tgz", + "integrity": "sha512-SzL1EX9Rt5GptIo87t6nDxAc9TtYtl6DyAPNz/sCltspdd69KQgs23sTRa26/tkNFCS1jziRN7vpN3mlnmm5wA==", + "dependencies": { + "@lexical/html": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/mark": "0.17.1", + "@lexical/table": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, "node_modules/@lexical/dragon": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.14.3.tgz", - "integrity": "sha512-GTnt5a5Zs1f3q5Z9tC63VPzCFNAG+37ySHO+mQpVqlTsDmwSeJzFKGZyxq81tZXsKaXQZ4llc9K6I1f/XJoypw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.17.1.tgz", + "integrity": "sha512-lhBRKP7RlhiVCLtF0qiNqmMhEO6cQB43sMe7d4bvuY1G2++oKY/XAJPg6QJZdXRrCGRQ6vZ26QRNhRPmCxL5Ng==", "dependencies": { - "lexical": "0.14.3" + "lexical": "0.17.1" } }, "node_modules/@lexical/hashtag": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.14.3.tgz", - "integrity": "sha512-BlMhegitxNscJyM0QGjnzpt7QQaiftVf80dqfiVGdgFJi9hS4wrYEsPpA7jlsZG5Q46DSw/zMRp3tpHfdU6TCQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.17.1.tgz", + "integrity": "sha512-XtP0BI8vEewAe7tzq9MC49UPUvuChuNJI/jqFp+ezZlt/RUq0BClQCOPuSlrTJhluvE2rWnUnOnVMk8ILRvggQ==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/history": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.14.3.tgz", - "integrity": "sha512-I5Ssaz+uRYsFmqN5WfKCyTkPPV1CTnEQ21vuKp8PVI4hBdlIy5aJdeQXbQhg0BdCtQVSjpm7WRGMk5ATiAXLPw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.17.1.tgz", + "integrity": "sha512-OU/ohajz4FXchUhghsWC7xeBPypFe50FCm5OePwo767G7P233IztgRKIng2pTT4zhCPW7S6Mfl53JoFHKehpWA==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/html": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.14.3.tgz", - "integrity": "sha512-ID4RdHdOXv2qIg6cqNhbYiqgcV5aEJFAV+zZ14CMpxPlW71tiRlmy/Pp4WqCFgjnZ2GZRq34+kag+cT2H69ILQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.17.1.tgz", + "integrity": "sha512-yGG+K2DXl7Wn2DpNuZ0Y3uCHJgfHkJN3/MmnFb4jLnH1FoJJiuy7WJb/BRRh9H+6xBJ9v70iv+kttDJ0u1xp5w==", "dependencies": { - "@lexical/selection": "0.14.3", - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/link": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.14.3.tgz", - "integrity": "sha512-txhuzcx2OfOtZ/fy9cgauDGW1gi2vSU0iQdde4i0UP2KK4ltioA9eFkjqAacGiPvwJ8w2CZV9q5Ck4DgFAKQ7w==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.17.1.tgz", + "integrity": "sha512-qFJEKBesZAtR8kfJfIVXRFXVw6dwcpmGCW7duJbtBRjdLjralOxrlVKyFhW9PEXGhi4Mdq2Ux16YnnDncpORdQ==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/list": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.14.3.tgz", - "integrity": "sha512-d9ZiEkZ34DpzBNq2GkedJpXF8sIxSQvHOGhNbVvTuBvgDcCwbmXL0KY4k+xu+jMScRO/3oR7C6YZpZT3GaUO+Q==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.17.1.tgz", + "integrity": "sha512-k9ZnmQuBvW+xVUtWJZwoGtiVG2cy+hxzkLGU4jTq1sqxRIoSeGcjvhFAK8JSEj4i21SgkB1FmkWXoYK5kbwtRA==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/mark": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.14.3.tgz", - "integrity": "sha512-HegYMuiCazmM4XXVUzteA5bOFEiWxeIZSMK98rCV7t5czYlQmgaV5PWIT5/wLnSgrJA6apa02JHLINE9CuUHlw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.17.1.tgz", + "integrity": "sha512-V82SSRjvygmV+ZMwVpy5gwgr2ZDrJpl3TvEDO+G5I4SDSjbgvua8hO4dKryqiDVlooxQq9dsou0GrZ9Qtm6rYg==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/markdown": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.14.3.tgz", - "integrity": "sha512-G97Twk0qq5Mkj7S95fFODN6D7nBZsHiXgd2QeCZQ+qbrItEsjEsM0vCtVBELpZzyl700ExfIJCA9eHrq28VNxw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.17.1.tgz", + "integrity": "sha512-uexR9snyT54jfQTrbr/GZAtzX+8Oyykr4p1HS0vCVL1KU5MDuP2PoyFfOv3rcfB2TASc+aYiINhU2gSXzwCHNg==", "dependencies": { - "@lexical/code": "0.14.3", - "@lexical/link": "0.14.3", - "@lexical/list": "0.14.3", - "@lexical/rich-text": "0.14.3", - "@lexical/text": "0.14.3", - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/code": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/text": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/offset": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.14.3.tgz", - "integrity": "sha512-xzyHLED9N3VPsLSpxs235W1xnh1xLl0SFqLLN9fkZs4fBLPtoPrzfYjjTMx6KgRPCa96GauAMsAaKn+JWHaD4g==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.17.1.tgz", + "integrity": "sha512-fX0ZSIFWwUKAjxf6l21vyXFozJGExKWyWxA+EMuOloNAGotHnAInxep0Mt8t/xcvHs7luuyQUxEPw7YrTJP7aw==", "dependencies": { - "lexical": "0.14.3" + "lexical": "0.17.1" } }, "node_modules/@lexical/overflow": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.14.3.tgz", - "integrity": "sha512-2PabHT5vCtfN1lx2d3j1AW6naGJEcjLyUxEMrPzqNZ8IDGuLbD3uRi/wS8evmFLgKkF5mqRnPPlpwGbqGg+qUw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.17.1.tgz", + "integrity": "sha512-oElVDq486R3rO2+Zz0EllXJGpW3tN0tfcH+joZ5h36+URKuNeKddqkJuDRvgSLOr9l8Jhtv3+/YKduPJVKMz6w==", "dependencies": { - "lexical": "0.14.3" + "lexical": "0.17.1" } }, "node_modules/@lexical/plain-text": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.14.3.tgz", - "integrity": "sha512-Ct3sQmhc34Iuj0YWT5dlLzTcuCLAMx7uaLKb0lxb7A6bcUBPfC1eBv2KtILZ9eW/GEUCMTqYEnmixTY7vPR9AA==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.17.1.tgz", + "integrity": "sha512-CSvi4j1a4ame0OAvOKUCCmn2XrNsWcST4lExGTa9Ei/VIh8IZ+a97h4Uby8T3lqOp10x+oiizYWzY30pb9QaBg==", "dependencies": { - "@lexical/clipboard": "0.14.3", - "@lexical/selection": "0.14.3", - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/react": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.14.3.tgz", - "integrity": "sha512-sUgF7dStJTYvkS14QzlpB5XJ5p498JDSEBSADRsf0KOJsTINAQhh27vXuS8/I2FH3FanonH/RrLwSildL/FnzA==", - "dependencies": { - "@lexical/clipboard": "0.14.3", - "@lexical/code": "0.14.3", - "@lexical/dragon": "0.14.3", - "@lexical/hashtag": "0.14.3", - "@lexical/history": "0.14.3", - "@lexical/link": "0.14.3", - "@lexical/list": "0.14.3", - "@lexical/mark": "0.14.3", - "@lexical/markdown": "0.14.3", - "@lexical/overflow": "0.14.3", - "@lexical/plain-text": "0.14.3", - "@lexical/rich-text": "0.14.3", - "@lexical/selection": "0.14.3", - "@lexical/table": "0.14.3", - "@lexical/text": "0.14.3", - "@lexical/utils": "0.14.3", - "@lexical/yjs": "0.14.3", - "lexical": "0.14.3", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.17.1.tgz", + "integrity": "sha512-DI4k25tO0E1WyozrjaLgKMOmLjOB7+39MT4eZN9brPlU7g+w0wzdGbTZUPgPmFGIKPK+MSLybCwAJCK97j8HzQ==", + "dependencies": { + "@lexical/clipboard": "0.17.1", + "@lexical/code": "0.17.1", + "@lexical/devtools-core": "0.17.1", + "@lexical/dragon": "0.17.1", + "@lexical/hashtag": "0.17.1", + "@lexical/history": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/mark": "0.17.1", + "@lexical/markdown": "0.17.1", + "@lexical/overflow": "0.17.1", + "@lexical/plain-text": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "@lexical/text": "0.17.1", + "@lexical/utils": "0.17.1", + "@lexical/yjs": "0.17.1", + "lexical": "0.17.1", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -1111,59 +1167,59 @@ } }, "node_modules/@lexical/rich-text": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.14.3.tgz", - "integrity": "sha512-o8wGvRDyPSRcfb6bauF5lzK5u/kzCW+hAQq0ExM1e8p4GHDb0vwz9DA6NH5D0BPHb2fUgknwClHOoJX95WUA8A==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.17.1.tgz", + "integrity": "sha512-T3kvj4P1OpedX9jvxN3WN8NP1Khol6mCW2ScFIRNRz2dsXgyN00thH1Q1J/uyu7aKyGS7rzcY0rb1Pz1qFufqQ==", "dependencies": { - "@lexical/clipboard": "0.14.3", - "@lexical/selection": "0.14.3", - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/selection": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.14.3.tgz", - "integrity": "sha512-43EmqG6flLqFJJNZ7GCxFlx3qXy7osB3AQBgxKTthWtQeBrJPdgacctL1jhO7etTIQWP5C1DExy3opDLVKyDjg==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.17.1.tgz", + "integrity": "sha512-qBKVn+lMV2YIoyRELNr1/QssXx/4c0id9NCB/BOuYlG8du5IjviVJquEF56NEv2t0GedDv4BpUwkhXT2QbNAxA==", "dependencies": { - "lexical": "0.14.3" + "lexical": "0.17.1" } }, "node_modules/@lexical/table": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.14.3.tgz", - "integrity": "sha512-9btpU2lfAE34ucIqlMu5RiSVlxREXY7Zp+s26oFsXNoNPhW57iND96TrqwYo9FJl/6zXXfvqYxnUEcUD2dLgwQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.17.1.tgz", + "integrity": "sha512-2fUYPmxhyuMQX3MRvSsNaxbgvwGNJpHaKx1Ldc+PT2MvDZ6ALZkfsxbi0do54Q3i7dOon8/avRp4TuVaCnqvoA==", "dependencies": { - "@lexical/utils": "0.14.3", - "lexical": "0.14.3" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/text": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.14.3.tgz", - "integrity": "sha512-7+B9KkA37iHTlPqt6GHdfBIoaA9dQfhKrQNP9+422/CO/adCru4S94yNxiHXFq7iCvgucfuFop9M8jOfqLQbBQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.17.1.tgz", + "integrity": "sha512-zD2pAGXaMfPpT8PeNrx3+n0+jGnQORHyn0NEBO+hnyacKfUq5z5sI6Gebsq5NwH789bRadmJM5LvX5w8fsuv6w==", "dependencies": { - "lexical": "0.14.3" + "lexical": "0.17.1" } }, "node_modules/@lexical/utils": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.14.3.tgz", - "integrity": "sha512-coqG2AO7QhJCM0xBlYvtETjl0il9u4HQRuc8ye3j8jMfNadVvVVWO3Fodmm/8FTPyJuxIij1Ruma9zqhlAbN6Q==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.17.1.tgz", + "integrity": "sha512-jCQER5EsvhLNxKH3qgcpdWj/necUb82Xjp8qWQ3c0tyL07hIRm2tDRA/s9mQmvcP855HEZSmGVmR5SKtkcEAVg==", "dependencies": { - "@lexical/list": "0.14.3", - "@lexical/selection": "0.14.3", - "@lexical/table": "0.14.3", - "lexical": "0.14.3" + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "lexical": "0.17.1" } }, "node_modules/@lexical/yjs": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.14.3.tgz", - "integrity": "sha512-Ju+PQJg4NjQoNzfPlQKa6A71sjgGWj5lL4cbe+4xlNoknfK3NApVeznOi3xAM7rUCr6fPBAjzF9/uwfMXR451g==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.17.1.tgz", + "integrity": "sha512-9mn5PDtaH5uLMH6hQ59EAx5FkRzmJJFcVs3E6zSIbtgkG3UASR3CFEfgsLKTjl/GC5NnTGuMck+jXaupDVBhOg==", "dependencies": { - "@lexical/offset": "0.14.3", - "lexical": "0.14.3" + "@lexical/offset": "0.17.1", + "lexical": "0.17.1" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -1536,12 +1592,12 @@ } }, "node_modules/@volar/kit": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.1.6.tgz", - "integrity": "sha512-dSuXChDGM0nSG/0fxqlNfadjpAeeo1P1SJPBQ+pDf8H1XrqeJq5gIhxRTEbiS+dyNIG69ATq1CArkbCif+oxJw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.5.tgz", + "integrity": "sha512-ZzyErW5UiDfiIuJ/lpqc2Kx5PHDGDZ/bPlPJYpRcxlrn8Z8aDhRlsLHkNKcNiH65TmNahk2kbLaiejiqu6BD3A==", "dependencies": { - "@volar/language-service": "2.1.6", - "@volar/typescript": "2.1.6", + "@volar/language-service": "2.4.5", + "@volar/typescript": "2.4.5", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" @@ -1551,23 +1607,21 @@ } }, "node_modules/@volar/language-core": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.6.tgz", - "integrity": "sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz", + "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==", "dependencies": { - "@volar/source-map": "2.1.6" + "@volar/source-map": "2.4.5" } }, "node_modules/@volar/language-server": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.1.6.tgz", - "integrity": "sha512-0w+FV8ro37hVb3qE4ONo3VbS5kEQXv4H/D2xCePyY5dRw6XnbJAPFNKvoxI9mxHTPonvIG1si5rN9MSGSKtgZQ==", - "dependencies": { - "@volar/language-core": "2.1.6", - "@volar/language-service": "2.1.6", - "@volar/snapshot-document": "2.1.6", - "@volar/typescript": "2.1.6", - "@vscode/l10n": "^0.0.16", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.5.tgz", + "integrity": "sha512-l5PswE0JzCtstTlwBUpikeSa3lNUBJhTuWtj9KclZTGi2Uex4RcqGOhTiDsUUtvdv/hEuYCxGq1EdJJPlQsD/g==", + "dependencies": { + "@volar/language-core": "2.4.5", + "@volar/language-service": "2.4.5", + "@volar/typescript": "2.4.5", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", @@ -1577,46 +1631,35 @@ } }, "node_modules/@volar/language-service": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.1.6.tgz", - "integrity": "sha512-1OpbbPQ6wUIumwMP5r45y8utVEmvq1n6BC8JHqGKsuFr9RGFIldDBlvA/xuO3MDKhjmmPGPHKb54kg1/YN78ow==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.5.tgz", + "integrity": "sha512-xiFlL0aViGg6JhwAXyohPrdlID13uom8WQg6DWYaV8ob8RRy+zoLlBUI8SpQctwlWEO9poyrYK01revijAwkcw==", "dependencies": { - "@volar/language-core": "2.1.6", + "@volar/language-core": "2.4.5", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, - "node_modules/@volar/snapshot-document": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/snapshot-document/-/snapshot-document-2.1.6.tgz", - "integrity": "sha512-YNYk1sCOrGg7VHbZM+1It97q0GWhFxdqIwnxSNFoL0X1LuSRXoCT2DRb/aa1J6aBpPMbKqSFUWHGQEAFUnc4Zw==", - "dependencies": { - "vscode-languageserver-protocol": "^3.17.5", - "vscode-languageserver-textdocument": "^1.0.11" - } - }, "node_modules/@volar/source-map": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.6.tgz", - "integrity": "sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==", - "dependencies": { - "muggle-string": "^0.4.0" - } + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz", + "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==" }, "node_modules/@volar/typescript": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.6.tgz", - "integrity": "sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz", + "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==", "dependencies": { - "@volar/language-core": "2.1.6", - "path-browserify": "^1.0.1" + "@volar/language-core": "2.4.5", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "node_modules/@vscode/emmet-helper": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.9.2.tgz", - "integrity": "sha512-MaGuyW+fa13q3aYsluKqclmh62Hgp0BpKIqS66fCxfOaBcVQ1OnMQxRRgQUYnCkxFISAQlkJ0qWWPyXjro1Qrg==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.9.3.tgz", + "integrity": "sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==", "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", @@ -1631,9 +1674,9 @@ "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" }, "node_modules/@vscode/l10n": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", - "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" }, "node_modules/acorn": { "version": "8.11.3", @@ -1646,6 +1689,21 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -2575,9 +2633,9 @@ "integrity": "sha512-OGkMXLY7XH6ykHE5ZOVVIMHaGAvvxqw98cswTKB683dntBJre7ufm9wouJ0ExDm0VXhHenU8mREvxIbV5nNoVQ==" }, "node_modules/emmet": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.7.tgz", - "integrity": "sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==", + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.8.tgz", + "integrity": "sha512-wFe/dxsx7oi/M2UJ/3yBu4Fm24Irho6lqut4C1YFaZebCvCCMygoDGC7W6I+8+K8PAjfa/Ojn52UHi8WCdDiRA==", "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" @@ -2738,6 +2796,11 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -2759,6 +2822,11 @@ "node": ">=8.6.0" } }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3421,6 +3489,11 @@ "node": ">=4" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3454,14 +3527,14 @@ } }, "node_modules/lexical": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.14.3.tgz", - "integrity": "sha512-LaWSKj6OpvJ+bdfQA2AybEzho0YoWfAdRGkuCtPNYd/uf7IHyoEwCFQsIBvWCQF23saDgE1NONR4uiwl6iaJ9g==" + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.17.1.tgz", + "integrity": "sha512-72/MhR7jqmyqD10bmJw8gztlCm4KDDT+TPtU4elqXrEvHoO5XENi34YAEUD9gIkPfqSwyLa9mwAX1nKzIr5xEA==" }, "node_modules/lib0": { - "version": "0.2.93", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.93.tgz", - "integrity": "sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==", + "version": "0.2.97", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.97.tgz", + "integrity": "sha512-Q4d1ekgvufi9FiHkkL46AhecfNjznSL9MRNoJRQ76gBHS9OqU2ArfQK0FvBpuxgWeJeNI0LVgAYMIpsGeX4gYg==", "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" @@ -3527,6 +3600,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/log-symbols": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", @@ -5050,6 +5128,21 @@ "node": ">=8.15" } }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -5357,6 +5450,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -6309,31 +6410,17 @@ } }, "node_modules/typescript-auto-import-cache": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.2.tgz", - "integrity": "sha512-+laqe5SFL1vN62FPOOJSUDTZxtgsoOXjneYOXIpx5rQ4UMiN89NAtJLpqLqyebv9fgQ/IMeeTX+mQyRnwvJzvg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz", + "integrity": "sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==", "dependencies": { "semver": "^7.3.8" } }, - "node_modules/typescript-auto-import-cache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/typescript-auto-import-cache/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -6341,11 +6428,6 @@ "node": ">=10" } }, - "node_modules/typescript-auto-import-cache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/ultrahtml": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", @@ -7026,16 +7108,16 @@ } }, "node_modules/volar-service-css": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.34.tgz", - "integrity": "sha512-C7ua0j80ZD7bsgALAz/cA1bykPehoIa5n+3+Ccr+YLpj0fypqw9iLUmGLX11CqzqNCO2XFGe/1eXB/c+SWrF/g==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.61.tgz", + "integrity": "sha512-Ct9L/w+IB1JU8F4jofcNCGoHy6TF83aiapfZq9A0qYYpq+Kk5dH+ONS+rVZSsuhsunq8UvAuF8Gk6B8IFLfniw==", "dependencies": { - "vscode-css-languageservice": "^6.2.10", + "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { - "@volar/language-service": "~2.1.0" + "@volar/language-service": "~2.4.0" }, "peerDependenciesMeta": { "@volar/language-service": { @@ -7044,15 +7126,17 @@ } }, "node_modules/volar-service-emmet": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.34.tgz", - "integrity": "sha512-ubQvMCmHPp8Ic82LMPkgrp9ot+u2p/RDd0RyT0EykRkZpWsagHUF5HWkVheLfiMyx2rFuWx/+7qZPOgypx6h6g==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.61.tgz", + "integrity": "sha512-iiYqBxjjcekqrRruw4COQHZME6EZYWVbkHjHDbULpml3g8HGJHzpAMkj9tXNCPxf36A+f1oUYjsvZt36qPg4cg==", "dependencies": { - "@vscode/emmet-helper": "^2.9.2", - "vscode-html-languageservice": "^5.1.0" + "@emmetio/css-parser": "^0.4.0", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" }, "peerDependencies": { - "@volar/language-service": "~2.1.0" + "@volar/language-service": "~2.4.0" }, "peerDependenciesMeta": { "@volar/language-service": { @@ -7061,16 +7145,16 @@ } }, "node_modules/volar-service-html": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.34.tgz", - "integrity": "sha512-kMEneea1tQbiRcyKavqdrSVt8zV06t+0/3pGkjO3gV6sikXTNShIDkdtB4Tq9vE2cQdM50TuS7utVV7iysUxHw==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.61.tgz", + "integrity": "sha512-yFE+YmmgqIL5HI4ORqP++IYb1QaGcv+xBboI0WkCxJJ/M35HZj7f5rbT3eQ24ECLXFbFCFanckwyWJVz5KmN3Q==", "dependencies": { - "vscode-html-languageservice": "^5.1.0", + "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { - "@volar/language-service": "~2.1.0" + "@volar/language-service": "~2.4.0" }, "peerDependenciesMeta": { "@volar/language-service": { @@ -7079,14 +7163,14 @@ } }, "node_modules/volar-service-prettier": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.34.tgz", - "integrity": "sha512-BNfJ8FwfPi1Wm/JkuzNjraOLdtKieGksNT/bDyquygVawv1QUzO2HB1hiMKfZGdcSFG5ZL9R0j7bBfRTfXA2gg==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.61.tgz", + "integrity": "sha512-F612nql5I0IS8HxXemCGvOR2Uxd4XooIwqYVUvk7WSBxP/+xu1jYvE3QJ7EVpl8Ty3S4SxPXYiYTsG3bi+gzIQ==", "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { - "@volar/language-service": "~2.1.0", + "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "peerDependenciesMeta": { @@ -7099,18 +7183,19 @@ } }, "node_modules/volar-service-typescript": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.34.tgz", - "integrity": "sha512-NbAry0w8ZXFgGsflvMwmPDCzgJGx3C+eYxFEbldaumkpTAJiywECWiUbPIOfmEHgpOllUKSnhwtLlWFK4YnfQg==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.61.tgz", + "integrity": "sha512-4kRHxVbW7wFBHZWRU6yWxTgiKETBDIJNwmJUAWeP0mHaKpnDGj/astdRFKqGFRYVeEYl45lcUPhdJyrzanjsdQ==", "dependencies": { "path-browserify": "^1.0.1", - "semver": "^7.5.4", - "typescript-auto-import-cache": "^0.3.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.3", "vscode-languageserver-textdocument": "^1.0.11", - "vscode-nls": "^5.2.0" + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" }, "peerDependencies": { - "@volar/language-service": "~2.1.0" + "@volar/language-service": "~2.4.0" }, "peerDependenciesMeta": { "@volar/language-service": { @@ -7119,11 +7204,14 @@ } }, "node_modules/volar-service-typescript-twoslash-queries": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.34.tgz", - "integrity": "sha512-XAY2YtWKUp6ht89gxt3L5Dr46LU45d/VlBkj1KXUwNlinpoWiGN4Nm3B6DRF3VoBThAnQgm4c7WD0S+5yTzh+w==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.61.tgz", + "integrity": "sha512-99FICGrEF0r1E2tV+SvprHPw9Knyg7BdW2fUch0tf59kG+KG+Tj4tL6tUg+cy8f23O/VXlmsWFMIE+bx1dXPnQ==", + "dependencies": { + "vscode-uri": "^3.0.8" + }, "peerDependencies": { - "@volar/language-service": "~2.1.0" + "@volar/language-service": "~2.4.0" }, "peerDependenciesMeta": { "@volar/language-service": { @@ -7131,67 +7219,75 @@ } } }, - "node_modules/volar-service-typescript/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" + "node_modules/volar-service-typescript/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, - "node_modules/volar-service-typescript/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "node_modules/volar-service-yaml": { + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.61.tgz", + "integrity": "sha512-L+gbDiLDQQ1rZUbJ3mf3doDsoQUa8OZM/xdpk/unMg1Vz24Zmi2Ign8GrZyBD7bRoIQDwOH9gdktGDKzRPpUNw==", "dependencies": { - "lru-cache": "^6.0.0" + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.15.0" }, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "@volar/language-service": "~2.4.0" }, - "engines": { - "node": ">=10" + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } } }, - "node_modules/volar-service-typescript/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/vscode-css-languageservice": { - "version": "6.2.13", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.2.13.tgz", - "integrity": "sha512-2rKWXfH++Kxd9Z4QuEgd1IF7WmblWWU7DScuyf1YumoGLkY9DW6wF/OTlhOyO2rN63sWHX2dehIpKBbho4ZwvA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.1.tgz", + "integrity": "sha512-1BzTBuJfwMc3A0uX4JBdJgoxp74cjj4q2mDJdp49yD/GuAq4X0k5WtK6fNcMYr+FfJ9nqgR6lpfCSZDkARJ5qQ==", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.0.8" } }, - "node_modules/vscode-css-languageservice/node_modules/@vscode/l10n": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", - "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" - }, "node_modules/vscode-html-languageservice": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.2.0.tgz", - "integrity": "sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.3.1.tgz", + "integrity": "sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.0.8" } }, - "node_modules/vscode-html-languageservice/node_modules/@vscode/l10n": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", - "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", @@ -7222,9 +7318,9 @@ } }, "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", - "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", @@ -7386,6 +7482,86 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yaml-language-server": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.15.0.tgz", + "integrity": "sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==", + "dependencies": { + "ajv": "^8.11.0", + "lodash": "4.17.21", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2", + "yaml": "2.2.2" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + }, + "optionalDependencies": { + "prettier": "2.8.7" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==" + }, + "node_modules/yaml-language-server/node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/yaml-language-server/node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -7449,9 +7625,9 @@ } }, "node_modules/yjs": { - "version": "13.6.14", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.14.tgz", - "integrity": "sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ==", + "version": "13.6.19", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.19.tgz", + "integrity": "sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==", "peer": true, "dependencies": { "lib0": "^0.2.86" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index dc92f3c2030..c278cecc2f4 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -11,7 +11,7 @@ "test": "playwright test" }, "dependencies": { - "@astrojs/check": "^0.5.9", + "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", "@lexical/react": "0.17.1", "@lexical/utils": "0.17.1", @@ -26,6 +26,5 @@ "devDependencies": { "@playwright/test": "^1.43.1" }, - "sideEffects": false, - "exports": {} + "sideEffects": false } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/src/env.d.ts b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/src/env.d.ts index f964fe0cffd..acef35f175a 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/src/env.d.ts +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/src/env.d.ts @@ -1 +1,2 @@ +/// /// From 23b3a6d79680e5bfab58be1e39d145f002603fd7 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 20 Sep 2024 11:11:47 +0800 Subject: [PATCH 023/133] [lexical-markdown] shouldMergeAdjacentLines as an option (#6642) --- .../lexical-markdown/flow/LexicalMarkdown.js.flow | 1 + .../lexical-markdown/src/MarkdownTransformers.ts | 8 ++++++-- .../src/__tests__/unit/LexicalMarkdown.test.ts | 14 ++++++++++++++ packages/lexical-markdown/src/index.ts | 6 +++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow index 4ceb12cffaa..19bf5f59f36 100644 --- a/packages/lexical-markdown/flow/LexicalMarkdown.js.flow +++ b/packages/lexical-markdown/flow/LexicalMarkdown.js.flow @@ -95,6 +95,7 @@ declare export function $convertFromMarkdownString( transformers?: Array, node?: ElementNode, shouldPreserveNewLines?: boolean, + shouldMergeAdjacentLines?: boolean, ): void; // TODO: diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index 37e3d4b8f6d..5c9005f4a93 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -532,7 +532,10 @@ export const LINK: TextMatchTransformer = { type: 'text-match', }; -export function normalizeMarkdown(input: string): string { +export function normalizeMarkdown( + input: string, + shouldMergeAdjacentLines = true, +): string { const lines = input.split('\n'); let inCodeBlock = false; const sanitizedLines: string[] = []; @@ -573,7 +576,8 @@ export function normalizeMarkdown(input: string): string { UNORDERED_LIST_REGEX.test(line) || CHECK_LIST_REGEX.test(line) || TABLE_ROW_REG_EXP.test(line) || - TABLE_ROW_DIVIDER_REG_EXP.test(line) + TABLE_ROW_DIVIDER_REG_EXP.test(line) || + !shouldMergeAdjacentLines ) { sanitizedLines.push(line); } else { diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index a18318f0ae6..c7c9874bb2f 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -65,6 +65,7 @@ describe('Markdown', () => { skipExport?: true; skipImport?: true; shouldPreserveNewLines?: true; + shouldMergeAdjacentLines?: true | false; customTransformers?: Transformer[]; }>; @@ -202,6 +203,17 @@ describe('Markdown', () => { html: '

Hello world!

', md: '*Hello **world**!*', }, + { + html: '

helloworld

', + md: 'hello\nworld', + shouldMergeAdjacentLines: true, + skipExport: true, + }, + { + html: '

hello
world

', + md: 'hello\nworld', + shouldMergeAdjacentLines: false, + }, { html: '

hello
world

', md: 'hello\nworld', @@ -342,6 +354,7 @@ describe('Markdown', () => { md, skipImport, shouldPreserveNewLines, + shouldMergeAdjacentLines, customTransformers, } of IMPORT_AND_EXPORT) { if (skipImport) { @@ -371,6 +384,7 @@ describe('Markdown', () => { ], undefined, shouldPreserveNewLines, + shouldMergeAdjacentLines, ), { discrete: true, diff --git a/packages/lexical-markdown/src/index.ts b/packages/lexical-markdown/src/index.ts index ec32900fce5..671b56e5e4e 100644 --- a/packages/lexical-markdown/src/index.ts +++ b/packages/lexical-markdown/src/index.ts @@ -76,16 +76,20 @@ const TRANSFORMERS: Array = [ /** * Renders markdown from a string. The selection is moved to the start after the operation. + * + * @param {boolean} [shouldPreserveNewLines] By setting this to true, new lines will be preserved between conversions + * @param {boolean} [shouldMergeAdjacentLines] By setting this to true, adjacent non empty lines will be merged according to commonmark spec: https://spec.commonmark.org/0.24/#example-177. Not applicable if shouldPreserveNewLines = true. */ function $convertFromMarkdownString( markdown: string, transformers: Array = TRANSFORMERS, node?: ElementNode, shouldPreserveNewLines = false, + shouldMergeAdjacentLines = true, ): void { const sanitizedMarkdown = shouldPreserveNewLines ? markdown - : normalizeMarkdown(markdown); + : normalizeMarkdown(markdown, shouldMergeAdjacentLines); const importMarkdown = createMarkdownImport( transformers, shouldPreserveNewLines, From db6454f18a531607ab3f2380458bed2c05a1d6b9 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 20 Sep 2024 11:21:39 +0800 Subject: [PATCH 024/133] [lexical-playground] Bug Fix: empty code block not focused (#6649) --- .../src/plugins/ActionsPlugin/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx index 13b9b759db8..7fa791a2a1b 100644 --- a/packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx @@ -183,11 +183,12 @@ export default function ActionsPlugin({ undefined, //node shouldPreserveNewLinesInMarkdown, ); - root - .clear() - .append( - $createCodeNode('markdown').append($createTextNode(markdown)), - ); + const codeNode = $createCodeNode('markdown'); + codeNode.append($createTextNode(markdown)); + root.clear().append(codeNode); + if (markdown.length === 0) { + codeNode.select(); + } } }); }, [editor, shouldPreserveNewLinesInMarkdown]); @@ -218,6 +219,7 @@ export default function ActionsPlugin({ aria-label="Import editor state from JSON"> + + {isEditable && ( + + )} Date: Wed, 16 Oct 2024 08:48:23 -0600 Subject: [PATCH 053/133] [lexical-markdown] Feature: add ability to hook into the import process for multiline element transformers (#6682) Co-authored-by: Sherry --- .../lexical-markdown/src/MarkdownImport.ts | 24 +++- .../src/MarkdownTransformers.ts | 13 ++ .../__tests__/unit/LexicalMarkdown.test.ts | 117 ++++++++++++++++++ 3 files changed, 149 insertions(+), 5 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 99b7900b144..2f7dc27324b 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -117,16 +117,30 @@ function $importMultiline( multilineElementTransformers: Array, rootNode: ElementNode, ): [boolean, number] { - for (const { - regExpStart, - regExpEnd, - replace, - } of multilineElementTransformers) { + for (const transformer of multilineElementTransformers) { + const {handleImportAfterStartMatch, regExpEnd, regExpStart, replace} = + transformer; + const startMatch = lines[startLineIndex].match(regExpStart); if (!startMatch) { continue; // Try next transformer } + if (handleImportAfterStartMatch) { + const result = handleImportAfterStartMatch({ + lines, + rootNode, + startLineIndex, + startMatch, + transformer, + }); + if (result === null) { + continue; + } else if (result) { + return result; + } + } + const regexpEndRegex: RegExp | undefined = typeof regExpEnd === 'object' && 'regExp' in regExpEnd ? regExpEnd.regExp diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index b724b3ba0ca..2a335156213 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -75,6 +75,19 @@ export type ElementTransformer = { }; export type MultilineElementTransformer = { + /** + * Use this function to manually handle the import process, once the `regExpStart` has matched successfully. + * Without providing this function, the default behavior is to match until `regExpEnd` is found, or until the end of the document if `regExpEnd.optional` is true. + * + * @returns a tuple or null. The first element of the returned tuple is a boolean indicating if a multiline element was imported. The second element is the index of the last line that was processed. If null is returned, the next multilineElementTransformer will be tried. If undefined is returned, the default behavior will be used. + */ + handleImportAfterStartMatch?: (args: { + lines: Array; + rootNode: ElementNode; + startLineIndex: number; + startMatch: RegExpMatchArray; + transformer: MultilineElementTransformer; + }) => [boolean, number] | null | undefined; dependencies: Array>; /** * `export` is called when the `$convertToMarkdownString` is called to convert the editor state into markdown. diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index d4c1a90148a..be7199eefab 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -23,6 +23,7 @@ import { TRANSFORMERS, } from '../..'; import { + CODE, MultilineElementTransformer, normalizeMarkdown, } from '../../MarkdownTransformers'; @@ -58,6 +59,115 @@ const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { type: 'multiline-element', }; +const CODE_TAG_COUNTER_EXAMPLE: MultilineElementTransformer = { + dependencies: CODE.dependencies, + export: CODE.export, + handleImportAfterStartMatch({lines, rootNode, startLineIndex, startMatch}) { + const regexpEndRegex: RegExp | undefined = /[ \t]*```$/; + + const isEndOptional = false; + + let endLineIndex = startLineIndex; + const linesLength = lines.length; + + let openedSubStartMatches = 0; + + // check every single line for the closing match. It could also be on the same line as the opening match. + while (endLineIndex < linesLength) { + const potentialSubStartMatch = + lines[endLineIndex].match(/^[ \t]*```(\w+)?/); + + const endMatch = regexpEndRegex + ? lines[endLineIndex].match(regexpEndRegex) + : null; + + if (potentialSubStartMatch) { + if (endMatch) { + if ((potentialSubStartMatch.index ?? 0) < (endMatch.index ?? 0)) { + openedSubStartMatches++; + } + } else { + openedSubStartMatches++; + } + } + + if (endMatch) { + openedSubStartMatches--; + } + + if (!endMatch || openedSubStartMatches > 0) { + if ( + !isEndOptional || + (isEndOptional && endLineIndex < linesLength - 1) // Optional end, but didn't reach the end of the document yet => continue searching for potential closing match + ) { + endLineIndex++; + continue; // Search next line for closing match + } + } + + // Now, check if the closing match matched is the same as the opening match. + // If it is, we need to continue searching for the actual closing match. + if ( + endMatch && + startLineIndex === endLineIndex && + endMatch.index === startMatch.index + ) { + endLineIndex++; + continue; // Search next line for closing match + } + + // At this point, we have found the closing match. Next: calculate the lines in between open and closing match + // This should not include the matches themselves, and be split up by lines + const linesInBetween: string[] = []; + + if (endMatch && startLineIndex === endLineIndex) { + linesInBetween.push( + lines[startLineIndex].slice( + startMatch[0].length, + -endMatch[0].length, + ), + ); + } else { + for (let i = startLineIndex; i <= endLineIndex; i++) { + if (i === startLineIndex) { + const text = lines[i].slice(startMatch[0].length); + linesInBetween.push(text); // Also include empty text + } else if (i === endLineIndex && endMatch) { + const text = lines[i].slice(0, -endMatch[0].length); + linesInBetween.push(text); // Also include empty text + } else { + linesInBetween.push(lines[i]); + } + } + } + + if ( + CODE.replace( + rootNode, + null, + startMatch, + endMatch, + linesInBetween, + true, + ) !== false + ) { + // Return here. This $importMultiline function is run line by line and should only process a single multiline element at a time. + return [true, endLineIndex]; + } + + // The replace function returned false, despite finding the matching open and close tags => this transformer does not want to handle it. + // Thus, we continue letting the remaining transformers handle the passed lines of text from the beginning + break; + } + + // No multiline transformer handled this line successfully + return [false, startLineIndex]; + }, + regExpStart: CODE.regExpStart, + replace: CODE.replace, + type: 'multiline-element', +}; + describe('Markdown', () => { type Input = Array<{ html: string; @@ -344,6 +454,13 @@ describe('Markdown', () => { shouldMergeAdjacentLines: true, skipExport: true, }, + { + customTransformers: [CODE_TAG_COUNTER_EXAMPLE], + // Ensure special ``` code block supports nested code blocks + html: '
Code\n```ts\nSub Code\n```
', + md: '```ts\nCode\n```ts\nSub Code\n```\n```', + skipExport: true, + }, ]; const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = { From ad1d14efc3a06ea692d293135d79ca9ffdfd900b Mon Sep 17 00:00:00 2001 From: Neysan Foo Date: Thu, 17 Oct 2024 04:21:42 +0800 Subject: [PATCH 054/133] [lexical-playground] Bug Fix: Disable equation editing in read-only mode (#6707) --- .../src/nodes/EquationComponent.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/lexical-playground/src/nodes/EquationComponent.tsx b/packages/lexical-playground/src/nodes/EquationComponent.tsx index e4eeb64aeb2..6929f5363f7 100644 --- a/packages/lexical-playground/src/nodes/EquationComponent.tsx +++ b/packages/lexical-playground/src/nodes/EquationComponent.tsx @@ -7,6 +7,7 @@ */ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import {mergeRegister} from '@lexical/utils'; import { $getNodeByKey, @@ -37,6 +38,7 @@ export default function EquationComponent({ nodeKey, }: EquationComponentProps): JSX.Element { const [editor] = useLexicalComposerContext(); + const isEditable = useLexicalEditable(); const [equationValue, setEquationValue] = useState(equation); const [showEquationEditor, setShowEquationEditor] = useState(false); const inputRef = useRef(null); @@ -64,6 +66,9 @@ export default function EquationComponent({ }, [showEquationEditor, equation, equationValue]); useEffect(() => { + if (!isEditable) { + return; + } if (showEquationEditor) { return mergeRegister( editor.registerCommand( @@ -107,11 +112,11 @@ export default function EquationComponent({ } }); } - }, [editor, nodeKey, onHide, showEquationEditor]); + }, [editor, nodeKey, onHide, showEquationEditor, isEditable]); return ( <> - {showEquationEditor ? ( + {showEquationEditor && isEditable ? ( setShowEquationEditor(true)} + onDoubleClick={() => { + if (isEditable) { + setShowEquationEditor(true); + } + }} /> )} From 24e83416064fbc5d4dc87a31f789b3c9bdda7b36 Mon Sep 17 00:00:00 2001 From: Katsia <47710336+KatsiarynaDzibrova@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:22:20 +0100 Subject: [PATCH 055/133] Bug Fix: Shift+down selects an extra subsequent element for Table selection (#6679) Co-authored-by: Ivaylo Pavlov --- .../__tests__/e2e/Selection.spec.mjs | 59 ++++++++++++++-- .../src/LexicalTableSelectionHelpers.ts | 69 ++++++++++++++++++- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index d458cb1ea6a..026cd94d92d 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -1011,8 +1011,8 @@ test.describe.parallel('Selection', () => { await assertSelection(page, { anchorOffset: 0, anchorPath: [0], - focusOffset: 0, - focusPath: [2], + focusOffset: 1, + focusPath: [1, 2, 1], }); }); @@ -1036,8 +1036,8 @@ test.describe.parallel('Selection', () => { await assertSelection(page, { anchorOffset: 0, anchorPath: [2], - focusOffset: 0, - focusPath: [0], + focusOffset: 1, + focusPath: [1, 1, 0], }); }); @@ -1147,4 +1147,55 @@ test.describe.parallel('Selection', () => { focus: {x: 1, y: 1}, }); }); + + test('shift+arrowdown into a table does not select element after', async ({ + page, + isPlainText, + isCollab, + legacyEvents, + browserName, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await insertTable(page, 2, 2); + + await moveToEditorEnd(page); + await page.keyboard.type('def'); + + await moveToEditorBeginning(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.up('Shift'); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 1, + focusPath: [1, 2, 1], + }); + }); + + test('shift+arrowup into a table does not select element before', async ({ + page, + isPlainText, + isCollab, + legacyEvents, + browserName, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await insertTable(page, 2, 2); + await moveToEditorBeginning(page); + await page.keyboard.type('abc'); + + await moveToEditorEnd(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.up('Shift'); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 1, + focusPath: [1, 1, 0], + }); + }); }); diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index be84112c306..f85709749b4 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -1401,8 +1401,64 @@ function $handleArrowKey( (direction === 'up' || direction === 'down') ) { const focusNode = selection.focus.getNode(); - if ($isRootOrShadowRoot(focusNode)) { - const selectedNode = selection.getNodes()[0]; + const isTableUnselect = + !selection.isCollapsed() && + ((direction === 'up' && !selection.isBackward()) || + (direction === 'down' && selection.isBackward())); + if (isTableUnselect) { + let focusParentNode = $findMatchingParent(focusNode, (n) => + $isTableNode(n), + ); + if ($isTableCellNode(focusParentNode)) { + focusParentNode = $findMatchingParent( + focusParentNode, + $isTableNode, + ); + } + if (focusParentNode !== tableNode) { + return false; + } + if (!focusParentNode) { + return false; + } + const sibling = + direction === 'down' + ? focusParentNode.getNextSibling() + : focusParentNode.getPreviousSibling(); + if (!sibling) { + return false; + } + let newOffset = 0; + if (direction === 'up') { + if ($isElementNode(sibling)) { + newOffset = sibling.getChildrenSize(); + } + } + let newFocusNode = sibling; + if (direction === 'up') { + if ($isElementNode(sibling)) { + const lastCell = sibling.getLastChild(); + newFocusNode = lastCell ? lastCell : sibling; + newOffset = $isTextNode(newFocusNode) + ? newFocusNode.getTextContentSize() + : 0; + } + } + const newSelection = selection.clone(); + + newSelection.focus.set( + newFocusNode.getKey(), + newOffset, + $isTextNode(newFocusNode) ? 'text' : 'element', + ); + $setSelection(newSelection); + stopEvent(event); + return true; + } else if ($isRootOrShadowRoot(focusNode)) { + const selectedNode = + direction === 'up' + ? selection.getNodes()[selection.getNodes().length - 1] + : selection.getNodes()[0]; if (selectedNode) { const tableCellNode = $findMatchingParent( selectedNode, @@ -1441,10 +1497,16 @@ function $handleArrowKey( } return false; } else { - const focusParentNode = $findMatchingParent( + let focusParentNode = $findMatchingParent( focusNode, (n) => $isElementNode(n) && !n.isInline(), ); + if ($isTableCellNode(focusParentNode)) { + focusParentNode = $findMatchingParent( + focusParentNode, + $isTableNode, + ); + } if (!focusParentNode) { return false; } @@ -1469,6 +1531,7 @@ function $handleArrowKey( direction === 'up' ? 0 : lastCellNode.getChildrenSize(), 'element', ); + stopEvent(event); $setSelection(newSelection); return true; } From 790b5161d2f15e22bc5d7037a2e2f5fca5795af7 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 17 Oct 2024 04:30:54 +0530 Subject: [PATCH 056/133] [lexical-table] Return inserted node from `$insertTableRow__EXPERIMENTAL` and `$insertTableColumn__EXPERIMENTAL` (#6741) --- .../lexical-table/flow/LexicalTable.js.flow | 4 ++-- .../lexical-table/src/LexicalTableUtils.ts | 23 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/lexical-table/flow/LexicalTable.js.flow b/packages/lexical-table/flow/LexicalTable.js.flow index 0c58fc56374..8ec1b813b10 100644 --- a/packages/lexical-table/flow/LexicalTable.js.flow +++ b/packages/lexical-table/flow/LexicalTable.js.flow @@ -237,11 +237,11 @@ declare export function $deleteTableColumn( declare export function $insertTableRow__EXPERIMENTAL( insertAfter: boolean, -): void; +): TableRowNode | null; declare export function $insertTableColumn__EXPERIMENTAL( insertAfter: boolean, -): void; +): TableCellNode | null; declare export function $deleteTableRow__EXPERIMENTAL(): void; diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index 7a4aa7534ea..c2cce0126a1 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -245,7 +245,14 @@ const getHeaderState = ( return TableCellHeaderStates.NO_STATUS; }; -export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void { +/** + * Inserts a table row before or after the current focus cell node, + * taking into account any spans. If successful, returns the + * inserted table row node. + */ +export function $insertTableRow__EXPERIMENTAL( + insertAfter = true, +): TableRowNode | null { const selection = $getSelection(); invariant( $isRangeSelection(selection) || $isTableSelection(selection), @@ -256,6 +263,7 @@ export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void { const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell); const columnCount = gridMap[0].length; const {startRow: focusStartRow} = focusCellMap; + let insertedRow: TableRowNode | null = null; if (insertAfter) { const focusEndRow = focusStartRow + focusCell.__rowSpan - 1; const focusEndRowMap = gridMap[focusEndRow]; @@ -284,6 +292,7 @@ export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void { 'focusEndRow is not a TableRowNode', ); focusEndRowNode.insertAfter(newRow); + insertedRow = newRow; } else { const focusStartRowMap = gridMap[focusStartRow]; const newRow = $createTableRowNode(); @@ -311,7 +320,9 @@ export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void { 'focusEndRow is not a TableRowNode', ); focusStartRowNode.insertBefore(newRow); + insertedRow = newRow; } + return insertedRow; } export function $insertTableColumn( @@ -373,7 +384,14 @@ export function $insertTableColumn( return tableNode; } -export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void { +/** + * Inserts a column before or after the current focus cell node, + * taking into account any spans. If successful, returns the + * first inserted cell node. + */ +export function $insertTableColumn__EXPERIMENTAL( + insertAfter = true, +): TableCellNode | null { const selection = $getSelection(); invariant( $isRangeSelection(selection) || $isTableSelection(selection), @@ -479,6 +497,7 @@ export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void { newColWidths.splice(columnIndex, 0, newWidth); grid.setColWidths(newColWidths); } + return firstInsertedCell; } export function $deleteTableColumn( From b2d57a0162671ab339a3b6bb578218d916ff2131 Mon Sep 17 00:00:00 2001 From: EJ Hammond <67350250+ejhammond@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:40:36 -0400 Subject: [PATCH 057/133] [lexical-react] Feature: Add aria-errormessage and aria-invalid support to LexicalContentEditable (#6745) --- .../lexical-react/flow/LexicalContentEditable.js.flow | 3 +++ .../src/shared/LexicalContentEditableElement.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index c5a7feb55fc..270ea7998ca 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -24,6 +24,9 @@ type HTMLDivElementDOMProps = $ReadOnly<{ 'aria-labeledby'?: void | string, 'aria-activedescendant'?: void | string, 'aria-autocomplete'?: void | string, + 'aria-describedby'?: void | string, + 'aria-errormessage'?: void | string, + 'aria-invalid'?: void | boolean, 'aria-owns'?: void | string, 'title'?: void | string, onClick?: void | (e: SyntheticEvent) => mixed, diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 2e1208e0d64..373bbceba73 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -20,7 +20,9 @@ export type Props = { ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; ariaControls?: React.AriaAttributes['aria-controls']; ariaDescribedBy?: React.AriaAttributes['aria-describedby']; + ariaErrorMessage?: React.AriaAttributes['aria-errormessage']; ariaExpanded?: React.AriaAttributes['aria-expanded']; + ariaInvalid?: React.AriaAttributes['aria-invalid']; ariaLabel?: React.AriaAttributes['aria-label']; ariaLabelledBy?: React.AriaAttributes['aria-labelledby']; ariaMultiline?: React.AriaAttributes['aria-multiline']; @@ -37,7 +39,9 @@ function ContentEditableElementImpl( ariaAutoComplete, ariaControls, ariaDescribedBy, + ariaErrorMessage, ariaExpanded, + ariaInvalid, ariaLabel, ariaLabelledBy, ariaMultiline, @@ -89,9 +93,15 @@ function ContentEditableElementImpl( aria-autocomplete={isEditable ? ariaAutoComplete : 'none'} aria-controls={isEditable ? ariaControls : undefined} aria-describedby={ariaDescribedBy} + // for compat, only override aria-errormessage if ariaErrorMessage is defined + {...(ariaErrorMessage != null + ? {'aria-errormessage': ariaErrorMessage} + : {})} aria-expanded={ isEditable && role === 'combobox' ? !!ariaExpanded : undefined } + // for compat, only override aria-invalid if ariaInvalid is defined + {...(ariaInvalid != null ? {'aria-invalid': ariaInvalid} : {})} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} aria-multiline={ariaMultiline} From 1b4f1e0b820b1d3257b9161f69bec716a6bdb179 Mon Sep 17 00:00:00 2001 From: EJ Hammond <67350250+ejhammond@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:27:55 -0400 Subject: [PATCH 058/133] Add ariaErrorMessage and ariaInvalid to Flow type (#6751) --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 270ea7998ca..3c50a112513 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -63,7 +63,9 @@ export type Props = $ReadOnly<{ ariaAutoComplete?: string, ariaControls?: string, ariaDescribedBy?: string, + ariaErrorMessage?: string, ariaExpanded?: boolean, + ariaInvalid?: boolean, ariaLabel?: string, ariaLabelledBy?: string, ariaMultiline?: boolean, From 6e299aa37e2baa3040ddc21fdf71cf55f7357f90 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 21 Oct 2024 22:34:03 +0100 Subject: [PATCH 059/133] [lexical-table] [lexical-selection] Try to fix calling split on undefined (#6746) --- packages/lexical-selection/src/utils.ts | 3 +++ packages/lexical-table/src/LexicalTableCellNode.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/lexical-selection/src/utils.ts b/packages/lexical-selection/src/utils.ts index 0608706eab7..df85bc72c24 100644 --- a/packages/lexical-selection/src/utils.ts +++ b/packages/lexical-selection/src/utils.ts @@ -176,6 +176,9 @@ export function createRectsFromDOMRange( */ export function getStyleObjectFromRawCSS(css: string): Record { const styleObject: Record = {}; + if (!css) { + return styleObject; + } const styles = css.split(';'); for (const style of styles) { diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index 7f752777dc0..525a8bce82c 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -322,7 +322,7 @@ export function $convertTableCellNodeElement( } const style = domNode_.style; - const textDecoration = style.textDecoration.split(' '); + const textDecoration = ((style && style.textDecoration) || '').split(' '); const hasBoldFontWeight = style.fontWeight === '700' || style.fontWeight === 'bold'; const hasLinethroughTextDecoration = textDecoration.includes('line-through'); From 3055e54acd7088b2e413a97d7b0c99c4456b852c Mon Sep 17 00:00:00 2001 From: Neysan Foo Date: Tue, 22 Oct 2024 08:36:33 +0800 Subject: [PATCH 060/133] [lexical-playground] Bug Fix: Disable table hover actions in read-only mode (#6706) --- .../plugins/TableHoverActionsPlugin/index.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx index 4c88e8d52b3..44fc3368a02 100644 --- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -7,6 +7,7 @@ */ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import { $getTableColumnIndexFromTableCellNode, $getTableRowIndexFromTableCellNode, @@ -32,8 +33,9 @@ function TableHoverActionsContainer({ anchorElem, }: { anchorElem: HTMLElement; -}): JSX.Element { +}): JSX.Element | null { const [editor] = useLexicalComposerContext(); + const isEditable = useLexicalEditable(); const [isShownRow, setShownRow] = useState(false); const [isShownColumn, setShownColumn] = useState(false); const [shouldListenMouseMove, setShouldListenMouseMove] = @@ -215,6 +217,10 @@ function TableHoverActionsContainer({ }); }; + if (!isEditable) { + return null; + } + return ( <> {isShownRow && ( @@ -268,8 +274,12 @@ export default function TableHoverActionsPlugin({ }: { anchorElem?: HTMLElement; }): React.ReactPortal | null { - return createPortal( - , - anchorElem, - ); + const isEditable = useLexicalEditable(); + + return isEditable + ? createPortal( + , + anchorElem, + ) + : null; } From 4e1a3f43955bd103686731a89fa3dd3a1494684b Mon Sep 17 00:00:00 2001 From: Neysan Foo Date: Tue, 22 Oct 2024 08:51:19 +0800 Subject: [PATCH 061/133] [lexical-playground] Bug Fix: Disable editing of Excalidraw Component in Read-Only Mode (#6704) Co-authored-by: Bob Ippolito --- .../ExcalidrawNode/ExcalidrawComponent.tsx | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx index 646d0004a38..5a45068469e 100644 --- a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +++ b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx @@ -11,6 +11,7 @@ import type {NodeKey} from 'lexical'; import {AppState, BinaryFiles} from '@excalidraw/excalidraw/types/types'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; import {mergeRegister} from '@lexical/utils'; import { @@ -40,6 +41,7 @@ export default function ExcalidrawComponent({ height: 'inherit' | number; }): JSX.Element { const [editor] = useLexicalComposerContext(); + const isEditable = useLexicalEditable(); const [isModalOpen, setModalOpen] = useState( data === '[]' && editor.isEditable(), ); @@ -66,16 +68,13 @@ export default function ExcalidrawComponent({ [editor, isSelected, nodeKey], ); - // Set editor to readOnly if Excalidraw is open to prevent unwanted changes useEffect(() => { - if (isModalOpen) { - editor.setEditable(false); - } else { - editor.setEditable(true); + if (!isEditable) { + if (isSelected) { + clearSelection(); + } + return; } - }, [isModalOpen, editor]); - - useEffect(() => { return mergeRegister( editor.registerCommand( CLICK_COMMAND, @@ -113,7 +112,15 @@ export default function ExcalidrawComponent({ COMMAND_PRIORITY_LOW, ), ); - }, [clearSelection, editor, isSelected, isResizing, $onDelete, setSelected]); + }, [ + clearSelection, + editor, + isSelected, + isResizing, + $onDelete, + setSelected, + isEditable, + ]); const deleteNode = useCallback(() => { setModalOpen(false); @@ -130,9 +137,6 @@ export default function ExcalidrawComponent({ aps: Partial, fls: BinaryFiles, ) => { - if (!editor.isEditable()) { - return; - } return editor.update(() => { const node = $getNodeByKey(nodeKey); if ($isExcalidrawNode(node)) { @@ -198,20 +202,21 @@ export default function ExcalidrawComponent({ return ( <> - { - editor.setEditable(true); - setData(els, aps, fls); - setModalOpen(false); - }} - closeOnClickOutside={false} - /> + {isEditable && isModalOpen && ( + { + setData(els, aps, fls); + setModalOpen(false); + }} + closeOnClickOutside={false} + /> + )} {elements.length > 0 && ( = MAX_ALLOWED_FONT_SIZE) } - onClick={() => handleButtonClick(updateFontSizeType.increment)} - className="toolbar-item font-increment"> + onClick={() => + updateFontSize(editor, UpdateFontSizeType.increment, inputValue) + } + className="toolbar-item font-increment" + aria-label="Increase font size" + title={`Increase font size (${SHORTCUTS.INCREASE_FONT_SIZE})`}> diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index 7ce9abbdbcc..ed5da202bca 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -7,63 +7,42 @@ */ import { - $createCodeNode, $isCodeNode, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, getLanguageFriendlyName, } from '@lexical/code'; import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; -import { - $isListNode, - INSERT_CHECK_LIST_COMMAND, - INSERT_ORDERED_LIST_COMMAND, - INSERT_UNORDERED_LIST_COMMAND, - ListNode, -} from '@lexical/list'; +import {$isListNode, ListNode} from '@lexical/list'; import {INSERT_EMBED_COMMAND} from '@lexical/react/LexicalAutoEmbedPlugin'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {$isDecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode'; import {INSERT_HORIZONTAL_RULE_COMMAND} from '@lexical/react/LexicalHorizontalRuleNode'; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, - $isQuoteNode, - HeadingTagType, -} from '@lexical/rich-text'; +import {$isHeadingNode} from '@lexical/rich-text'; import { $getSelectionStyleValueForProperty, $isParentElementRTL, $patchStyleText, - $setBlocksType, } from '@lexical/selection'; import {$isTableNode, $isTableSelection} from '@lexical/table'; import { $findMatchingParent, - $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $isEditorIsNestedEditor, mergeRegister, } from '@lexical/utils'; import { - $createParagraphNode, $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isRootOrShadowRoot, - $isTextNode, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_CRITICAL, - COMMAND_PRIORITY_NORMAL, ElementFormatType, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, INDENT_CONTENT_COMMAND, - KEY_MODIFIER_COMMAND, LexicalEditor, NodeKey, OUTDENT_CONTENT_COMMAND, @@ -75,6 +54,10 @@ import {Dispatch, useCallback, useEffect, useState} from 'react'; import * as React from 'react'; import {IS_APPLE} from 'shared/environment'; +import { + blockTypeToBlockName, + useToolbarState, +} from '../../context/ToolbarContext'; import useModal from '../../hooks/useModal'; import catTypingGif from '../../images/cat-typing.gif'; import {$createStickyNode} from '../../nodes/StickyNode'; @@ -95,23 +78,19 @@ import {InsertInlineImageDialog} from '../InlineImagePlugin'; import InsertLayoutDialog from '../LayoutPlugin/InsertLayoutDialog'; import {INSERT_PAGE_BREAK} from '../PageBreakPlugin'; import {InsertPollDialog} from '../PollPlugin'; +import {SHORTCUTS} from '../ShortcutsPlugin/shortcuts'; import {InsertTableDialog} from '../TablePlugin'; import FontSize from './fontSize'; - -const blockTypeToBlockName = { - bullet: 'Bulleted List', - check: 'Check List', - code: 'Code Block', - h1: 'Heading 1', - h2: 'Heading 2', - h3: 'Heading 3', - h4: 'Heading 4', - h5: 'Heading 5', - h6: 'Heading 6', - number: 'Numbered List', - paragraph: 'Normal', - quote: 'Quote', -}; +import { + clearFormatting, + formatBulletList, + formatCheckList, + formatCode, + formatHeading, + formatNumberedList, + formatParagraph, + formatQuote, +} from './utils'; const rootTypeToRootName = { root: 'Root', @@ -213,79 +192,6 @@ function BlockFormatDropDown({ editor: LexicalEditor; disabled?: boolean; }): JSX.Element { - const formatParagraph = () => { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createParagraphNode()); - } - }); - }; - - const formatHeading = (headingSize: HeadingTagType) => { - if (blockType !== headingSize) { - editor.update(() => { - const selection = $getSelection(); - $setBlocksType(selection, () => $createHeadingNode(headingSize)); - }); - } - }; - - const formatBulletList = () => { - if (blockType !== 'bullet') { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatCheckList = () => { - if (blockType !== 'check') { - editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatNumberedList = () => { - if (blockType !== 'number') { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatQuote = () => { - if (blockType !== 'quote') { - editor.update(() => { - const selection = $getSelection(); - $setBlocksType(selection, () => $createQuoteNode()); - }); - } - }; - - const formatCode = () => { - if (blockType !== 'code') { - editor.update(() => { - let selection = $getSelection(); - - if (selection !== null) { - if (selection.isCollapsed()) { - $setBlocksType(selection, () => $createCodeNode()); - } else { - const textContent = selection.getTextContent(); - const codeNode = $createCodeNode(); - selection.insertNodes([codeNode]); - selection = $getSelection(); - if ($isRangeSelection(selection)) { - selection.insertRawText(textContent); - } - } - } - }); - } - }; - return ( - - Normal + className={ + 'item wide ' + dropDownActiveClass(blockType === 'paragraph') + } + onClick={() => formatParagraph(editor)}> +
+ + Normal +
+ {SHORTCUTS.NORMAL}
formatHeading('h1')}> - - Heading 1 + className={'item wide ' + dropDownActiveClass(blockType === 'h1')} + onClick={() => formatHeading(editor, blockType, 'h1')}> +
+ + Heading 1 +
+ {SHORTCUTS.HEADING1}
formatHeading('h2')}> - - Heading 2 + className={'item wide ' + dropDownActiveClass(blockType === 'h2')} + onClick={() => formatHeading(editor, blockType, 'h2')}> +
+ + Heading 2 +
+ {SHORTCUTS.HEADING2}
formatHeading('h3')}> - - Heading 3 + className={'item wide ' + dropDownActiveClass(blockType === 'h3')} + onClick={() => formatHeading(editor, blockType, 'h3')}> +
+ + Heading 3 +
+ {SHORTCUTS.HEADING3}
- - Bullet List + className={'item wide ' + dropDownActiveClass(blockType === 'bullet')} + onClick={() => formatBulletList(editor, blockType)}> +
+ + Bullet List +
+ {SHORTCUTS.BULLET_LIST}
- - Numbered List + className={'item wide ' + dropDownActiveClass(blockType === 'number')} + onClick={() => formatNumberedList(editor, blockType)}> +
+ + Numbered List +
+ {SHORTCUTS.NUMBERED_LIST}
- - Check List + className={'item wide ' + dropDownActiveClass(blockType === 'check')} + onClick={() => formatCheckList(editor, blockType)}> +
+ + Check List +
+ {SHORTCUTS.CHECK_LIST}
- - Quote + className={'item wide ' + dropDownActiveClass(blockType === 'quote')} + onClick={() => formatQuote(editor, blockType)}> +
+ + Quote +
+ {SHORTCUTS.QUOTE}
- - Code Block + className={'item wide ' + dropDownActiveClass(blockType === 'code')} + onClick={() => formatCode(editor, blockType)}> +
+ + Code Block +
+ {SHORTCUTS.CODE_BLOCK}
); @@ -436,39 +371,51 @@ function ElementFormatDropdown({ onClick={() => { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left'); }} - className="item"> - - Left Align + className="item wide"> +
+ + Left Align +
+ {SHORTCUTS.LEFT_ALIGN} { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center'); }} - className="item"> - - Center Align + className="item wide"> +
+ + Center Align +
+ {SHORTCUTS.CENTER_ALIGN}
{ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right'); }} - className="item"> - - Right Align + className="item wide"> +
+ + Right Align +
+ {SHORTCUTS.RIGHT_ALIGN}
{ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify'); }} - className="item"> - - Justify Align + className="item wide"> +
+ + Justify Align +
+ {SHORTCUTS.JUSTIFY_ALIGN}
{ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'start'); }} - className="item"> + className="item wide"> { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'end'); }} - className="item"> + className="item wide"> { editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined); }} - className="item"> - - Outdent + className="item wide"> +
+ + Outdent +
+ {SHORTCUTS.OUTDENT}
{ editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); }} - className="item"> - - Indent + className="item wide"> +
+ + Indent +
+ {SHORTCUTS.INDENT}
); } export default function ToolbarPlugin({ + editor, + activeEditor, + setActiveEditor, setIsLinkEditMode, }: { + editor: LexicalEditor; + activeEditor: LexicalEditor; + setActiveEditor: Dispatch; setIsLinkEditMode: Dispatch; }): JSX.Element { - const [editor] = useLexicalComposerContext(); - const [activeEditor, setActiveEditor] = useState(editor); - const [blockType, setBlockType] = - useState('paragraph'); - const [rootType, setRootType] = - useState('root'); const [selectedElementKey, setSelectedElementKey] = useState( null, ); - const [fontSize, setFontSize] = useState('15px'); - const [fontColor, setFontColor] = useState('#000'); - const [bgColor, setBgColor] = useState('#fff'); - const [fontFamily, setFontFamily] = useState('Arial'); - const [elementFormat, setElementFormat] = useState('left'); - const [isLink, setIsLink] = useState(false); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [isStrikethrough, setIsStrikethrough] = useState(false); - const [isSubscript, setIsSubscript] = useState(false); - const [isSuperscript, setIsSuperscript] = useState(false); - const [isCode, setIsCode] = useState(false); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); const [modal, showModal] = useModal(); - const [isRTL, setIsRTL] = useState(false); - const [codeLanguage, setCodeLanguage] = useState(''); const [isEditable, setIsEditable] = useState(() => editor.isEditable()); - const [isImageCaption, setIsImageCaption] = useState(false); + const {toolbarState, updateToolbarState} = useToolbarState(); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { if (activeEditor !== editor && $isEditorIsNestedEditor(activeEditor)) { const rootElement = activeEditor.getRootElement(); - setIsImageCaption( + updateToolbarState( + 'isImageCaption', !!rootElement?.parentElement?.classList.contains( 'image-caption-container', ), ); } else { - setIsImageCaption(false); + updateToolbarState('isImageCaption', false); } const anchorNode = selection.anchor.getNode(); @@ -578,22 +515,19 @@ export default function ToolbarPlugin({ const elementKey = element.getKey(); const elementDOM = activeEditor.getElementByKey(elementKey); - setIsRTL($isParentElementRTL(selection)); + updateToolbarState('isRTL', $isParentElementRTL(selection)); // Update links const node = getSelectedNode(selection); const parent = node.getParent(); - if ($isLinkNode(parent) || $isLinkNode(node)) { - setIsLink(true); - } else { - setIsLink(false); - } + const isLink = $isLinkNode(parent) || $isLinkNode(node); + updateToolbarState('isLink', isLink); const tableNode = $findMatchingParent(node, $isTableNode); if ($isTableNode(tableNode)) { - setRootType('table'); + updateToolbarState('rootType', 'table'); } else { - setRootType('root'); + updateToolbarState('rootType', 'root'); } if (elementDOM !== null) { @@ -606,18 +540,23 @@ export default function ToolbarPlugin({ const type = parentList ? parentList.getListType() : element.getListType(); - setBlockType(type); + + updateToolbarState('blockType', type); } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (type in blockTypeToBlockName) { - setBlockType(type as keyof typeof blockTypeToBlockName); + updateToolbarState( + 'blockType', + type as keyof typeof blockTypeToBlockName, + ); } if ($isCodeNode(element)) { const language = element.getLanguage() as keyof typeof CODE_LANGUAGE_MAP; - setCodeLanguage( + updateToolbarState( + 'codeLanguage', language ? CODE_LANGUAGE_MAP[language] || language : '', ); return; @@ -625,17 +564,20 @@ export default function ToolbarPlugin({ } } // Handle buttons - setFontColor( + updateToolbarState( + 'fontColor', $getSelectionStyleValueForProperty(selection, 'color', '#000'), ); - setBgColor( + updateToolbarState( + 'bgColor', $getSelectionStyleValueForProperty( selection, 'background-color', '#fff', ), ); - setFontFamily( + updateToolbarState( + 'fontFamily', $getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'), ); let matchingParent; @@ -648,7 +590,8 @@ export default function ToolbarPlugin({ } // If matchingParent is a valid node, pass it's format type - setElementFormat( + updateToolbarState( + 'elementFormat', $isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) @@ -658,19 +601,22 @@ export default function ToolbarPlugin({ } if ($isRangeSelection(selection) || $isTableSelection(selection)) { // Update text format - setIsBold(selection.hasFormat('bold')); - setIsItalic(selection.hasFormat('italic')); - setIsUnderline(selection.hasFormat('underline')); - setIsStrikethrough(selection.hasFormat('strikethrough')); - setIsSubscript(selection.hasFormat('subscript')); - setIsSuperscript(selection.hasFormat('superscript')); - setIsCode(selection.hasFormat('code')); - - setFontSize( + updateToolbarState('isBold', selection.hasFormat('bold')); + updateToolbarState('isItalic', selection.hasFormat('italic')); + updateToolbarState('isUnderline', selection.hasFormat('underline')); + updateToolbarState( + 'isStrikethrough', + selection.hasFormat('strikethrough'), + ); + updateToolbarState('isSubscript', selection.hasFormat('subscript')); + updateToolbarState('isSuperscript', selection.hasFormat('superscript')); + updateToolbarState('isCode', selection.hasFormat('code')); + updateToolbarState( + 'fontSize', $getSelectionStyleValueForProperty(selection, 'font-size', '15px'), ); } - }, [activeEditor, editor]); + }, [activeEditor, editor, updateToolbarState]); useEffect(() => { return editor.registerCommand( @@ -682,7 +628,7 @@ export default function ToolbarPlugin({ }, COMMAND_PRIORITY_CRITICAL, ); - }, [editor, $updateToolbar]); + }, [editor, $updateToolbar, setActiveEditor]); useEffect(() => { activeEditor.getEditorState().read(() => { @@ -703,7 +649,7 @@ export default function ToolbarPlugin({ activeEditor.registerCommand( CAN_UNDO_COMMAND, (payload) => { - setCanUndo(payload); + updateToolbarState('canUndo', payload); return false; }, COMMAND_PRIORITY_CRITICAL, @@ -711,38 +657,13 @@ export default function ToolbarPlugin({ activeEditor.registerCommand( CAN_REDO_COMMAND, (payload) => { - setCanRedo(payload); + updateToolbarState('canRedo', payload); return false; }, COMMAND_PRIORITY_CRITICAL, ), ); - }, [$updateToolbar, activeEditor, editor]); - - useEffect(() => { - return activeEditor.registerCommand( - KEY_MODIFIER_COMMAND, - (payload) => { - const event: KeyboardEvent = payload; - const {code, ctrlKey, metaKey} = event; - - if (code === 'KeyK' && (ctrlKey || metaKey)) { - event.preventDefault(); - let url: string | null; - if (!isLink) { - setIsLinkEditMode(true); - url = sanitizeUrl('https://'); - } else { - setIsLinkEditMode(false); - url = null; - } - return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, url); - } - return false; - }, - COMMAND_PRIORITY_NORMAL, - ); - }, [activeEditor, isLink, setIsLinkEditMode]); + }, [$updateToolbar, activeEditor, editor, updateToolbarState]); const applyStyleText = useCallback( (styles: Record, skipHistoryStack?: boolean) => { @@ -759,62 +680,6 @@ export default function ToolbarPlugin({ [activeEditor], ); - const clearFormatting = useCallback(() => { - activeEditor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection) || $isTableSelection(selection)) { - const anchor = selection.anchor; - const focus = selection.focus; - const nodes = selection.getNodes(); - const extractedNodes = selection.extract(); - - if (anchor.key === focus.key && anchor.offset === focus.offset) { - return; - } - - nodes.forEach((node, idx) => { - // We split the first and last node by the selection - // So that we don't format unselected text inside those nodes - if ($isTextNode(node)) { - // Use a separate variable to ensure TS does not lose the refinement - let textNode = node; - if (idx === 0 && anchor.offset !== 0) { - textNode = textNode.splitText(anchor.offset)[1] || textNode; - } - if (idx === nodes.length - 1) { - textNode = textNode.splitText(focus.offset)[0] || textNode; - } - /** - * If the selected text has one format applied - * selecting a portion of the text, could - * clear the format to the wrong portion of the text. - * - * The cleared text is based on the length of the selected text. - */ - // We need this in case the selected text only has one format - const extractedTextNode = extractedNodes[0]; - if (nodes.length === 1 && $isTextNode(extractedTextNode)) { - textNode = extractedTextNode; - } - - if (textNode.__style !== '') { - textNode.setStyle(''); - } - if (textNode.__format !== 0) { - textNode.setFormat(0); - $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); - } - node = textNode; - } else if ($isHeadingNode(node) || $isQuoteNode(node)) { - node.replace($createParagraphNode(), true); - } else if ($isDecoratorBlockNode(node)) { - node.setFormat(''); - } - }); - } - }); - }, [activeEditor]); - const onFontColorSelect = useCallback( (value: string, skipHistoryStack: boolean) => { applyStyleText({color: value}, skipHistoryStack); @@ -830,7 +695,7 @@ export default function ToolbarPlugin({ ); const insertLink = useCallback(() => { - if (!isLink) { + if (!toolbarState.isLink) { setIsLinkEditMode(true); activeEditor.dispatchCommand( TOGGLE_LINK_COMMAND, @@ -840,7 +705,7 @@ export default function ToolbarPlugin({ setIsLinkEditMode(false); activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } - }, [activeEditor, isLink, setIsLinkEditMode]); + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); const onCodeLanguageSelect = useCallback( (value: string) => { @@ -859,13 +724,13 @@ export default function ToolbarPlugin({ activeEditor.dispatchCommand(INSERT_IMAGE_COMMAND, payload); }; - const canViewerSeeInsertDropdown = !isImageCaption; - const canViewerSeeInsertCodeButton = !isImageCaption; + const canViewerSeeInsertDropdown = !toolbarState.isImageCaption; + const canViewerSeeInsertCodeButton = !toolbarState.isImageCaption; return (
- {blockType in blockTypeToBlockName && activeEditor === editor && ( - <> - - - - )} - {blockType === 'code' ? ( + {toolbarState.blockType in blockTypeToBlockName && + activeEditor === editor && ( + <> + + + + )} + {toolbarState.blockType === 'code' ? ( {CODE_LANGUAGE_OPTIONS.map(([value, name]) => { return ( onCodeLanguageSelect(value)} key={value}> @@ -922,12 +788,12 @@ export default function ToolbarPlugin({ @@ -937,12 +803,12 @@ export default function ToolbarPlugin({ onClick={() => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); }} - className={'toolbar-item spaced ' + (isBold ? 'active' : '')} - title={IS_APPLE ? 'Bold (⌘B)' : 'Bold (Ctrl+B)'} + className={ + 'toolbar-item spaced ' + (toolbarState.isBold ? 'active' : '') + } + title={`Bold (${SHORTCUTS.BOLD})`} type="button" - aria-label={`Format text as bold. Shortcut: ${ - IS_APPLE ? '⌘B' : 'Ctrl+B' - }`}> + aria-label={`Format text as bold. Shortcut: ${SHORTCUTS.BOLD}`}> {canViewerSeeInsertCodeButton && ( @@ -977,8 +844,10 @@ export default function ToolbarPlugin({ onClick={() => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code'); }} - className={'toolbar-item spaced ' + (isCode ? 'active' : '')} - title="Insert code block" + className={ + 'toolbar-item spaced ' + (toolbarState.isCode ? 'active' : '') + } + title={`Insert code block (${SHORTCUTS.INSERT_CODE_BLOCK})`} type="button" aria-label="Insert code block"> @@ -987,9 +856,11 @@ export default function ToolbarPlugin({ @@ -998,7 +869,7 @@ export default function ToolbarPlugin({ buttonClassName="toolbar-item color-picker" buttonAriaLabel="Formatting text color" buttonIconClassName="icon font-color" - color={fontColor} + color={toolbarState.fontColor} onChange={onFontColorSelect} title="text color" /> @@ -1007,7 +878,7 @@ export default function ToolbarPlugin({ buttonClassName="toolbar-item color-picker" buttonAriaLabel="Formatting background color" buttonIconClassName="icon bg-color" - color={bgColor} + color={toolbarState.bgColor} onChange={onBgColorSelect} title="bg color" /> @@ -1024,21 +895,31 @@ export default function ToolbarPlugin({ 'strikethrough', ); }} - className={'item ' + dropDownActiveClass(isStrikethrough)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isStrikethrough) + } title="Strikethrough" aria-label="Format text with a strikethrough"> - - Strikethrough +
+ + Strikethrough +
+ {SHORTCUTS.STRIKETHROUGH}
{ activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); }} - className={'item ' + dropDownActiveClass(isSubscript)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isSubscript) + } title="Subscript" aria-label="Format text with a subscript"> - - Subscript +
+ + Subscript +
+ {SHORTCUTS.SUBSCRIPT}
{ @@ -1047,19 +928,27 @@ export default function ToolbarPlugin({ 'superscript', ); }} - className={'item ' + dropDownActiveClass(isSuperscript)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isSuperscript) + } title="Superscript" aria-label="Format text with a superscript"> - - Superscript +
+ + Superscript +
+ {SHORTCUTS.SUPERSCRIPT}
clearFormatting(activeEditor)} + className="item wide" title="Clear text formatting" aria-label="Clear all text formatting"> - - Clear Formatting +
+ + Clear Formatting +
+ {SHORTCUTS.CLEAR_FORMATTING}
{canViewerSeeInsertDropdown && ( @@ -1236,9 +1125,9 @@ export default function ToolbarPlugin({ {modal} diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts new file mode 100644 index 00000000000..8d0ca04d5e3 --- /dev/null +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts @@ -0,0 +1,292 @@ +/** + * 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 {$createCodeNode} from '@lexical/code'; +import { + INSERT_CHECK_LIST_COMMAND, + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, +} from '@lexical/list'; +import {$isDecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode'; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + $isQuoteNode, + HeadingTagType, +} from '@lexical/rich-text'; +import {$patchStyleText, $setBlocksType} from '@lexical/selection'; +import {$isTableSelection} from '@lexical/table'; +import {$getNearestBlockElementAncestorOrThrow} from '@lexical/utils'; +import { + $createParagraphNode, + $getSelection, + $isRangeSelection, + $isTextNode, + LexicalEditor, +} from 'lexical'; + +import { + DEFAULT_FONT_SIZE, + MAX_ALLOWED_FONT_SIZE, + MIN_ALLOWED_FONT_SIZE, +} from '../../context/ToolbarContext'; + +// eslint-disable-next-line no-shadow +export enum UpdateFontSizeType { + increment = 1, + decrement, +} + +/** + * Calculates the new font size based on the update type. + * @param currentFontSize - The current font size + * @param updateType - The type of change, either increment or decrement + * @returns the next font size + */ +export const calculateNextFontSize = ( + currentFontSize: number, + updateType: UpdateFontSizeType | null, +) => { + if (!updateType) { + return currentFontSize; + } + + let updatedFontSize: number = currentFontSize; + switch (updateType) { + case UpdateFontSizeType.decrement: + switch (true) { + case currentFontSize > MAX_ALLOWED_FONT_SIZE: + updatedFontSize = MAX_ALLOWED_FONT_SIZE; + break; + case currentFontSize >= 48: + updatedFontSize -= 12; + break; + case currentFontSize >= 24: + updatedFontSize -= 4; + break; + case currentFontSize >= 14: + updatedFontSize -= 2; + break; + case currentFontSize >= 9: + updatedFontSize -= 1; + break; + default: + updatedFontSize = MIN_ALLOWED_FONT_SIZE; + break; + } + break; + + case UpdateFontSizeType.increment: + switch (true) { + case currentFontSize < MIN_ALLOWED_FONT_SIZE: + updatedFontSize = MIN_ALLOWED_FONT_SIZE; + break; + case currentFontSize < 12: + updatedFontSize += 1; + break; + case currentFontSize < 20: + updatedFontSize += 2; + break; + case currentFontSize < 36: + updatedFontSize += 4; + break; + case currentFontSize <= 60: + updatedFontSize += 12; + break; + default: + updatedFontSize = MAX_ALLOWED_FONT_SIZE; + break; + } + break; + + default: + break; + } + return updatedFontSize; +}; + +/** + * Patches the selection with the updated font size. + */ +export const updateFontSizeInSelection = ( + editor: LexicalEditor, + newFontSize: string | null, + updateType: UpdateFontSizeType | null, +) => { + const getNextFontSize = (prevFontSize: string | null): string => { + if (!prevFontSize) { + prevFontSize = `${DEFAULT_FONT_SIZE}px`; + } + prevFontSize = prevFontSize.slice(0, -2); + const nextFontSize = calculateNextFontSize( + Number(prevFontSize), + updateType, + ); + return `${nextFontSize}px`; + }; + + editor.update(() => { + if (editor.isEditable()) { + const selection = $getSelection(); + if (selection !== null) { + $patchStyleText(selection, { + 'font-size': newFontSize || getNextFontSize, + }); + } + } + }); +}; + +export const updateFontSize = ( + editor: LexicalEditor, + updateType: UpdateFontSizeType, + inputValue: string, +) => { + if (inputValue !== '') { + const nextFontSize = calculateNextFontSize(Number(inputValue), updateType); + updateFontSizeInSelection(editor, String(nextFontSize) + 'px', null); + } else { + updateFontSizeInSelection(editor, null, updateType); + } +}; + +export const formatParagraph = (editor: LexicalEditor) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + $setBlocksType(selection, () => $createParagraphNode()); + } + }); +}; + +export const formatHeading = ( + editor: LexicalEditor, + blockType: string, + headingSize: HeadingTagType, +) => { + if (blockType !== headingSize) { + editor.update(() => { + const selection = $getSelection(); + $setBlocksType(selection, () => $createHeadingNode(headingSize)); + }); + } +}; + +export const formatBulletList = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'bullet') { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatCheckList = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'check') { + editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatNumberedList = ( + editor: LexicalEditor, + blockType: string, +) => { + if (blockType !== 'number') { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatQuote = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'quote') { + editor.update(() => { + const selection = $getSelection(); + $setBlocksType(selection, () => $createQuoteNode()); + }); + } +}; + +export const formatCode = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'code') { + editor.update(() => { + let selection = $getSelection(); + + if (selection !== null) { + if (selection.isCollapsed()) { + $setBlocksType(selection, () => $createCodeNode()); + } else { + const textContent = selection.getTextContent(); + const codeNode = $createCodeNode(); + selection.insertNodes([codeNode]); + selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertRawText(textContent); + } + } + } + }); + } +}; + +export const clearFormatting = (editor: LexicalEditor) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection) || $isTableSelection(selection)) { + const anchor = selection.anchor; + const focus = selection.focus; + const nodes = selection.getNodes(); + const extractedNodes = selection.extract(); + + if (anchor.key === focus.key && anchor.offset === focus.offset) { + return; + } + + nodes.forEach((node, idx) => { + // We split the first and last node by the selection + // So that we don't format unselected text inside those nodes + if ($isTextNode(node)) { + // Use a separate variable to ensure TS does not lose the refinement + let textNode = node; + if (idx === 0 && anchor.offset !== 0) { + textNode = textNode.splitText(anchor.offset)[1] || textNode; + } + if (idx === nodes.length - 1) { + textNode = textNode.splitText(focus.offset)[0] || textNode; + } + /** + * If the selected text has one format applied + * selecting a portion of the text, could + * clear the format to the wrong portion of the text. + * + * The cleared text is based on the length of the selected text. + */ + // We need this in case the selected text only has one format + const extractedTextNode = extractedNodes[0]; + if (nodes.length === 1 && $isTextNode(extractedTextNode)) { + textNode = extractedTextNode; + } + + if (textNode.__style !== '') { + textNode.setStyle(''); + } + if (textNode.__format !== 0) { + textNode.setFormat(0); + $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); + } + node = textNode; + } else if ($isHeadingNode(node) || $isQuoteNode(node)) { + node.replace($createParagraphNode(), true); + } else if ($isDecoratorBlockNode(node)) { + node.setFormat(''); + } + }); + } + }); +}; From a4e70168a13271dfdaa8e2684f5fc99658f3236c Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Wed, 6 Nov 2024 02:01:45 +0000 Subject: [PATCH 079/133] Fix importDOM for Layout plugin (#6799) --- .../src/nodes/LayoutItemNode.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lexical-playground/src/nodes/LayoutItemNode.ts b/packages/lexical-playground/src/nodes/LayoutItemNode.ts index d579b4ad6d8..3d227976d64 100644 --- a/packages/lexical-playground/src/nodes/LayoutItemNode.ts +++ b/packages/lexical-playground/src/nodes/LayoutItemNode.ts @@ -8,6 +8,7 @@ import type { DOMConversionMap, + DOMConversionOutput, EditorConfig, LexicalNode, SerializedElementNode, @@ -18,6 +19,10 @@ import {ElementNode} from 'lexical'; export type SerializedLayoutItemNode = SerializedElementNode; +function $convertLayoutItemElement(): DOMConversionOutput | null { + return {node: $createLayoutItemNode()}; +} + export class LayoutItemNode extends ElementNode { static getType(): string { return 'layout-item'; @@ -29,6 +34,7 @@ export class LayoutItemNode extends ElementNode { createDOM(config: EditorConfig): HTMLElement { const dom = document.createElement('div'); + dom.setAttribute('data-lexical-layout-item', 'true'); if (typeof config.theme.layoutItem === 'string') { addClassNamesToElement(dom, config.theme.layoutItem); } @@ -40,7 +46,17 @@ export class LayoutItemNode extends ElementNode { } static importDOM(): DOMConversionMap | null { - return {}; + return { + div: (domNode: HTMLElement) => { + if (!domNode.hasAttribute('data-lexical-layout-item')) { + return null; + } + return { + conversion: $convertLayoutItemElement, + priority: 2, + }; + }, + }; } static importJSON(): LayoutItemNode { From 8eae296ea39ff0dd707c901493553f1d889e9174 Mon Sep 17 00:00:00 2001 From: Rajiv Anisetti Date: Wed, 6 Nov 2024 18:38:32 -0500 Subject: [PATCH 080/133] Add optional selection argument to $getHtmlContent flow type (#6803) --- packages/lexical-clipboard/flow/LexicalClipboard.js.flow | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-clipboard/flow/LexicalClipboard.js.flow b/packages/lexical-clipboard/flow/LexicalClipboard.js.flow index db534fbbe43..6ca0ee622b0 100644 --- a/packages/lexical-clipboard/flow/LexicalClipboard.js.flow +++ b/packages/lexical-clipboard/flow/LexicalClipboard.js.flow @@ -24,7 +24,11 @@ declare export function $insertDataTransferForRichText( editor: LexicalEditor, ): void; -declare export function $getHtmlContent(editor: LexicalEditor): string | null; +declare export function $getHtmlContent( + editor: LexicalEditor, + selection?: BaseSelection, +): string | null; + declare export function $getLexicalContent( editor: LexicalEditor, ): string | null; From 76f8569211780ee7e913176d99d23eddd1cd3b6c Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:54:12 +0100 Subject: [PATCH 081/133] [lexical] Chore: Add export of DOMExportOutputMap from lexical (#6805) --- packages/lexical/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 0bc18239b37..d5c94e23453 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -42,6 +42,7 @@ export type { DOMConversionMap, DOMConversionOutput, DOMExportOutput, + DOMExportOutputMap, LexicalNode, NodeKey, NodeMap, From 29611d22b4bbb5cec41c625a03d2abda738d5e65 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 7 Nov 2024 15:59:38 -0800 Subject: [PATCH 082/133] v0.20.0 (#6809) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 18 + examples/react-plain-text/package.json | 6 +- examples/react-rich-collab/package.json | 8 +- examples/react-rich/package.json | 6 +- examples/react-table/package.json | 6 +- examples/vanilla-js-plugin/package.json | 12 +- examples/vanilla-js/package.json | 12 +- package-lock.json | 446 +++++++++--------- package.json | 2 +- packages/lexical-clipboard/package.json | 12 +- packages/lexical-code/package.json | 6 +- packages/lexical-devtools-core/package.json | 14 +- packages/lexical-devtools/package.json | 6 +- packages/lexical-dragon/package.json | 4 +- packages/lexical-eslint-plugin/package.json | 2 +- packages/lexical-file/package.json | 4 +- packages/lexical-hashtag/package.json | 6 +- packages/lexical-headless/package.json | 4 +- packages/lexical-history/package.json | 6 +- packages/lexical-html/package.json | 8 +- packages/lexical-link/package.json | 6 +- packages/lexical-list/package.json | 6 +- packages/lexical-mark/package.json | 6 +- packages/lexical-markdown/package.json | 16 +- packages/lexical-offset/package.json | 4 +- packages/lexical-overflow/package.json | 4 +- packages/lexical-plain-text/package.json | 10 +- packages/lexical-playground/package.json | 32 +- packages/lexical-react/package.json | 40 +- packages/lexical-rich-text/package.json | 10 +- packages/lexical-selection/package.json | 4 +- packages/lexical-table/package.json | 8 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 8 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 8 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- 41 files changed, 405 insertions(+), 387 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1618f50e10d..e8614e6be2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## v0.20.0 (2024-11-07) + +- Add optional selection argument to getHtmlContent flow type (#6803) Rajiv Anisetti +- Fix importDOM for Layout plugin (#6799) Ivaylo Pavlov +- lexical-playground Feature Add more keyboard shortcuts (#6754) Bedru Umer +- lexical-website Documentation Update Add Documentation for html Property in Lexical Editor Configuration (#6770) Ajaezo Kingsley +- lexical-yjs Bug Fix clean up dangling text after undo in collaboration (#6670) Michael Shafer +- Lexical-website BugFix Change button text colour to improve visibility (#6796) Fadekemi Adebayo +- lexical-markdown Feature add ability to control finding the end of a node matched by TextMatchTransformer (#6681) Alessio Gravili +- lexical-react Bug Fix LexicalTypeaheadMenuPlugin SSR error ReferenceError document is not defined (#6794) Bedru Umer +- lexical-website Chore upgrade to Docusaurus v3.6 - Docusaurus Faster (#6761) Sbastien Lorber +- Bug Fix ContextMenu Paste option not preserving style (#6780) C. +- lexical-playground Fix the placement of the fontSize button in the ToolbarPlugin and hide the vertical scroll (Bug Fix) (#6786) Oluwasanya Olaoluwa +- #6768 fix to avoid infinite markdown shortcut matchers run (#6778) Maksim Horbachevsky +- lexical Feature Add onUpdate function during update with onUpdate (correct baselline) (#6773) Michael Landis +- v0.19.0 (#6774) Sherry +- v0.19.0 Lexical GitHub Actions Bot + ## v0.19.0 (2024-10-28) - lexical Add missing commands to Lexical.js.flow (#6769) Sherry diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index 09efd494a71..fde9b24aacf 100644 --- a/examples/react-plain-text/package.json +++ b/examples/react-plain-text/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-plain-text-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index a6255202a9e..da5cf1bef88 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-collab-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -12,9 +12,9 @@ "server:webrtc": "cross-env HOST=localhost PORT=1235 npx y-webrtc" }, "dependencies": { - "@lexical/react": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "y-webrtc": "^10.3.0", diff --git a/examples/react-rich/package.json b/examples/react-rich/package.json index 994710af1a1..35918ba999d 100644 --- a/examples/react-rich/package.json +++ b/examples/react-rich/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-table/package.json b/examples/react-table/package.json index 9ae92f1d043..037fbd8e494 100644 --- a/examples/react-table/package.json +++ b/examples/react-table/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-table-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vanilla-js-plugin/package.json b/examples/vanilla-js-plugin/package.json index dac39f57f09..403d0199954 100644 --- a/examples/vanilla-js-plugin/package.json +++ b/examples/vanilla-js-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-plugin-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.19.0" + "lexical": "0.20.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 69930c7e448..89982850634 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index a296b7da9a4..b147a804740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.19.0", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "workspaces": [ "packages/*" @@ -38886,28 +38886,28 @@ } }, "packages/lexical": { - "version": "0.19.0", + "version": "0.20.0", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" }, "devDependencies": { @@ -38916,7 +38916,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.19.0", + "version": "0.20.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -38933,12 +38933,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.19.0", + "lexical": "0.20.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -38946,15 +38946,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", @@ -38963,15 +38963,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -38982,136 +38982,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-playground": { - "version": "0.19.0", + "version": "0.20.0", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -39134,28 +39134,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.19.0", - "license": "MIT", - "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "version": "0.20.0", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -39165,55 +39165,55 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { "@docusaurus/core": "3.6.0", "@docusaurus/faster": "3.6.0", @@ -39243,12 +39243,12 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -39281,10 +39281,10 @@ } }, "packages/shared": { - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } }, @@ -43914,19 +43914,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.19.0", + "@lexical/utils": "0.20.0", "@types/prismjs": "^1.26.0", - "lexical": "0.19.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" } }, @@ -43938,7 +43938,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -43947,7 +43947,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.19.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -43959,18 +43959,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/eslint-plugin": { @@ -43982,152 +43982,152 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/website": { @@ -44160,9 +44160,9 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" } }, "@mdx-js/mdx": { @@ -56203,26 +56203,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@types/lodash-es": "^4.14.182", "@vitejs/plugin-react": "^4.2.1", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -62509,7 +62509,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "shebang-command": { diff --git a/package.json b/package.json index 8e1061ae939..ee24db444cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index 59da212fb4d..f515b440818 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index e436a96468e..7265a938997 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index fc8e4f3c714..9361a8c699c 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index 406b826393c..ed014eed61f 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -2,7 +2,7 @@ "name": "@lexical/devtools", "description": "Lexical DevTools browser extension", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "wxt", @@ -41,12 +41,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.19.0", + "lexical": "0.20.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index d446fa6e700..9d4c1e67857 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index d8311110bb0..01c8727bc8e 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 28e08173e4a..a19c36867f7 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 2a3b0cb6d42..e591e17fac4 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index ac5bfbc81b0..100dcbbc90c 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 8c5fea30560..75cfee062ab 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index b7310fcded0..77fbe5e4b88 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 6bbc2f9f1d0..8c2363d556e 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index c75ffb5d8bd..b190078c9fb 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 3a81b001412..51dbfa99949 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index 796ba318519..a14fd94e555 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index bbfdee16989..46954e7f762 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index 26fbd14b150..faf7c4073ae 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index ecaf6a53f53..e8045945b1e 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,7 +7,7 @@ "plain-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 32911a5d25d..244dbe2a692 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.19.0", + "version": "0.20.0", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index 24abcc61c8a..c01c40fd23d 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index 4745a432de2..972c69af447 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,7 +7,7 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 08c1002dd52..88fe2149704 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index d2164872321..8aba5552fe6 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 1075e5979d8..bf8042fea89 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 4b10fbb739c..103f06f7ed3 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index dd621c118f7..020be5e1ba7 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index d281b9c9393..497b72af328 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,13 +11,13 @@ "crdt" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index 35711998c37..385584c84a0 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 3f145586bf2..c59c3f6537c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index d8ecd2945c4..4cb6414e571 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,7 +1,7 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.19.0", + "version": "0.20.0", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/react": "0.20.0", + "@lexical/utils": "0.20.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.19.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index a524ec4e1e1..f5d0384fc65 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "lexical-esm-nextjs", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "next": "^14.2.1", "react": "^18", "react-dom": "^18" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json index 0a59b0510b6..df0007067f7 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json @@ -1,6 +1,6 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.19.0", + "lexical": "0.20.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", From fd4aca766f74ce3a4cf95230b8a95169426c0e9d Mon Sep 17 00:00:00 2001 From: Ebad Date: Fri, 8 Nov 2024 16:14:32 +0000 Subject: [PATCH 083/133] [lexical-mark] Bug Fix: Stop MarkNode __ids array deep copy in clone (#6810) Co-authored-by: Ebad Salehi --- packages/lexical-mark/src/MarkNode.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/lexical-mark/src/MarkNode.ts b/packages/lexical-mark/src/MarkNode.ts index 3a91f1c30e1..a19bd626fc7 100644 --- a/packages/lexical-mark/src/MarkNode.ts +++ b/packages/lexical-mark/src/MarkNode.ts @@ -32,14 +32,14 @@ export type SerializedMarkNode = Spread< /** @noInheritDoc */ export class MarkNode extends ElementNode { /** @internal */ - __ids: Array; + __ids: readonly string[]; static getType(): string { return 'mark'; } static clone(node: MarkNode): MarkNode { - return new MarkNode(Array.from(node.__ids), node.__key); + return new MarkNode(node.__ids, node.__key); } static importDOM(): null { @@ -57,13 +57,13 @@ export class MarkNode extends ElementNode { exportJSON(): SerializedMarkNode { return { ...super.exportJSON(), - ids: this.getIDs(), + ids: Array.from(this.getIDs()), type: 'mark', version: 1, }; } - constructor(ids: Array, key?: NodeKey) { + constructor(ids: readonly string[], key?: NodeKey) { super(key); this.__ids = ids || []; } @@ -112,13 +112,13 @@ export class MarkNode extends ElementNode { getIDs(): Array { const self = this.getLatest(); - return $isMarkNode(self) ? self.__ids : []; + return $isMarkNode(self) ? Array.from(self.__ids) : []; } addID(id: string): void { const self = this.getWritable(); if ($isMarkNode(self)) { - const ids = self.__ids; + const ids = Array.from(self.__ids); self.__ids = ids; for (let i = 0; i < ids.length; i++) { // If we already have it, don't add again @@ -133,7 +133,7 @@ export class MarkNode extends ElementNode { deleteID(id: string): void { const self = this.getWritable(); if ($isMarkNode(self)) { - const ids = self.__ids; + const ids = Array.from(self.__ids); self.__ids = ids; for (let i = 0; i < ids.length; i++) { if (id === ids[i]) { @@ -197,7 +197,7 @@ export class MarkNode extends ElementNode { } } -export function $createMarkNode(ids: Array): MarkNode { +export function $createMarkNode(ids: readonly string[]): MarkNode { return $applyNodeReplacement(new MarkNode(ids)); } From 2bdaed5e434cc0ab33d417a2a7f27dbba5b2611e Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 11 Nov 2024 15:03:47 +0530 Subject: [PATCH 084/133] [lexical-list] Bug Fix: Handle appending inline element nodes in ListNode.append (#6791) --- packages/lexical-list/src/LexicalListNode.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/lexical-list/src/LexicalListNode.ts b/packages/lexical-list/src/LexicalListNode.ts index 45a87f30d2a..2af911c7a8e 100644 --- a/packages/lexical-list/src/LexicalListNode.ts +++ b/packages/lexical-list/src/LexicalListNode.ts @@ -200,8 +200,12 @@ export class ListNode extends ElementNode { if ($isListNode(currentNode)) { listItemNode.append(currentNode); } else if ($isElementNode(currentNode)) { - const textNode = $createTextNode(currentNode.getTextContent()); - listItemNode.append(textNode); + if (currentNode.isInline()) { + listItemNode.append(currentNode); + } else { + const textNode = $createTextNode(currentNode.getTextContent()); + listItemNode.append(textNode); + } } else { listItemNode.append(currentNode); } From 23f4a7e953075320e3c2113f4feeb8b62934f208 Mon Sep 17 00:00:00 2001 From: Sevki Date: Mon, 11 Nov 2024 11:12:58 +0000 Subject: [PATCH 085/133] Mention nodes shouldn't be spellcheck'd :) (#6788) Co-authored-by: Bob Ippolito Co-authored-by: Sherry Wong Co-authored-by: Sherry --- .../__tests__/e2e/ClearFormatting.spec.mjs | 2 ++ .../__tests__/e2e/Composition.spec.mjs | 2 ++ .../__tests__/e2e/Mentions.spec.mjs | 33 +++++++++++++++++++ .../379-backspace-with-mentions.spec.mjs | 2 ++ .../src/nodes/MentionNode.ts | 1 + 5 files changed, 40 insertions(+) diff --git a/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs index 2749686abe0..c52a894569c 100644 --- a/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs @@ -159,6 +159,7 @@ test.describe('Clear All Formatting', () => {

Luke Skywalker @@ -180,6 +181,7 @@ test.describe('Clear All Formatting', () => { dir="ltr"> Luke Skywalker diff --git a/packages/lexical-playground/__tests__/e2e/Composition.spec.mjs b/packages/lexical-playground/__tests__/e2e/Composition.spec.mjs index 5685cc2b95c..6e07eaae818 100644 --- a/packages/lexical-playground/__tests__/e2e/Composition.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Composition.spec.mjs @@ -750,6 +750,7 @@ test.describe('Composition', () => { dir="ltr"> Luke Skywalker @@ -950,6 +951,7 @@ test.describe('Composition', () => { dir="ltr"> Luke Skywalker diff --git a/packages/lexical-playground/__tests__/e2e/Mentions.spec.mjs b/packages/lexical-playground/__tests__/e2e/Mentions.spec.mjs index 197f106a132..86084759c66 100644 --- a/packages/lexical-playground/__tests__/e2e/Mentions.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Mentions.spec.mjs @@ -61,6 +61,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -137,6 +138,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -168,6 +170,7 @@ test.describe('Mentions', () => {

Luke @@ -232,6 +235,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -264,6 +268,7 @@ test.describe('Mentions', () => {

Skywalker @@ -308,6 +313,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -340,6 +346,7 @@ test.describe('Mentions', () => {

Luke @@ -384,6 +391,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -407,6 +415,7 @@ test.describe('Mentions', () => {

Luke @@ -486,6 +495,7 @@ test.describe('Mentions', () => { abc Luke Skywalker @@ -511,6 +521,7 @@ test.describe('Mentions', () => { abc Luke @@ -590,6 +601,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -597,6 +609,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -604,6 +617,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -611,6 +625,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -646,6 +661,7 @@ test.describe('Mentions', () => { Skywalker Luke Skywalker @@ -653,6 +669,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -660,6 +677,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -677,6 +695,7 @@ test.describe('Mentions', () => { Skywalker Luke Skywalker @@ -684,6 +703,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -691,6 +711,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -714,6 +735,7 @@ test.describe('Mentions', () => {

Luke Skywalker @@ -721,6 +743,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -728,6 +751,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -743,6 +767,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -750,6 +775,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -757,6 +783,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -783,6 +810,7 @@ test.describe('Mentions', () => { Skywalker Luke Skywalker @@ -790,6 +818,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -807,6 +836,7 @@ test.describe('Mentions', () => { Skywalker Luke Skywalker @@ -814,6 +844,7 @@ test.describe('Mentions', () => { Luke Skywalker @@ -879,6 +910,7 @@ test.describe('Mentions', () => { dir="ltr"> Luke Skywalker @@ -971,6 +1003,7 @@ test.describe('Mentions', () => { dir="ltr"> Luke Skywalker diff --git a/packages/lexical-playground/__tests__/regression/379-backspace-with-mentions.spec.mjs b/packages/lexical-playground/__tests__/regression/379-backspace-with-mentions.spec.mjs index 01635c43d02..51185ecc75b 100644 --- a/packages/lexical-playground/__tests__/regression/379-backspace-with-mentions.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/379-backspace-with-mentions.spec.mjs @@ -35,6 +35,7 @@ test.describe('Regression test #379', () => {

Luke Skywalker @@ -57,6 +58,7 @@ test.describe('Regression test #379', () => {

Luke Skywalker diff --git a/packages/lexical-playground/src/nodes/MentionNode.ts b/packages/lexical-playground/src/nodes/MentionNode.ts index 9f0d6e1423a..b6a41754025 100644 --- a/packages/lexical-playground/src/nodes/MentionNode.ts +++ b/packages/lexical-playground/src/nodes/MentionNode.ts @@ -80,6 +80,7 @@ export class MentionNode extends TextNode { const dom = super.createDOM(config); dom.style.cssText = mentionStyle; dom.className = 'mention'; + dom.spellcheck = false; return dom; } From 3acfed5e68c2db926a1d91c08ce8b5c1ff00e66a Mon Sep 17 00:00:00 2001 From: Oluwasanya Olaoluwa Date: Tue, 12 Nov 2024 18:33:54 +0100 Subject: [PATCH 086/133] Fix: Aria attributes for ContentEditable are ignored (#6814) --- .../LexicalContentEditableElement.test.tsx | 228 ++++++++++++++++++ .../shared/LexicalContentEditableElement.tsx | 2 +- 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 packages/lexical-react/src/__tests__/unit/LexicalContentEditableElement.test.tsx diff --git a/packages/lexical-react/src/__tests__/unit/LexicalContentEditableElement.test.tsx b/packages/lexical-react/src/__tests__/unit/LexicalContentEditableElement.test.tsx new file mode 100644 index 00000000000..9d8bc2398d1 --- /dev/null +++ b/packages/lexical-react/src/__tests__/unit/LexicalContentEditableElement.test.tsx @@ -0,0 +1,228 @@ +/** + * 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 {createEditor, LexicalEditor} from 'lexical'; +import {createRoot, Root} from 'react-dom/client'; +import * as ReactTestUtils from 'shared/react-test-utils'; + +import {ContentEditableElement} from '../../shared/LexicalContentEditableElement'; + +describe('ContentEditableElement tests', () => { + let container: HTMLDivElement | null = null; + let reactRoot: Root; + let editor: LexicalEditor; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + reactRoot = createRoot(container); + + editor = createEditor({ + namespace: 'ContentEditableElement', + onError: (error) => { + throw error; + }, + }); + }); + + afterEach(async () => { + if (container) { + await ReactTestUtils.act(async () => { + reactRoot.unmount(); + }); + document.body.removeChild(container); + } + editor.setRootElement(null); //editor cleanup + }); + + it('renders the correct ARIA attributes when editable', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('aria-labelledby')).toBe('test-label'); + expect(element.getAttribute('contenteditable')).toBe('true'); + }); + + it('renders aria-labelledby attribute correctly', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('.ContentEditable__root')!; + expect(element.getAttribute('aria-labelledby')).toBe('TEST'); + }); + + it('renders the correct ARIA attributes for different roles', async () => { + const roles = ['textbox', 'combobox', 'listbox', 'spinbutton']; + + for (const role of roles) { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector(`[role="${role}"]`)!; + expect(element.getAttribute('role')).toBe(role); + } + }); + it('renders optional ARIA attributes when provided', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('aria-describedby')).toBe('test-description'); // Check aria-describedby + }); + + it('renders aria-expanded correctly for role combobox', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="combobox"]')!; + + expect(element.getAttribute('aria-expanded')).toBe('true'); // Verify that aria-expanded is correctly set. + }); + + it('renders aria-invalid and aria-required correctly', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('aria-invalid')).toBe('true'); // Verify aria-invalid + expect(element.getAttribute('aria-required')).toBe('true'); // Verify aria-required + }); + + it('applies custom attributes and styles correctly', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]') as HTMLElement; + expect(element.getAttribute('data-testid')).toBe('test-element'); // Verify custom data attribute + expect(element.style.color).toBe('red'); // Verify inline styles + expect(element.style.fontSize).toBe('16px'); // Verify inline styles + }); + + it('renders aria-invalid and aria-required correctly when set to false', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('aria-invalid')).toBe('false'); // Verify aria-invalid + expect(element.getAttribute('aria-required')).toBe('false'); // Verify aria-required + }); + + it('renders custom data attributes correctly', async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('data-testid')).toBe('test-element'); // Verify custom data attribute + expect(element.getAttribute('data-custom-attribute')).toBe('custom-value'); // Verify custom data attribute + }); + + it('registers and cleans up root element properly', async () => { + let rootElement: HTMLElement | null = null; + editor.setRootElement = jest.fn((element) => { + rootElement = element; + }); + + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(rootElement).toBe(element); // Verify registration. + + await ReactTestUtils.act(async () => { + reactRoot.unmount(); // Unmount the component. + }); + + expect(rootElement).toBeNull(); // Verify cleanup. + }); + + it('renders the correct spellCheck attribute for different values', async () => { + const spellCheckValues = [true, false]; + + for (const spellCheck of spellCheckValues) { + await ReactTestUtils.act(async () => { + reactRoot.render( + , + ); + }); + + const element = container!.querySelector('[role="textbox"]')!; + expect(element.getAttribute('spellcheck')).toBe(spellCheck.toString()); + } + }); +}); diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 373bbceba73..64db5d1709b 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -88,7 +88,6 @@ function ContentEditableElementImpl( return (

); } From f198ba7c7d04ea99cb5a61dc9eae0b6857ba45bf Mon Sep 17 00:00:00 2001 From: wnhlee <2wheeh@gmail.com> Date: Wed, 13 Nov 2024 10:55:10 +0900 Subject: [PATCH 087/133] [lexical-rich-text][lexical-plain-text] workaround for Korean IME issue on iOS (#6819) Co-authored-by: kei kim Co-authored-by: deadintegral (kei) Co-authored-by: Sherry Wong Co-authored-by: Bob Ippolito --- packages/lexical-plain-text/src/index.ts | 6 ++++++ packages/lexical-rich-text/src/index.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/lexical-plain-text/src/index.ts b/packages/lexical-plain-text/src/index.ts index 1df4cb1c1ca..3300e8d73c3 100644 --- a/packages/lexical-plain-text/src/index.ts +++ b/packages/lexical-plain-text/src/index.ts @@ -274,6 +274,12 @@ export function registerPlainText(editor: LexicalEditor): () => void { return false; } + // Exception handling for iOS native behavior instead of Lexical's behavior when using Korean on iOS devices. + // more details - https://github.com/facebook/lexical/issues/5841 + if (IS_IOS && navigator.language === 'ko-KR') { + return false; + } + event.preventDefault(); return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true); }, diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index bf53a8acdd4..cec5da17fa7 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -860,7 +860,7 @@ export function registerRichText(editor: LexicalEditor): () => void { if (!$isRangeSelection(selection)) { return false; } - event.preventDefault(); + const {anchor} = selection; const anchorNode = anchor.getNode(); @@ -871,9 +871,18 @@ export function registerRichText(editor: LexicalEditor): () => void { ) { const element = $getNearestBlockElementAncestorOrThrow(anchorNode); if (element.getIndent() > 0) { + event.preventDefault(); return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined); } } + + // Exception handling for iOS native behavior instead of Lexical's behavior when using Korean on iOS devices. + // more details - https://github.com/facebook/lexical/issues/5841 + if (IS_IOS && navigator.language === 'ko-KR') { + return false; + } + event.preventDefault(); + return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true); }, COMMAND_PRIORITY_EDITOR, From 506ef8946779083fc2e9801779b68cb92b517a19 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 12 Nov 2024 20:16:31 -0800 Subject: [PATCH 088/133] [lexical][lexical-table] Feature: Scrollable tables with experimental getDOMSlot API (#6759) --- .../__tests__/e2e/Collaboration.spec.mjs | 32 +- .../html/TablesHTMLCopyAndPaste.spec.mjs | 4 +- .../lexical/ContextMenuCopyAndPaste.spec.mjs | 3 - .../__tests__/e2e/Indentation.spec.mjs | 4 +- .../__tests__/e2e/Selection.spec.mjs | 4 +- .../__tests__/e2e/Tables.spec.mjs | 146 ++- .../__tests__/e2e/TextEntry.spec.mjs | 1 - .../__tests__/e2e/Toolbar.spec.mjs | 7 +- .../4661-insert-column-selection.spec.mjs | 4 +- .../__tests__/utils/index.mjs | 61 +- packages/lexical-playground/src/Editor.tsx | 2 + packages/lexical-playground/src/Settings.tsx | 14 +- .../lexical-playground/src/appSettings.ts | 1 + packages/lexical-playground/src/index.css | 3 +- .../plugins/TableActionMenuPlugin/index.tsx | 17 +- .../src/plugins/TableCellResizer/index.tsx | 6 +- .../plugins/TableHoverActionsPlugin/index.tsx | 64 +- .../src/themes/PlaygroundEditorTheme.css | 8 + .../src/themes/PlaygroundEditorTheme.ts | 1 + .../lexical-react/src/LexicalTablePlugin.ts | 93 +- .../lexical-table/flow/LexicalTable.js.flow | 1 + .../lexical-table/src/LexicalTableNode.ts | 114 ++- .../lexical-table/src/LexicalTableObserver.ts | 218 ++-- .../src/LexicalTableSelectionHelpers.ts | 110 +- .../__tests__/unit/LexicalTableNode.test.tsx | 969 +++++++++++------- packages/lexical-table/src/index.ts | 5 +- .../lexical-website/docs/react/plugins.md | 2 + packages/lexical/flow/Lexical.js.flow | 28 + packages/lexical/src/LexicalEditor.ts | 2 + packages/lexical/src/LexicalMutations.ts | 98 +- packages/lexical/src/LexicalNode.ts | 19 + packages/lexical/src/LexicalReconciler.ts | 207 ++-- packages/lexical/src/LexicalSelection.ts | 82 +- packages/lexical/src/LexicalUtils.ts | 63 +- .../lexical/src/__tests__/utils/index.tsx | 21 +- packages/lexical/src/index.ts | 3 + .../lexical/src/nodes/LexicalElementNode.ts | 265 +++++ .../unit/LexicalElementNode.test.tsx | 88 ++ 38 files changed, 1922 insertions(+), 848 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs index b47b3c04f6a..296d9ac21a0 100644 --- a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs @@ -322,17 +322,43 @@ test.describe('Collaboration', () => { // Left collaborator types two pieces of text in the same paragraph, but with different styling. await focusEditor(page); await page.keyboard.type('normal'); + await assertHTML( + page, + html` +

+ normal +

+ `, + ); await sleep(1050); await toggleBold(page); await page.keyboard.type('bold'); + await assertHTML( + page, + html` +

+ normal + + bold + +

+ `, + ); + const boldSleep = sleep(1050); + // Right collaborator types at the end of the paragraph. - await sleep(50); await page .frameLocator('iframe[name="right"]') .locator('[data-lexical-editor="true"]') .focus(); - await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph + await page.keyboard.press('ArrowDown', {delay: 50}); // Move caret to end of paragraph await page.keyboard.type('BOLD'); await assertHTML( @@ -352,7 +378,7 @@ test.describe('Collaboration', () => { ); // Left collaborator undoes their bold text. - await sleep(50); + await boldSleep; await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click(); // The undo also removed bold the text node from YJS. diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs index 520b08552c5..8ac28284f78 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs @@ -17,7 +17,9 @@ import { } from '../../../utils/index.mjs'; test.describe('HTML Tables CopyAndPaste', () => { - test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); + test.beforeEach(({isCollab, page}) => + initialize({isCollab, page, tableHorizontalScroll: false}), + ); test('Copy + paste (Table - Google Docs)', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs index 108cd0f0617..db9aba8103b 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs @@ -32,9 +32,7 @@ test.describe('ContextMenuCopyAndPaste', () => { await page.keyboard.type('hello'); await click(page, '.lock'); - await page.pause(); await doubleClick(page, 'div[contenteditable="false"] span'); - await page.pause(); await withExclusiveClipboardAccess(async () => { await click(page, 'div[contenteditable="false"] span', {button: 'right'}); await click(page, '#typeahead-menu [role="option"] :text("Copy")'); @@ -72,7 +70,6 @@ test.describe('ContextMenuCopyAndPaste', () => { await click(page, '.font-increment'); await focusEditor(page); await page.keyboard.type('MLH Fellowship'); - //await page.pause(); await moveToLineEnd(page); await page.keyboard.press('Enter'); await page.keyboard.type('Fall 2024'); diff --git a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs index 3a856bd19f3..58a0b41af91 100644 --- a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs @@ -18,7 +18,9 @@ import { } from '../utils/index.mjs'; test.describe('Identation', () => { - test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); + test.beforeEach(({isCollab, page}) => + initialize({isCollab, page, tableHorizontalScroll: false}), + ); test(`Can create content and indent and outdent it all`, async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 026cd94d92d..71bba6fa5da 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -53,7 +53,9 @@ import { } from '../utils/index.mjs'; test.describe.parallel('Selection', () => { - test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); + test.beforeEach(({isCollab, page}) => + initialize({isCollab, page, tableHorizontalScroll: false}), + ); test('does not focus the editor on load', async ({page}) => { const editorHasFocus = async () => await evaluate(page, () => { diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index adbfbc73be2..d9362c28aed 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -18,7 +18,7 @@ import { selectCharacters, } from '../keyboardShortcuts/index.mjs'; import { - assertHTML, + assertHTML as rawAssertHTML, assertSelection, click, clickSelectors, @@ -37,6 +37,7 @@ import { insertTableRowBelow, IS_COLLAB, IS_LINUX, + IS_TABLE_HORIZONTAL_SCROLL, IS_WINDOWS, LEGACY_EVENTS, mergeTableCells, @@ -52,6 +53,7 @@ import { unmergeTableCell, waitForSelector, withExclusiveClipboardAccess, + wrapTableHtml, } from '../utils/index.mjs'; async function fillTablePartiallyWithText(page) { @@ -75,6 +77,28 @@ async function fillTablePartiallyWithText(page) { await page.keyboard.press('c'); } +async function assertHTML( + page, + expectedHtml, + expectedHtmlFrameRight = undefined, + options = undefined, + ...args +) { + return await rawAssertHTML( + page, + IS_TABLE_HORIZONTAL_SCROLL + ? wrapTableHtml(expectedHtml, options) + : expectedHtml, + IS_TABLE_HORIZONTAL_SCROLL && expectedHtmlFrameRight !== undefined + ? wrapTableHtml(expectedHtmlFrameRight, options) + : expectedHtmlFrameRight, + options, + ...args, + ); +} + +const WRAPPER = IS_TABLE_HORIZONTAL_SCROLL ? [0] : []; + test.describe.parallel('Tables', () => { test(`Can a table be inserted from the toolbar`, async ({ page, @@ -181,12 +205,13 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); await moveLeft(page, 1); + await assertSelection(page, { anchorOffset: 0, anchorPath: [0], @@ -196,19 +221,20 @@ test.describe.parallel('Tables', () => { await moveRight(page, 1); await page.keyboard.type('ab'); + await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 1, 0, 0, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 1, 0, 0, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], }); await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0], }); }); @@ -226,9 +252,9 @@ test.describe.parallel('Tables', () => { await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0], }); await moveRight(page, 1); @@ -243,9 +269,9 @@ test.describe.parallel('Tables', () => { await page.keyboard.type('ab'); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 2, 1, 0, 0, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0, 0, 0], focusOffset: 2, - focusPath: [1, 2, 1, 0, 0, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0, 0, 0], }); await moveRight(page, 3); @@ -271,17 +297,17 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 1, ...WRAPPER, 1, 0, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); }); @@ -300,17 +326,17 @@ test.describe.parallel('Tables', () => { await moveRight(page, 3); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 1, 0, 1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 1, 0, 1, ...WRAPPER, 2, 1, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 2], + anchorPath: [1, ...WRAPPER, 1, 0, 2], focusOffset: 0, - focusPath: [1, 1, 0, 2], + focusPath: [1, ...WRAPPER, 1, 0, 2], }); }); }); @@ -345,9 +371,9 @@ test.describe.parallel('Tables', () => { await deleteBackward(page); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0], }); await assertHTML( page, @@ -381,14 +407,24 @@ test.describe.parallel('Tables', () => { ); await moveRight(page, 1); - // The native window selection should be on the root, whereas - // the editor selection should be on the last cell of the table. - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [], - focusOffset: 2, - focusPath: [], - }); + if (WRAPPER.length === 0) { + // The native window selection should be on the root, whereas + // the editor selection should be on the last cell of the table. + await assertSelection(page, { + anchorOffset: 2, + anchorPath: [], + focusOffset: 2, + focusPath: [], + }); + } else { + // The native window selection is in the wrapper after the table + await assertSelection(page, { + anchorOffset: WRAPPER[0] + 1, + anchorPath: [1], + focusOffset: WRAPPER[0] + 1, + focusPath: [1], + }); + } await page.keyboard.press('Enter'); await assertSelection(page, { @@ -514,9 +550,9 @@ test.describe.parallel('Tables', () => { await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0], }); }); @@ -566,57 +602,57 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, ...WRAPPER, 1, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, ...WRAPPER, 1, 1, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 0, 0], + anchorPath: [1, ...WRAPPER, 2, 0, 0], focusOffset: 0, - focusPath: [1, 2, 0, 0], + focusPath: [1, ...WRAPPER, 2, 0, 0], }); await moveRight(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 1, 0], + anchorPath: [1, ...WRAPPER, 2, 1, 0], focusOffset: 0, - focusPath: [1, 2, 1, 0], + focusPath: [1, ...WRAPPER, 2, 1, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 0, 0], + anchorPath: [1, ...WRAPPER, 2, 0, 0], focusOffset: 0, - focusPath: [1, 2, 0, 0], + focusPath: [1, ...WRAPPER, 2, 0, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 1, 0], + anchorPath: [1, ...WRAPPER, 1, 1, 0], focusOffset: 0, - focusPath: [1, 1, 1, 0], + focusPath: [1, ...WRAPPER, 1, 1, 0], }); await moveLeft(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); }); @@ -633,25 +669,25 @@ test.describe.parallel('Tables', () => { await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); await moveDown(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 2, 0, 0], + anchorPath: [1, ...WRAPPER, 2, 0, 0], focusOffset: 0, - focusPath: [1, 2, 0, 0], + focusPath: [1, ...WRAPPER, 2, 0, 0], }); await moveUp(page, 1); await assertSelection(page, { anchorOffset: 0, - anchorPath: [1, 1, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0], focusOffset: 0, - focusPath: [1, 1, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0], }); }); @@ -669,9 +705,9 @@ test.describe.parallel('Tables', () => { await page.keyboard.type('@A'); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 1, 0, 0, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 1, 0, 0, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], }); await waitForSelector(page, `#typeahead-menu ul li:first-child.selected`); @@ -679,9 +715,9 @@ test.describe.parallel('Tables', () => { await moveDown(page, 1); await assertSelection(page, { anchorOffset: 2, - anchorPath: [1, 1, 0, 0, 0, 0], + anchorPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], focusOffset: 2, - focusPath: [1, 1, 0, 0, 0, 0], + focusPath: [1, ...WRAPPER, 1, 0, 0, 0, 0], }); await waitForSelector( diff --git a/packages/lexical-playground/__tests__/e2e/TextEntry.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextEntry.spec.mjs index 6a88dd49de3..c2c60760526 100644 --- a/packages/lexical-playground/__tests__/e2e/TextEntry.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextEntry.spec.mjs @@ -641,7 +641,6 @@ test.describe('TextEntry', () => {


`, ); - await page.pause(); await assertSelection(page, { anchorOffset: 0, anchorPath: [1], diff --git a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs index 12cec473d34..232dfa8dd68 100644 --- a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs @@ -31,7 +31,12 @@ import { test.describe('Toolbar', () => { test.beforeEach(({isCollab, page}) => - initialize({isCollab, page, showNestedEditorTreeView: false}), + initialize({ + isCollab, + page, + showNestedEditorTreeView: false, + tableHorizontalScroll: false, + }), ); test( diff --git a/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs b/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs index abcd0492817..d2f29d9aade 100644 --- a/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4661-insert-column-selection.spec.mjs @@ -20,7 +20,9 @@ import { } from '../utils/index.mjs'; test.describe('Regression test #4661', () => { - test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); + test.beforeEach(({isCollab, page}) => + initialize({isCollab, page, tableHorizontalScroll: false}), + ); test('inserting 2 columns before inserts before selection', async ({ page, isPlainText, diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 2b461f5fb05..9f43f61e5fd 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -34,6 +34,8 @@ export const IS_COLLAB = const IS_RICH_TEXT = process.env.E2E_EDITOR_MODE !== 'plain-text'; const IS_PLAIN_TEXT = process.env.E2E_EDITOR_MODE === 'plain-text'; export const LEGACY_EVENTS = process.env.E2E_EVENTS_MODE === 'legacy-events'; +export const IS_TABLE_HORIZONTAL_SCROLL = + process.env.E2E_TABLE_MODE !== 'legacy'; export const SAMPLE_IMAGE_URL = E2E_PORT === 3000 ? '/src/images/yellow-flower.jpg' @@ -52,6 +54,21 @@ function wrapAndSlowDown(method, delay) { }; } +export function wrapTableHtml(expected, {ignoreClasses = false} = {}) { + return html` + ${expected + .replace( + //g, '
')} + `; +} + export async function initialize({ page, isCollab, @@ -64,6 +81,7 @@ export async function initialize({ tableCellMerge, tableCellBackgroundColor, shouldUseLexicalContextMenu, + tableHorizontalScroll, }) { // Tests with legacy events often fail to register keypress, so // slowing it down to reduce flakiness @@ -76,6 +94,8 @@ export async function initialize({ appSettings.isRichText = IS_RICH_TEXT; appSettings.emptyEditor = true; appSettings.disableBeforeInput = LEGACY_EVENTS; + appSettings.tableHorizontalScroll = + tableHorizontalScroll ?? IS_TABLE_HORIZONTAL_SCROLL; if (isCollab) { appSettings.isCollab = isCollab; appSettings.collabId = randomUUID(); @@ -175,6 +195,16 @@ export async function clickSelectors(page, selectors) { await click(page, selectors[i]); } } + +function removeSafariLinebreakImgHack(actualHtml) { + return E2E_BROWSER === 'webkit' + ? actualHtml.replaceAll( + /]+ )?data-lexical-linebreak="true"(?: [^>]+)?>/g, + '', + ) + : actualHtml; +} + /** * @param {import('@playwright/test').Page | import('@playwright/test').Frame} pageOrFrame */ @@ -191,10 +221,12 @@ async function assertHTMLOnPageOrFrame( ignoreInlineStyles, }); return await expect(async () => { - const actualHtml = await pageOrFrame - .locator('div[contenteditable="true"]') - .first() - .innerHTML(); + const actualHtml = removeSafariLinebreakImgHack( + await pageOrFrame + .locator('div[contenteditable="true"]') + .first() + .innerHTML(), + ); let actual = prettifyHTML(actualHtml.replace(/\n/gm, ''), { ignoreClasses, ignoreInlineStyles, @@ -338,13 +370,30 @@ async function assertSelectionOnPageOrFrame(page, expected) { return path.reverse(); }; + const fixOffset = (node, offset) => { + // If the selection offset is at the br of a webkit img+br linebreak + // then move the offset to the img so the tests are consistent across + // browsers + if (node && node.nodeType === Node.ELEMENT_NODE && offset > 0) { + const child = node.children[offset - 1]; + if ( + child && + child.nodeType === Node.ELEMENT_NODE && + child.getAttribute('data-lexical-linebreak') === 'true' + ) { + return offset - 1; + } + } + return offset; + }; + const {anchorNode, anchorOffset, focusNode, focusOffset} = window.getSelection(); return { - anchorOffset, + anchorOffset: fixOffset(anchorNode, anchorOffset), anchorPath: getPathFromNode(anchorNode), - focusOffset, + focusOffset: fixOffset(focusNode, focusOffset), focusPath: getPathFromNode(focusNode), }; }, expected); diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 2c4f0419575..3fd409b5774 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -94,6 +94,7 @@ export default function Editor(): JSX.Element { shouldPreserveNewLinesInMarkdown, tableCellMerge, tableCellBackgroundColor, + tableHorizontalScroll, }, } = useSettings(); const isEditable = useLexicalEditable(); @@ -199,6 +200,7 @@ export default function Editor(): JSX.Element { diff --git a/packages/lexical-playground/src/Settings.tsx b/packages/lexical-playground/src/Settings.tsx index b015f570d9d..2a126f8d0db 100644 --- a/packages/lexical-playground/src/Settings.tsx +++ b/packages/lexical-playground/src/Settings.tsx @@ -28,10 +28,11 @@ export default function Settings(): JSX.Element { isAutocomplete, showTreeView, showNestedEditorTreeView, - disableBeforeInput, + // disableBeforeInput, showTableOfContents, shouldUseLexicalContextMenu, shouldPreserveNewLinesInMarkdown, + // tableHorizontalScroll, }, } = useSettings(); useEffect(() => { @@ -132,14 +133,14 @@ export default function Settings(): JSX.Element { checked={isAutocomplete} text="Autocomplete" /> - { setOption('disableBeforeInput', !disableBeforeInput); setTimeout(() => window.location.reload(), 500); }} checked={disableBeforeInput} text="Legacy Events" - /> + /> */} { setOption('showTableOfContents', !showTableOfContents); @@ -167,6 +168,13 @@ export default function Settings(): JSX.Element { checked={shouldPreserveNewLinesInMarkdown} text="Preserve newlines in Markdown" /> + {/* { + setOption('tableHorizontalScroll', !tableHorizontalScroll); + }} + checked={tableHorizontalScroll} + text="Tables have horizontal scroll" + /> */}
) : null} diff --git a/packages/lexical-playground/src/appSettings.ts b/packages/lexical-playground/src/appSettings.ts index d698af382c2..ab489c3e668 100644 --- a/packages/lexical-playground/src/appSettings.ts +++ b/packages/lexical-playground/src/appSettings.ts @@ -29,6 +29,7 @@ export const DEFAULT_SETTINGS = { showTreeView: true, tableCellBackgroundColor: true, tableCellMerge: true, + tableHorizontalScroll: true, } as const; // These are mutated in setupEnv diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index e5362290c69..31718a446af 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -74,17 +74,18 @@ header h1 { .editor-scroller { min-height: 150px; + max-width: 100%; border: 0; display: flex; position: relative; outline: 0; z-index: 0; - overflow: auto; resize: vertical; } .editor { flex: auto; + max-width: 100%; position: relative; resize: vertical; z-index: -1; diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 60a09ab8a09..43f1f8aa5d2 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -24,8 +24,8 @@ import { $isTableRowNode, $isTableSelection, $unmergeCell, + getTableElement, getTableObserverFromTableElement, - HTMLTableElementWithWithTableSelectionState, TableCellHeaderStates, TableCellNode, TableRowNode, @@ -43,6 +43,7 @@ import { import * as React from 'react'; import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; +import invariant from 'shared/invariant'; import useModal from '../../hooks/useModal'; import ColorPicker from '../../ui/ColorPicker'; @@ -229,13 +230,15 @@ function TableActionMenu({ editor.update(() => { if (tableCellNode.isAttached()) { const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const tableElement = editor.getElementByKey( - tableNode.getKey(), - ) as HTMLTableElementWithWithTableSelectionState; + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNode.getKey()), + ); - if (!tableElement) { - throw new Error('Expected to find tableElement in DOM'); - } + invariant( + tableElement !== null, + 'TableActionMenu: Expected to find tableElement in DOM', + ); const tableObserver = getTableObserverFromTableElement(tableElement); if (tableObserver !== null) { diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index f0446ee6b1f..7f586262645 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -19,6 +19,7 @@ import { $isTableCellNode, $isTableRowNode, getDOMCellFromTarget, + getTableElement, TableNode, } from '@lexical/table'; import {calculateZoomLevel} from '@lexical/utils'; @@ -115,7 +116,10 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const tableElement = editor.getElementByKey(tableNode.getKey()); + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNode.getKey()), + ); if (!tableElement) { throw new Error('TableCellResizer: Table element not found.'); diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx index 44fc3368a02..92a26ff0015 100644 --- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -9,12 +9,14 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import { + $getTableAndElementByKey, $getTableColumnIndexFromTableCellNode, $getTableRowIndexFromTableCellNode, $insertTableColumn__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode, $isTableNode, + getTableElement, TableCellNode, TableNode, TableRowNode, @@ -75,7 +77,10 @@ function TableHoverActionsContainer({ return; } - tableDOMElement = editor.getElementByKey(table?.getKey()); + tableDOMElement = getTableElement( + table, + editor.getElementByKey(table.getKey()), + ); if (tableDOMElement) { const rowCount = table.getChildrenSize(); @@ -163,36 +168,37 @@ function TableHoverActionsContainer({ editor.registerMutationListener( TableNode, (mutations) => { - editor.getEditorState().read(() => { - for (const [key, type] of mutations) { - const tableDOMElement = editor.getElementByKey(key); - switch (type) { - case 'created': - tableSetRef.current.add(key); - setShouldListenMouseMove(tableSetRef.current.size > 0); - if (tableDOMElement) { - tableResizeObserver.observe(tableDOMElement); + editor.getEditorState().read( + () => { + let resetObserver = false; + for (const [key, type] of mutations) { + switch (type) { + case 'created': { + tableSetRef.current.add(key); + resetObserver = true; + break; + } + case 'destroyed': { + tableSetRef.current.delete(key); + resetObserver = true; + break; } - break; - - case 'destroyed': - tableSetRef.current.delete(key); - setShouldListenMouseMove(tableSetRef.current.size > 0); - // Reset resize observers - tableResizeObserver.disconnect(); - tableSetRef.current.forEach((tableKey: NodeKey) => { - const tableElement = editor.getElementByKey(tableKey); - if (tableElement) { - tableResizeObserver.observe(tableElement); - } - }); - break; - - default: - break; + default: + break; + } } - } - }); + if (resetObserver) { + // Reset resize observers + tableResizeObserver.disconnect(); + for (const tableKey of tableSetRef.current) { + const {tableElement} = $getTableAndElementByKey(tableKey); + tableResizeObserver.observe(tableElement); + } + setShouldListenMouseMove(tableSetRef.current.size > 0); + } + }, + {editor}, + ); }, {skipInitialization: false}, ), diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 22d27e4145e..cbed93864d1 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -116,6 +116,14 @@ text-align: right; min-width: 25px; } +.PlaygroundEditorTheme__tableScrollableWrapper { + overflow-x: auto; + margin: 0px 25px 30px 0px; +} +.PlaygroundEditorTheme__tableScrollableWrapper > .PlaygroundEditorTheme__table { + /* Remove the table's margin and put it on the wrapper */ + margin: 0; +} .PlaygroundEditorTheme__table { border-collapse: collapse; border-spacing: 0; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index c29d9d1434d..e1c87638895 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -103,6 +103,7 @@ const theme: EditorThemeClasses = { tableCellSortedIndicator: 'PlaygroundEditorTheme__tableCellSortedIndicator', tableResizeRuler: 'PlaygroundEditorTheme__tableCellResizeRuler', tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping', + tableScrollableWrapper: 'PlaygroundEditorTheme__tableScrollableWrapper', tableSelected: 'PlaygroundEditorTheme__tableSelected', tableSelection: 'PlaygroundEditorTheme__tableSelection', text: { diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts index e8b512eb790..a5c43d17c65 100644 --- a/packages/lexical-react/src/LexicalTablePlugin.ts +++ b/packages/lexical-react/src/LexicalTablePlugin.ts @@ -20,11 +20,13 @@ import { $createTableCellNode, $createTableNodeWithDimensions, $getNodeTriplet, + $getTableAndElementByKey, $isTableCellNode, - $isTableNode, $isTableRowNode, applyTableHandlers, + getTableElement, INSERT_TABLE_COMMAND, + setScrollableTablesActive, TableCellNode, TableNode, TableRowNode, @@ -36,24 +38,50 @@ import { } from '@lexical/utils'; import { $createParagraphNode, - $getNodeByKey, $isTextNode, COMMAND_PRIORITY_EDITOR, } from 'lexical'; import {useEffect} from 'react'; import invariant from 'shared/invariant'; +export interface TablePluginProps { + /** + * When `false` (default `true`), merged cell support (colspan and rowspan) will be disabled and all + * tables will be forced into a regular grid with 1x1 table cells. + */ + hasCellMerge?: boolean; + /** + * When `false` (default `true`), the background color of TableCellNode will always be removed. + */ + hasCellBackgroundColor?: boolean; + /** + * When `true` (default `true`), the tab key can be used to navigate table cells. + */ + hasTabHandler?: boolean; + /** + * When `true` (default `false`), tables will be wrapped in a `
` to enable horizontal scrolling + */ + hasHorizontalScroll?: boolean; +} + +/** + * A plugin to enable all of the features of Lexical's TableNode. + * + * @param props - See type for documentation + * @returns An element to render in your LexicalComposer + */ export function TablePlugin({ hasCellMerge = true, hasCellBackgroundColor = true, hasTabHandler = true, -}: { - hasCellMerge?: boolean; - hasCellBackgroundColor?: boolean; - hasTabHandler?: boolean; -}): JSX.Element | null { + hasHorizontalScroll = false, +}: TablePluginProps): JSX.Element | null { const [editor] = useLexicalComposerContext(); + useEffect(() => { + setScrollableTablesActive(editor, hasHorizontalScroll); + }, [editor, hasHorizontalScroll]); + useEffect(() => { if (!editor.hasNodes([TableNode, TableCellNode, TableRowNode])) { invariant( @@ -122,7 +150,7 @@ export function TablePlugin({ nodeKey: NodeKey, dom: HTMLElement, ) => { - const tableElement = dom as HTMLTableElementWithWithTableSelectionState; + const tableElement = getTableElement(tableNode, dom); const tableSelection = applyTableHandlers( tableNode, tableElement, @@ -135,34 +163,31 @@ export function TablePlugin({ const unregisterMutationListener = editor.registerMutationListener( TableNode, (nodeMutations) => { - for (const [nodeKey, mutation] of nodeMutations) { - if (mutation === 'created' || mutation === 'updated') { - const tableSelection = tableSelections.get(nodeKey); - const dom = editor.getElementByKey(nodeKey); - if (!(tableSelection && dom === tableSelection[1])) { - // The update created a new DOM node, destroy the existing TableObserver - if (tableSelection) { - tableSelection[0].removeListeners(); - tableSelections.delete(nodeKey); - } - if (dom !== null) { - // Create a new TableObserver - editor.getEditorState().read(() => { - const tableNode = $getNodeByKey(nodeKey); - if ($isTableNode(tableNode)) { - initializeTableNode(tableNode, nodeKey, dom); - } - }); + editor.getEditorState().read( + () => { + for (const [nodeKey, mutation] of nodeMutations) { + const tableSelection = tableSelections.get(nodeKey); + if (mutation === 'created' || mutation === 'updated') { + const {tableNode, tableElement} = + $getTableAndElementByKey(nodeKey); + if (tableSelection === undefined) { + initializeTableNode(tableNode, nodeKey, tableElement); + } else if (tableElement !== tableSelection[1]) { + // The update created a new DOM node, destroy the existing TableObserver + tableSelection[0].removeListeners(); + tableSelections.delete(nodeKey); + initializeTableNode(tableNode, nodeKey, tableElement); + } + } else if (mutation === 'destroyed') { + if (tableSelection !== undefined) { + tableSelection[0].removeListeners(); + tableSelections.delete(nodeKey); + } } } - } else if (mutation === 'destroyed') { - const tableSelection = tableSelections.get(nodeKey); - if (tableSelection !== undefined) { - tableSelection[0].removeListeners(); - tableSelections.delete(nodeKey); - } - } - } + }, + {editor}, + ); }, {skipInitialization: false}, ); diff --git a/packages/lexical-table/flow/LexicalTable.js.flow b/packages/lexical-table/flow/LexicalTable.js.flow index 8ec1b813b10..19014ebd897 100644 --- a/packages/lexical-table/flow/LexicalTable.js.flow +++ b/packages/lexical-table/flow/LexicalTable.js.flow @@ -162,6 +162,7 @@ declare export function applyTableHandlers( tableNode: TableNode, tableElement: HTMLElement, editor: LexicalEditor, + hasTabHandler: boolean, ): TableObserver; declare export function $getElementForTableNode( diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index be988c5af96..838758e2e83 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -6,18 +6,6 @@ * */ -import type { - DOMConversionMap, - DOMConversionOutput, - DOMExportOutput, - EditorConfig, - LexicalEditor, - LexicalNode, - NodeKey, - SerializedElementNode, - Spread, -} from 'lexical'; - import { addClassNamesToElement, isHTMLElement, @@ -25,9 +13,22 @@ import { } from '@lexical/utils'; import { $applyNodeReplacement, + $getEditor, $getNearestNodeFromDOMNode, + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + ElementDOMSlot, ElementNode, + LexicalEditor, + LexicalNode, + NodeKey, + SerializedElementNode, + setDOMUnmanaged, + Spread, } from 'lexical'; +import invariant from 'shared/invariant'; import {PIXEL_VALUE_REG_EXP} from './constants'; import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; @@ -79,6 +80,30 @@ function setRowStriping( } } +const scrollableEditors = new WeakSet(); + +export function $isScrollableTablesActive( + editor: LexicalEditor = $getEditor(), +): boolean { + return scrollableEditors.has(editor); +} + +export function setScrollableTablesActive( + editor: LexicalEditor, + active: boolean, +) { + if (active) { + if (__DEV__ && !editor._config.theme.tableScrollableWrapper) { + console.warn( + 'TableNode: hasHorizontalScroll is active but theme.tableScrollableWrapper is not defined.', + ); + } + scrollableEditors.add(editor); + } else { + scrollableEditors.delete(editor); + } +} + /** @noInheritDoc */ export class TableNode extends ElementNode { /** @internal */ @@ -142,6 +167,19 @@ export class TableNode extends ElementNode { }; } + getDOMSlot(element: HTMLElement): ElementDOMSlot { + const tableElement = + (element.nodeName !== 'TABLE' && element.querySelector('table')) || + element; + invariant( + tableElement.nodeName === 'TABLE', + 'TableNode.getDOMSlot: createDOM() did not return a table', + ); + return super + .getDOMSlot(tableElement) + .withAfter(tableElement.querySelector('colgroup')); + } + createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement { const tableElement = document.createElement('table'); const colGroup = document.createElement('colgroup'); @@ -152,11 +190,23 @@ export class TableNode extends ElementNode { this.getColumnCount(), this.getColWidths(), ); + setDOMUnmanaged(colGroup); addClassNamesToElement(tableElement, config.theme.table); if (this.__rowStriping) { setRowStriping(tableElement, config, true); } + if ($isScrollableTablesActive(editor)) { + const wrapperElement = document.createElement('div'); + const classes = config.theme.tableScrollableWrapper; + if (classes) { + addClassNamesToElement(wrapperElement, classes); + } else { + wrapperElement.style.cssText = 'overflow-x: auto;'; + } + wrapperElement.appendChild(tableElement); + return wrapperElement; + } return tableElement; } @@ -177,21 +227,24 @@ export class TableNode extends ElementNode { return { ...super.exportDOM(editor), after: (tableElement) => { - if (tableElement) { - const newElement = tableElement.cloneNode() as ParentNode; - const colGroup = document.createElement('colgroup'); + if ( + tableElement && + isHTMLElement(tableElement) && + tableElement.nodeName !== 'TABLE' + ) { + tableElement = tableElement.querySelector('table'); + } + if (!tableElement || !isHTMLElement(tableElement)) { + return null; + } + // Wrap direct descendant rows in a tbody for export + const rows = tableElement.querySelectorAll(':scope > tr'); + if (rows.length > 0) { const tBody = document.createElement('tbody'); - if (isHTMLElement(tableElement)) { - const cols = tableElement.querySelectorAll('col'); - colGroup.append(...cols); - const rows = tableElement.querySelectorAll('tr'); - tBody.append(...rows); - } - - newElement.replaceChildren(colGroup, tBody); - - return newElement as HTMLElement; + tBody.append(...rows); + tableElement.append(tBody); } + return tableElement; }, }; } @@ -344,12 +397,11 @@ export function $getElementForTableNode( tableNode: TableNode, ): TableDOMTable { const tableElement = editor.getElementByKey(tableNode.getKey()); - - if (tableElement == null) { - throw new Error('Table Element Not Found'); - } - - return getTable(tableElement); + invariant( + tableElement !== null, + '$getElementForTableNode: Table Element Not Found', + ); + return getTable(tableNode, tableElement); } export function $convertTableElement( diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 03030509a52..9d3ffbbc690 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -16,6 +16,7 @@ import { $createParagraphNode, $createRangeSelection, $createTextNode, + $getEditor, $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, @@ -28,7 +29,7 @@ import { import invariant from 'shared/invariant'; import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; -import {$isTableNode} from './LexicalTableNode'; +import {$isTableNode, TableNode} from './LexicalTableNode'; import { $createTableSelection, $isTableSelection, @@ -39,6 +40,8 @@ import { $updateDOMForSelection, getDOMSelection, getTable, + getTableElement, + HTMLTableElementWithWithTableSelectionState, } from './LexicalTableSelectionHelpers'; export type TableDOMCell = { @@ -57,6 +60,31 @@ export type TableDOMTable = { rows: number; }; +export function $getTableAndElementByKey( + tableNodeKey: NodeKey, + editor: LexicalEditor = $getEditor(), +): { + tableNode: TableNode; + tableElement: HTMLTableElementWithWithTableSelectionState; +} { + const tableNode = $getNodeByKey(tableNodeKey); + invariant( + $isTableNode(tableNode), + 'TableObserver: Expected tableNodeKey %s to be a TableNode', + tableNodeKey, + ); + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNodeKey), + ); + invariant( + tableElement !== null, + 'TableObserver: Expected to find TableElement in DOM for key %s', + tableNodeKey, + ); + return {tableElement, tableNode}; +} + export class TableObserver { focusX: number; focusY: number; @@ -74,6 +102,7 @@ export class TableObserver { tableSelection: TableSelection | null; hasHijackedSelectionStyles: boolean; isSelecting: boolean; + shouldCheckSelection: boolean; abortController: AbortController; listenerOptions: {signal: AbortSignal}; @@ -97,10 +126,11 @@ export class TableObserver { this.anchorCell = null; this.focusCell = null; this.hasHijackedSelectionStyles = false; - this.trackTable(); this.isSelecting = false; + this.shouldCheckSelection = false; this.abortController = new AbortController(); this.listenerOptions = {signal: this.abortController.signal}; + this.trackTable(); } getTable(): TableDOMTable { @@ -115,54 +145,57 @@ export class TableObserver { this.listenersToRemove.clear(); } + $lookup(): { + tableNode: TableNode; + tableElement: HTMLTableElementWithWithTableSelectionState; + } { + return $getTableAndElementByKey(this.tableNodeKey, this.editor); + } + trackTable() { const observer = new MutationObserver((records) => { - this.editor.update(() => { - let gridNeedsRedraw = false; - - for (let i = 0; i < records.length; i++) { - const record = records[i]; - const target = record.target; - const nodeName = target.nodeName; - - if ( - nodeName === 'TABLE' || - nodeName === 'TBODY' || - nodeName === 'THEAD' || - nodeName === 'TR' - ) { - gridNeedsRedraw = true; - break; + this.editor.getEditorState().read( + () => { + let gridNeedsRedraw = false; + + for (let i = 0; i < records.length; i++) { + const record = records[i]; + const target = record.target; + const nodeName = target.nodeName; + + if ( + nodeName === 'TABLE' || + nodeName === 'TBODY' || + nodeName === 'THEAD' || + nodeName === 'TR' + ) { + gridNeedsRedraw = true; + break; + } } - } - - if (!gridNeedsRedraw) { - return; - } - - const tableElement = this.editor.getElementByKey(this.tableNodeKey); - - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } - - this.table = getTable(tableElement); - }); - }); - this.editor.update(() => { - const tableElement = this.editor.getElementByKey(this.tableNodeKey); - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } + if (!gridNeedsRedraw) { + return; + } - this.table = getTable(tableElement); - observer.observe(tableElement, { - attributes: true, - childList: true, - subtree: true, - }); + const {tableNode, tableElement} = this.$lookup(); + this.table = getTable(tableNode, tableElement); + }, + {editor: this.editor}, + ); }); + this.editor.getEditorState().read( + () => { + const {tableNode, tableElement} = this.$lookup(); + this.table = getTable(tableNode, tableElement); + observer.observe(tableElement, { + attributes: true, + childList: true, + subtree: true, + }); + }, + {editor: this.editor}, + ); } clearHighlight() { @@ -182,19 +215,8 @@ export class TableObserver { this.enableHighlightStyle(); editor.update(() => { - const tableNode = $getNodeByKey(this.tableNodeKey); - - if (!$isTableNode(tableNode)) { - throw new Error('Expected TableNode.'); - } - - const tableElement = editor.getElementByKey(this.tableNodeKey); - - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } - - const grid = getTable(tableElement); + const {tableNode, tableElement} = this.$lookup(); + const grid = getTable(tableNode, tableElement); $updateDOMForSelection(editor, grid, null); $setSelection(null); editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); @@ -203,34 +225,34 @@ export class TableObserver { enableHighlightStyle() { const editor = this.editor; - editor.update(() => { - const tableElement = editor.getElementByKey(this.tableNodeKey); - - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } - - removeClassNamesFromElement( - tableElement, - editor._config.theme.tableSelection, - ); - tableElement.classList.remove('disable-selection'); - this.hasHijackedSelectionStyles = false; - }); + editor.getEditorState().read( + () => { + const {tableElement} = this.$lookup(); + + removeClassNamesFromElement( + tableElement, + editor._config.theme.tableSelection, + ); + tableElement.classList.remove('disable-selection'); + this.hasHijackedSelectionStyles = false; + }, + {editor}, + ); } disableHighlightStyle() { const editor = this.editor; - editor.update(() => { - const tableElement = editor.getElementByKey(this.tableNodeKey); - - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } - - addClassNamesToElement(tableElement, editor._config.theme.tableSelection); - this.hasHijackedSelectionStyles = true; - }); + editor.getEditorState().read( + () => { + const {tableElement} = this.$lookup(); + addClassNamesToElement( + tableElement, + editor._config.theme.tableSelection, + ); + this.hasHijackedSelectionStyles = true; + }, + {editor}, + ); } updateTableTableSelection(selection: TableSelection | null): void { @@ -248,20 +270,32 @@ export class TableObserver { } } + /** + * @internal + * Firefox has a strange behavior where pressing the down arrow key from + * above the table will move the caret after the table and then lexical + * will select the last cell instead of the first. + * We do still want to let the browser handle caret movement but we will + * use this property to "tag" the update so that we can recheck the + * selection after the event is processed. + */ + setShouldCheckSelection(): void { + this.shouldCheckSelection = true; + } + /** + * @internal + */ + getAndClearShouldCheckSelection(): boolean { + if (this.shouldCheckSelection) { + this.shouldCheckSelection = false; + return true; + } + return false; + } setFocusCellForSelection(cell: TableDOMCell, ignoreStart = false) { const editor = this.editor; editor.update(() => { - const tableNode = $getNodeByKey(this.tableNodeKey); - - if (!$isTableNode(tableNode)) { - throw new Error('Expected TableNode.'); - } - - const tableElement = editor.getElementByKey(this.tableNodeKey); - - if (!tableElement) { - throw new Error('Expected to find TableElement in DOM'); - } + const {tableNode} = this.$lookup(); const cellX = cell.x; const cellY = cell.y; diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index f85709749b4..f629d1feb0d 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -7,7 +7,6 @@ */ import type {TableCellNode} from './LexicalTableCellNode'; -import type {TableNode} from './LexicalTableNode'; import type {TableDOMCell, TableDOMRows} from './LexicalTableObserver'; import type { TableMapType, @@ -68,7 +67,11 @@ import {CAN_USE_DOM} from 'shared/canUseDOM'; import invariant from 'shared/invariant'; import {$isTableCellNode} from './LexicalTableCellNode'; -import {$isTableNode} from './LexicalTableNode'; +import { + $isScrollableTablesActive, + $isTableNode, + TableNode, +} from './LexicalTableNode'; import {TableDOMTable, TableObserver} from './LexicalTableObserver'; import {$isTableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; @@ -85,9 +88,27 @@ const isMouseDownOnEvent = (event: MouseEvent) => { return (event.buttons & 1) === 1; }; +export function getTableElement( + tableNode: TableNode, + dom: T, +): HTMLTableElementWithWithTableSelectionState | (T & null) { + if (!dom) { + return dom as T & null; + } + const element = ( + dom.nodeName === 'TABLE' ? dom : tableNode.getDOMSlot(dom).element + ) as HTMLTableElementWithWithTableSelectionState; + invariant( + element.nodeName === 'TABLE', + 'getTableElement: Expecting table in as DOM node for TableNode, not %s', + dom.nodeName, + ); + return element; +} + export function applyTableHandlers( tableNode: TableNode, - tableElement: HTMLTableElementWithWithTableSelectionState, + element: HTMLElement, editor: LexicalEditor, hasTabHandler: boolean, ): TableObserver { @@ -100,9 +121,10 @@ export function applyTableHandlers( const tableObserver = new TableObserver(editor, tableNode.getKey()); const editorWindow = editor._window || window; + const tableElement = getTableElement(tableNode, element); attachTableObserverToTableElement(tableElement, tableObserver); tableObserver.listenersToRemove.add(() => - deatatchTableObserverFromTableElement(tableElement, tableObserver), + detatchTableObserverFromTableElement(tableElement, tableObserver), ); const createMouseHandlers = () => { @@ -744,6 +766,29 @@ export function applyTableHandlers( () => { const selection = $getSelection(); const prevSelection = $getPreviousSelection(); + // If they pressed the down arrow with the selection outside of the + // table, and then the selection ends up in the table but not in the + // first cell, then move the selection to the first cell. + if ( + tableObserver.getAndClearShouldCheckSelection() && + $isRangeSelection(prevSelection) && + $isRangeSelection(selection) && + selection.isCollapsed() + ) { + const anchor = selection.anchor.getNode(); + const firstRow = tableNode.getFirstChild(); + const anchorCell = $findCellNode(anchor); + if (anchorCell !== null && $isTableRowNode(firstRow)) { + const firstCell = firstRow.getFirstChild(); + if ( + $isTableCellNode(firstCell) && + !$findMatchingParent(anchorCell, (node) => node.is(firstCell)) + ) { + firstCell.selectStart(); + return true; + } + } + } if ($isRangeSelection(selection)) { const {anchor, focus} = selection; @@ -944,7 +989,7 @@ export type HTMLTableElementWithWithTableSelectionState = HTMLTableElement & { [LEXICAL_ELEMENT_KEY]?: TableObserver | undefined; }; -export function deatatchTableObserverFromTableElement( +export function detatchTableObserverFromTableElement( tableElement: HTMLTableElementWithWithTableSelectionState, tableObserver: TableObserver, ) { @@ -1006,7 +1051,11 @@ export function doesTargetContainText(node: Node): boolean { return false; } -export function getTable(tableElement: HTMLElement): TableDOMTable { +export function getTable( + tableNode: TableNode, + dom: HTMLElement, +): TableDOMTable { + const tableElement = getTableElement(tableNode, dom); const domRows: TableDOMRows = []; const grid = { columns: 0, @@ -1538,6 +1587,10 @@ function $handleArrowKey( } } } + if (direction === 'down' && $isScrollableTablesActive(editor)) { + // Enable Firefox workaround + tableObserver.setShouldCheckSelection(); + } return false; } @@ -1559,11 +1612,12 @@ function $handleArrowKey( } const anchorCellTable = $findTableNode(anchorCellNode); if (anchorCellTable !== tableNode && anchorCellTable != null) { - const anchorCellTableElement = editor.getElementByKey( - anchorCellTable.getKey(), + const anchorCellTableElement = getTableElement( + anchorCellTable, + editor.getElementByKey(anchorCellTable.getKey()), ); if (anchorCellTableElement != null) { - tableObserver.table = getTable(anchorCellTableElement); + tableObserver.table = getTable(anchorCellTable, anchorCellTableElement); return $handleArrowKey( editor, event, @@ -1675,8 +1729,13 @@ function $handleArrowKey( ); const [tableNodeFromSelection] = selection.getNodes(); - const tableElement = editor.getElementByKey( - tableNodeFromSelection.getKey(), + invariant( + $isTableNode(tableNodeFromSelection), + '$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode', + ); + const tableElement = getTableElement( + tableNodeFromSelection, + editor.getElementByKey(tableNodeFromSelection.getKey()), ); if ( !$isTableCellNode(anchorCellNode) || @@ -1688,7 +1747,7 @@ function $handleArrowKey( } tableObserver.updateTableTableSelection(selection); - const grid = getTable(tableElement); + const grid = getTable(tableNodeFromSelection, tableElement); const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid); const anchorCell = tableNode.getDOMCellFromCordsOrThrow( cordsAnchor.x, @@ -1882,14 +1941,29 @@ function $getTableEdgeCursorPosition( return undefined; } - const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey()); - if (!tableNodeParentDOM) { - return undefined; - } - // TODO: Add support for nested tables const domSelection = window.getSelection(); - if (!domSelection || domSelection.anchorNode !== tableNodeParentDOM) { + if (!domSelection) { + return undefined; + } + const domAnchorNode = domSelection.anchorNode; + const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey()); + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNode.getKey()), + ); + // We are only interested in the scenario where the + // native selection anchor is: + // - at or inside the table's parent DOM + // - and NOT at or inside the table DOM + // It may be adjacent to the table DOM (e.g. in a wrapper) + if ( + !domAnchorNode || + !tableNodeParentDOM || + !tableElement || + !tableNodeParentDOM.contains(domAnchorNode) || + tableElement.contains(domAnchorNode) + ) { return undefined; } diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 570ac0e9ed2..df23bdcf843 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -27,8 +27,11 @@ import { } from 'lexical'; import { DataTransferMock, + expectHtmlToBeEqual, + html, initializeUnitTest, invariant, + polyfillContentEditable, } from 'lexical/src/__tests__/utils'; import {$getElementForTableNode, TableNode} from '../../LexicalTableNode'; @@ -68,386 +71,612 @@ const editorConfig = Object.freeze({ theme: { table: 'test-table-class', tableRowStriping: 'test-table-row-striping-class', + tableScrollableWrapper: 'table-scrollable-wrapper', }, }); -describe('LexicalTableNode tests', () => { - initializeUnitTest( - (testEnv) => { - beforeEach(async () => { - const {editor} = testEnv; - await editor.update(() => { - const root = $getRoot(); - const paragraph = $createParagraphNode(); - root.append(paragraph); - paragraph.select(); - }); - }); - - test('TableNode.constructor', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const tableNode = $createTableNode(); - - expect(tableNode).not.toBe(null); - }); - - expect(() => $createTableNode()).toThrow(); - }); - - test('TableNode.createDOM()', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const tableNode = $createTableNode(); - - expect(tableNode.createDOM(editorConfig).outerHTML).toBe( - `
`, - ); - }); - }); - - test('Copy table from an external source', async () => { - const {editor} = testEnv; - - const dataTransfer = new DataTransferMock(); - dataTransfer.setData( - 'text/html', - '

Hello there

General Kenobi!

Lexical is nice


', - ); - await editor.update(() => { - const selection = $getSelection(); - invariant( - $isRangeSelection(selection), - 'isRangeSelection(selection)', - ); - $insertDataTransferForRichText(dataTransfer, selection, editor); - }); - // Make sure paragraph is inserted inside empty cells - const emptyCell = '


${emptyCell}

Hello there

General Kenobi!

Lexical is nice

`, - ); - }); +function wrapTableHtml(expected: string): string { + return expected + .replace(//g, '
'); +} - test('Copy table from an external source like gdoc with formatting', async () => { - const {editor} = testEnv; +polyfillContentEditable(); - const dataTransfer = new DataTransferMock(); - dataTransfer.setData( - 'text/html', - '
SurfaceMWP_WORK_LS_COMPOSER77349
LexicalXDS_RICH_TEXT_AREAsdvd sdfvsfs
', - ); - await editor.update(() => { - const selection = $getSelection(); - invariant( - $isRangeSelection(selection), - 'isRangeSelection(selection)', - ); - $insertDataTransferForRichText(dataTransfer, selection, editor); - }); - expect(testEnv.innerHTML).toBe( - `

Surface

MWP_WORK_LS_COMPOSER

77349

Lexical

XDS_RICH_TEXT_AREA

sdvd sdfvsfs

`, +describe('LexicalTableNode tests', () => { + [false, true].forEach((hasHorizontalScroll) => { + describe(`hasHorizontalScroll={${hasHorizontalScroll}}`, () => { + function expectTableHtmlToBeEqual( + actual: string, + expected: string, + ): void { + return expectHtmlToBeEqual( + actual, + hasHorizontalScroll ? wrapTableHtml(expected) : expected, ); - }); - - test('Cut table in the middle of a range selection', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const paragraph = root.getFirstChild(); - const beforeText = $createTextNode('text before the table'); - const table = $createTableNodeWithDimensions(4, 4, true); - const afterText = $createTextNode('text after the table'); - - paragraph?.append(beforeText); - paragraph?.append(table); - paragraph?.append(afterText); - }); - await editor.update(() => { - editor.focus(); - $selectAll(); - }); - await editor.update(() => { - editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); - }); - - expect(testEnv.innerHTML).toBe(`


`); - }); - - test('Cut table as last node in range selection ', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const paragraph = root.getFirstChild(); - const beforeText = $createTextNode('text before the table'); - const table = $createTableNodeWithDimensions(4, 4, true); - - paragraph?.append(beforeText); - paragraph?.append(table); - }); - await editor.update(() => { - editor.focus(); - $selectAll(); - }); - await editor.update(() => { - editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); - }); - - expect(testEnv.innerHTML).toBe(`


`); - }); - - test('Cut table as first node in range selection ', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const paragraph = root.getFirstChild(); - const table = $createTableNodeWithDimensions(4, 4, true); - const afterText = $createTextNode('text after the table'); - - paragraph?.append(table); - paragraph?.append(afterText); - }); - await editor.update(() => { - editor.focus(); - $selectAll(); - }); - await editor.update(() => { - editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); - }); - - expect(testEnv.innerHTML).toBe(`


`); - }); - - test('Cut table is whole selection, should remove it', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const table = $createTableNodeWithDimensions(4, 4, true); - root.append(table); - }); - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - if (table) { - const DOMTable = $getElementForTableNode(editor, table); - if (DOMTable) { - table - ?.getCellNodeFromCords(0, 0, DOMTable) - ?.getLastChild() - ?.append($createTextNode('some text')); - const selection = $createTableSelection(); - selection.set( - table.__key, - table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', - table?.getCellNodeFromCords(3, 3, DOMTable)?.__key || '', + } + initializeUnitTest( + (testEnv) => { + beforeEach(async () => { + const {editor} = testEnv; + await editor.update(() => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + root.append(paragraph); + paragraph.select(); + }); + }); + + test('TableNode.constructor', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const tableNode = $createTableNode(); + + expect(tableNode).not.toBe(null); + }); + + expect(() => $createTableNode()).toThrow(); + }); + + test('TableNode.createDOM()', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const tableNode = $createTableNode(); + + expectTableHtmlToBeEqual( + tableNode.createDOM(editorConfig).outerHTML, + html` + + +
+ `, ); - $setSelection(selection); - editor.dispatchCommand(CUT_COMMAND, { - preventDefault: () => {}, - stopPropagation: () => {}, - } as ClipboardEvent); - } - } - }); - - expect(testEnv.innerHTML).toBe(`


`); - }); - - test('Cut subsection of table cells, should just clear contents', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const table = $createTableNodeWithDimensions(4, 4, true); - root.append(table); - }); - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - if (table) { - const DOMTable = $getElementForTableNode(editor, table); - if (DOMTable) { - table - ?.getCellNodeFromCords(0, 0, DOMTable) - ?.getLastChild() - ?.append($createTextNode('some text')); + }); + }); + + test('Copy table from an external source', async () => { + const {editor} = testEnv; + + const dataTransfer = new DataTransferMock(); + dataTransfer.setData( + 'text/html', + '

Hello there

General Kenobi!

Lexical is nice


', + ); + await editor.update(() => { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection), + 'isRangeSelection(selection)', + ); + $insertDataTransferForRichText(dataTransfer, selection, editor); + }); + // Make sure paragraph is inserted inside empty cells + const emptyCell = '


+ + + + + + + + + + + ${emptyCell} + +
+

+ Hello there +

+
+

+ General Kenobi! +

+
+

+ Lexical is nice +

+
+ `, + ); + }); + + test('Copy table from an external source like gdoc with formatting', async () => { + const {editor} = testEnv; + + const dataTransfer = new DataTransferMock(); + dataTransfer.setData( + 'text/html', + '
SurfaceMWP_WORK_LS_COMPOSER77349
LexicalXDS_RICH_TEXT_AREAsdvd sdfvsfs
', + ); + await editor.update(() => { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection), + 'isRangeSelection(selection)', + ); + $insertDataTransferForRichText(dataTransfer, selection, editor); + }); + expectTableHtmlToBeEqual( + testEnv.innerHTML, + html` + + + + + + + + + + + + + + + + +
+

+ Surface +

+
+

+ MWP_WORK_LS_COMPOSER +

+
+

+ 77349 +

+
+

+ Lexical +

+
+

+ XDS_RICH_TEXT_AREA +

+
+

+ sdvd + sdfvsfs +

+
+ `, + ); + }); + + test('Cut table in the middle of a range selection', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const paragraph = root.getFirstChild(); + const beforeText = $createTextNode('text before the table'); + const table = $createTableNodeWithDimensions(4, 4, true); + const afterText = $createTextNode('text after the table'); + + paragraph?.append(beforeText); + paragraph?.append(table); + paragraph?.append(afterText); + }); + await editor.update(() => { + editor.focus(); + $selectAll(); + }); + await editor.update(() => { + editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); + }); + + expectHtmlToBeEqual( + testEnv.innerHTML, + html` +


+ `, + ); + }); + + test('Cut table as last node in range selection ', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const paragraph = root.getFirstChild(); + const beforeText = $createTextNode('text before the table'); + const table = $createTableNodeWithDimensions(4, 4, true); + + paragraph?.append(beforeText); + paragraph?.append(table); + }); + await editor.update(() => { + editor.focus(); + $selectAll(); + }); + await editor.update(() => { + editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); + }); + + expectHtmlToBeEqual( + testEnv.innerHTML, + html` +


+ `, + ); + }); + + test('Cut table as first node in range selection ', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const paragraph = root.getFirstChild(); + const table = $createTableNodeWithDimensions(4, 4, true); + const afterText = $createTextNode('text after the table'); + + paragraph?.append(table); + paragraph?.append(afterText); + }); + await editor.update(() => { + editor.focus(); + $selectAll(); + }); + await editor.update(() => { + editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent); + }); + + expectHtmlToBeEqual( + testEnv.innerHTML, + html` +


+ `, + ); + }); + + test('Cut table is whole selection, should remove it', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 4, true); + root.append(table); + }); + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + const DOMTable = $getElementForTableNode(editor, table); + if (DOMTable) { + table + ?.getCellNodeFromCords(0, 0, DOMTable) + ?.getLastChild() + ?.append($createTextNode('some text')); + const selection = $createTableSelection(); + selection.set( + table.__key, + table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + table?.getCellNodeFromCords(3, 3, DOMTable)?.__key || '', + ); + $setSelection(selection); + editor.dispatchCommand(CUT_COMMAND, { + preventDefault: () => {}, + stopPropagation: () => {}, + } as ClipboardEvent); + } + } + }); + + expectHtmlToBeEqual( + testEnv.innerHTML, + html` +


+ `, + ); + }); + + test('Cut subsection of table cells, should just clear contents', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 4, true); + root.append(table); + }); + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + const DOMTable = $getElementForTableNode(editor, table); + if (DOMTable) { + table + ?.getCellNodeFromCords(0, 0, DOMTable) + ?.getLastChild() + ?.append($createTextNode('some text')); + const selection = $createTableSelection(); + selection.set( + table.__key, + table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + table?.getCellNodeFromCords(2, 2, DOMTable)?.__key || '', + ); + $setSelection(selection); + editor.dispatchCommand(CUT_COMMAND, { + preventDefault: () => {}, + stopPropagation: () => {}, + } as ClipboardEvent); + } + } + }); + + expectTableHtmlToBeEqual( + testEnv.innerHTML, + html` +


+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+ `, + ); + }); + + test('Table plain text output validation', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 4, true); + root.append(table); + }); + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + const DOMTable = $getElementForTableNode(editor, table); + if (DOMTable) { + table + ?.getCellNodeFromCords(0, 0, DOMTable) + ?.getLastChild() + ?.append($createTextNode('1')); + table + ?.getCellNodeFromCords(1, 0, DOMTable) + ?.getLastChild() + ?.append($createTextNode('')); + table + ?.getCellNodeFromCords(2, 0, DOMTable) + ?.getLastChild() + ?.append($createTextNode('2')); + table + ?.getCellNodeFromCords(0, 1, DOMTable) + ?.getLastChild() + ?.append($createTextNode('3')); + table + ?.getCellNodeFromCords(1, 1, DOMTable) + ?.getLastChild() + ?.append($createTextNode('4')); + table + ?.getCellNodeFromCords(2, 1, DOMTable) + ?.getLastChild() + ?.append($createTextNode('')); + const selection = $createTableSelection(); + selection.set( + table.__key, + table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + table?.getCellNodeFromCords(2, 1, DOMTable)?.__key || '', + ); + expect(selection.getTextContent()).toBe(`1\t\t2\n3\t4\t\n`); + } + } + }); + }); + + test('Toggle row striping ON/OFF', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 4, true); + root.append(table); + }); + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + table.setRowStriping(true); + } + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + + + +
+ `, + ); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + if (table) { + table.setRowStriping(false); + } + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + + + +
+ `, + ); + }); + }); + + test('Update column widths', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(4, 2, true); + root.append(table); + }); + + // Set widths + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + table!.setColWidths([50, 50]); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + +
+ `, + ); + const colWidths = table!.getColWidths(); + + // colwidths should be immutable in DEV + expect(() => { + (colWidths as number[]).push(100); + }).toThrow(); + expect(table!.getColWidths()).toStrictEqual([50, 50]); + expect(table!.getColumnCount()).toBe(2); + }); + + // Add a column + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + const DOMTable = $getElementForTableNode(editor, table!); const selection = $createTableSelection(); selection.set( - table.__key, - table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', - table?.getCellNodeFromCords(2, 2, DOMTable)?.__key || '', + table!.__key, + table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', + table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', ); $setSelection(selection); - editor.dispatchCommand(CUT_COMMAND, { - preventDefault: () => {}, - stopPropagation: () => {}, - } as ClipboardEvent); - } - } - }); - - expect(testEnv.innerHTML).toBe( - `


















`, - ); - }); - - test('Table plain text output validation', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const table = $createTableNodeWithDimensions(4, 4, true); - root.append(table); - }); - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - if (table) { - const DOMTable = $getElementForTableNode(editor, table); - if (DOMTable) { - table - ?.getCellNodeFromCords(0, 0, DOMTable) - ?.getLastChild() - ?.append($createTextNode('1')); - table - ?.getCellNodeFromCords(1, 0, DOMTable) - ?.getLastChild() - ?.append($createTextNode('')); - table - ?.getCellNodeFromCords(2, 0, DOMTable) - ?.getLastChild() - ?.append($createTextNode('2')); - table - ?.getCellNodeFromCords(0, 1, DOMTable) - ?.getLastChild() - ?.append($createTextNode('3')); - table - ?.getCellNodeFromCords(1, 1, DOMTable) - ?.getLastChild() - ?.append($createTextNode('4')); - table - ?.getCellNodeFromCords(2, 1, DOMTable) - ?.getLastChild() - ?.append($createTextNode('')); - const selection = $createTableSelection(); - selection.set( - table.__key, - table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', - table?.getCellNodeFromCords(2, 1, DOMTable)?.__key || '', + $insertTableColumn__EXPERIMENTAL(); + table!.setColWidths([50, 50, 100]); + }); + + await editor.update(() => { + const root = $getRoot(); + const table = root.getLastChild(); + expectTableHtmlToBeEqual( + table!.createDOM(editorConfig).outerHTML, + html` + + + + + + +
+ `, ); - expect(selection.getTextContent()).toBe(`1\t\t2\n3\t4\t\n`); - } - } - }); - }); - - test('Toggle row striping ON/OFF', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const table = $createTableNodeWithDimensions(4, 4, true); - root.append(table); - }); - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - if (table) { - table.setRowStriping(true); - } - }); - - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, - ); - }); - - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - if (table) { - table.setRowStriping(false); - } - }); - - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, - ); - }); - }); - - test('Update column widths', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const root = $getRoot(); - const table = $createTableNodeWithDimensions(4, 2, true); - root.append(table); - }); - - // Set widths - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - table!.setColWidths([50, 50]); - }); - - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, - ); - const colWidths = table!.getColWidths(); - - // colwidths should be immutable in DEV - expect(() => { - (colWidths as number[]).push(100); - }).toThrow(); - expect(table!.getColWidths()).toStrictEqual([50, 50]); - expect(table!.getColumnCount()).toBe(2); - }); - - // Add a column - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - const DOMTable = $getElementForTableNode(editor, table!); - const selection = $createTableSelection(); - selection.set( - table!.__key, - table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', - table!.getCellNodeFromCords(0, 0, DOMTable)?.__key || '', - ); - $setSelection(selection); - $insertTableColumn__EXPERIMENTAL(); - table!.setColWidths([50, 50, 100]); - }); - - await editor.update(() => { - const root = $getRoot(); - const table = root.getLastChild(); - expect(table!.createDOM(editorConfig).outerHTML).toBe( - `
`, - ); - expect(table!.getColWidths()).toStrictEqual([50, 50, 100]); - expect(table!.getColumnCount()).toBe(3); - }); - }); - }, - undefined, - , - ); + expect(table!.getColWidths()).toStrictEqual([50, 50, 100]); + expect(table!.getColumnCount()).toBe(3); + }); + }); + }, + {theme: editorConfig.theme}, + , + ); + }); + }); }); diff --git a/packages/lexical-table/src/index.ts b/packages/lexical-table/src/index.ts index 2429eb608a9..be452681b98 100644 --- a/packages/lexical-table/src/index.ts +++ b/packages/lexical-table/src/index.ts @@ -22,11 +22,13 @@ export type {SerializedTableNode} from './LexicalTableNode'; export { $createTableNode, $getElementForTableNode, + $isScrollableTablesActive, $isTableNode, + setScrollableTablesActive, TableNode, } from './LexicalTableNode'; export type {TableDOMCell} from './LexicalTableObserver'; -export {TableObserver} from './LexicalTableObserver'; +export {$getTableAndElementByKey, TableObserver} from './LexicalTableObserver'; export type {SerializedTableRowNode} from './LexicalTableRowNode'; export { $createTableRowNode, @@ -49,6 +51,7 @@ export { $findTableNode, applyTableHandlers, getDOMCellFromTarget, + getTableElement, getTableObserverFromTableElement, } from './LexicalTableSelectionHelpers'; export { diff --git a/packages/lexical-website/docs/react/plugins.md b/packages/lexical-website/docs/react/plugins.md index d2dcb206afb..1e7f60c0294 100644 --- a/packages/lexical-website/docs/react/plugins.md +++ b/packages/lexical-website/docs/react/plugins.md @@ -109,6 +109,8 @@ React wrapper for `@lexical/list` that adds support for check lists. Note that i ### `LexicalTablePlugin` +[![See API Documentation](/img/see-api-documentation.svg)](/docs/api/modules/lexical_react_LexicalTablePlugin) + React wrapper for `@lexical/table` that adds support for tables ```jsx diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index bc32e05bff6..dccc5987079 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -433,6 +433,7 @@ declare export class LexicalNode { selectPrevious(anchorOffset?: number, focusOffset?: number): RangeSelection; selectNext(anchorOffset?: number, focusOffset?: number): RangeSelection; markDirty(): void; + reconcileObservedMutation(dom: HTMLElement, editor: LexicalEditor): void; } export type NodeMap = Map; @@ -790,11 +791,38 @@ declare export class ElementNode extends LexicalNode { nodesToInsert: Array, ): this; exportJSON(): SerializedElementNode; + getDOMSlot(dom: HTMLElement): ElementDOMSlot; } declare export function $isElementNode( node: ?LexicalNode, ): node is ElementNode; +/** + * ElementDOMSlot + */ +declare export class ElementDOMSlot { + element: HTMLElement; + before: Node | null; + after: Node | null; + constructor(element: HTMLElement, before?: Node | null | void, after?: Node | null | void): void; + withBefore(before: Node | null | void): ElementDOMSlot; + withAfter(after: Node | null | void): ElementDOMSlot; + withElement(element: HTMLElement): ElementDOMSlot; + insertChild(dom: Node): this; + removeChild(dom: Node): this; + replaceChild(dom: Node, prevDom: Node): this; + getFirstChild(): Node | null; + // + getManagedLineBreak(): HTMLElement | null; + removeManagedLineBreak(): void; + insertManagedLineBreak(webkitHack: boolean): void; + getFirstChildOffset(): number; + resolveChildIndex(element: ElementNode, elementDOM: HTMLElement, initialDOM: Node, initialOffset: number): [node: ElementNode, idx: number]; +} + +declare export function setDOMUnmanaged(elementDOM: HTMLElement): void; +declare export function isDOMUnmanaged(elementDOM: HTMLElement): boolean; + /** * LexicalDecoratorNode */ diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 6016ae84956..174b18cb62c 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -144,7 +144,9 @@ export type EditorThemeClasses = { tableCellSortedIndicator?: EditorThemeClassName; tableResizeRuler?: EditorThemeClassName; tableRow?: EditorThemeClassName; + tableScrollableWrapper?: EditorThemeClassName; tableSelected?: EditorThemeClassName; + tableSelection?: EditorThemeClassName; text?: TextNodeThemeClasses; embedBlock?: { base?: EditorThemeClassName; diff --git a/packages/lexical/src/LexicalMutations.ts b/packages/lexical/src/LexicalMutations.ts index 15e4e510d39..fa58ebde193 100644 --- a/packages/lexical/src/LexicalMutations.ts +++ b/packages/lexical/src/LexicalMutations.ts @@ -6,8 +6,10 @@ * */ -import type {TextNode} from '.'; +import type {LexicalNode, TextNode} from '.'; import type {LexicalEditor} from './LexicalEditor'; +import type {EditorState} from './LexicalEditorState'; +import type {LexicalPrivateDOM} from './LexicalNode'; import type {BaseSelection} from './LexicalSelection'; import {IS_FIREFOX} from 'shared/environment'; @@ -15,7 +17,6 @@ import {IS_FIREFOX} from 'shared/environment'; import { $getSelection, $isDecoratorNode, - $isElementNode, $isRangeSelection, $isTextNode, $setSelection, @@ -23,12 +24,15 @@ import { import {DOM_TEXT_TYPE} from './LexicalConstants'; import {updateEditor} from './LexicalUpdates'; import { - $getNearestNodeFromDOMNode, + $getNodeByKey, $getNodeFromDOMNode, $updateTextNodeFromDOMContent, getDOMSelection, + getNodeKeyFromDOMNode, + getParentElement, getWindow, internalGetRoot, + isDOMUnmanaged, isFirefoxClipboardEvents, } from './LexicalUtils'; // The time between a text entry event and the mutation observer firing. @@ -53,14 +57,16 @@ function initTextEntryListener(editor: LexicalEditor): void { function isManagedLineBreak( dom: Node, - target: Node, + target: Node & LexicalPrivateDOM, editor: LexicalEditor, ): boolean { + const isBR = dom.nodeName === 'BR'; + const lexicalLineBreak = target.__lexicalLineBreak; return ( - // @ts-expect-error: internal field - target.__lexicalLineBreak === dom || - // @ts-ignore We intentionally add this to the Node. - dom[`__lexicalKey_${editor._key}`] !== undefined + (lexicalLineBreak && + (dom === lexicalLineBreak || + (isBR && dom.previousSibling === lexicalLineBreak))) || + (isBR && getNodeKeyFromDOMNode(dom, editor) !== undefined) ); } @@ -108,6 +114,30 @@ function shouldUpdateTextNodeFromMutation( return targetDOM.nodeType === DOM_TEXT_TYPE && targetNode.isAttached(); } +function $getNearestManagedNodePairFromDOMNode( + startingDOM: Node, + editor: LexicalEditor, + editorState: EditorState, + rootElement: HTMLElement | null, +): [HTMLElement, LexicalNode] | undefined { + for ( + let dom: Node | null = startingDOM; + dom && !isDOMUnmanaged(dom); + dom = getParentElement(dom) + ) { + const key = getNodeKeyFromDOMNode(dom, editor); + if (key !== undefined) { + const node = $getNodeByKey(key, editorState); + if (node) { + // All decorator nodes are unmanaged + return $isDecoratorNode(node) ? undefined : [dom as HTMLElement, node]; + } + } else if (dom === rootElement) { + return [rootElement, internalGetRoot(editorState)]; + } + } +} + export function $flushMutations( editor: LexicalEditor, mutations: Array, @@ -120,7 +150,7 @@ export function $flushMutations( try { updateEditor(editor, () => { const selection = $getSelection() || getLastSelection(editor); - const badDOMTargets = new Map(); + const badDOMTargets = new Map(); const rootElement = editor.getRootElement(); // We use the current editor state, as that reflects what is // actually "on screen". @@ -133,17 +163,16 @@ export function $flushMutations( const mutation = mutations[i]; const type = mutation.type; const targetDOM = mutation.target; - let targetNode = $getNearestNodeFromDOMNode( + const pair = $getNearestManagedNodePairFromDOMNode( targetDOM, + editor, currentEditorState, + rootElement, ); - - if ( - (targetNode === null && targetDOM !== rootElement) || - $isDecoratorNode(targetNode) - ) { + if (!pair) { continue; } + const [nodeDOM, targetNode] = pair; if (type === 'characterData') { // Text mutations are deferred and passed to mutation listeners to be @@ -176,8 +205,7 @@ export function $flushMutations( parentDOM != null && addedDOM !== blockCursorElement && node === null && - (addedDOM.nodeName !== 'BR' || - !isManagedLineBreak(addedDOM, parentDOM, editor)) + !isManagedLineBreak(addedDOM, parentDOM, editor) ) { if (IS_FIREFOX) { const possibleText = @@ -202,8 +230,7 @@ export function $flushMutations( const removedDOM = removedDOMs[s]; if ( - (removedDOM.nodeName === 'BR' && - isManagedLineBreak(removedDOM, targetDOM, editor)) || + isManagedLineBreak(removedDOM, targetDOM, editor) || blockCursorElement === removedDOM ) { targetDOM.appendChild(removedDOM); @@ -212,11 +239,7 @@ export function $flushMutations( } if (removedDOMsLength !== unremovedBRs) { - if (targetDOM === rootElement) { - targetNode = internalGetRoot(currentEditorState); - } - - badDOMTargets.set(targetDOM, targetNode); + badDOMTargets.set(nodeDOM, targetNode); } } } @@ -227,31 +250,8 @@ export function $flushMutations( // is Lexical's "current" editor state. This is basically like // an internal revert on the DOM. if (badDOMTargets.size > 0) { - for (const [targetDOM, targetNode] of badDOMTargets) { - if ($isElementNode(targetNode)) { - const childKeys = targetNode.getChildrenKeys(); - let currentDOM = targetDOM.firstChild; - - for (let s = 0; s < childKeys.length; s++) { - const key = childKeys[s]; - const correctDOM = editor.getElementByKey(key); - - if (correctDOM === null) { - continue; - } - - if (currentDOM == null) { - targetDOM.appendChild(correctDOM); - currentDOM = correctDOM; - } else if (currentDOM !== correctDOM) { - targetDOM.replaceChild(correctDOM, currentDOM); - } - - currentDOM = currentDOM.nextSibling; - } - } else if ($isTextNode(targetNode)) { - targetNode.markDirty(); - } + for (const [nodeDOM, targetNode] of badDOMTargets) { + targetNode.reconcileObservedMutation(nodeDOM, editor); } } diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 564989cdc2e..0aa1d1ca487 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -56,6 +56,15 @@ export type SerializedLexicalNode = { version: number; }; +/** @internal */ +export interface LexicalPrivateDOM { + __lexicalTextContent?: string | undefined | null; + __lexicalLineBreak?: HTMLBRElement | HTMLImageElement | undefined | null; + __lexicalDirTextContent?: string | undefined | null; + __lexicalDir?: 'ltr' | 'rtl' | null | undefined; + __lexicalUnmanaged?: boolean | undefined; +} + export function $removeNode( nodeToRemove: LexicalNode, restoreSelection: boolean, @@ -1160,6 +1169,16 @@ export class LexicalNode { markDirty(): void { this.getWritable(); } + + /** + * @internal + * + * When the reconciler detects that a node was mutated, this method + * may be called to restore the node to a known good state. + */ + reconcileObservedMutation(dom: HTMLElement, editor: LexicalEditor): void { + this.markDirty(); + } } function errorOnTypeKlassMismatch( diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 0ad9cf2c911..6fa946f61d6 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -13,8 +13,8 @@ import type { MutationListeners, RegisteredNodes, } from './LexicalEditor'; -import type {NodeKey, NodeMap} from './LexicalNode'; -import type {ElementNode} from './nodes/LexicalElementNode'; +import type {LexicalPrivateDOM, NodeKey, NodeMap} from './LexicalNode'; +import type {ElementDOMSlot, ElementNode} from './nodes/LexicalElementNode'; import invariant from 'shared/invariant'; import normalizeClassNames from 'shared/normalizeClassNames'; @@ -44,6 +44,7 @@ import { getElementByKeyOrThrow, getTextDirection, setMutatedNode, + setNodeKeyOnDOMNode, } from './LexicalUtils'; type IntentionallyMarkedAsDirtyElement = boolean; @@ -165,11 +166,7 @@ function setElementFormat(dom: HTMLElement, format: number): void { } } -function $createNode( - key: NodeKey, - parentDOM: null | HTMLElement, - insertDOM: null | Node, -): HTMLElement { +function $createNode(key: NodeKey, slot: ElementDOMSlot | null): HTMLElement { const node = activeNextNodeMap.get(key); if (node === undefined) { @@ -231,19 +228,8 @@ function $createNode( editorTextContent += text; } - if (parentDOM !== null) { - if (insertDOM != null) { - parentDOM.insertBefore(dom, insertDOM); - } else { - // @ts-expect-error: internal field - const possibleLineBreak = parentDOM.__lexicalLineBreak; - - if (possibleLineBreak != null) { - parentDOM.insertBefore(dom, possibleLineBreak); - } else { - parentDOM.appendChild(dom); - } - } + if (slot !== null) { + slot.insertChild(dom); } if (__DEV__) { @@ -269,25 +255,24 @@ function $createChildrenWithDirection( ): void { const previousSubTreeDirectionedTextContent = subTreeDirectionedTextContent; subTreeDirectionedTextContent = ''; - $createChildren(children, element, 0, endIndex, dom, null); + $createChildren(children, element, 0, endIndex, element.getDOMSlot(dom)); reconcileBlockDirection(element, dom); subTreeDirectionedTextContent = previousSubTreeDirectionedTextContent; } function $createChildren( children: Array, - element: ElementNode, + element: ElementNode & LexicalPrivateDOM, _startIndex: number, endIndex: number, - dom: null | HTMLElement, - insertDOM: null | HTMLElement, + slot: ElementDOMSlot, ): void { const previousSubTreeTextContent = subTreeTextContent; subTreeTextContent = ''; let startIndex = _startIndex; for (; startIndex <= endIndex; ++startIndex) { - $createNode(children[startIndex], dom, insertDOM); + $createNode(children[startIndex], slot); const node = activeNextNodeMap.get(children[startIndex]); if (node !== null && $isTextNode(node)) { if (subTreeTextFormat === null) { @@ -301,67 +286,49 @@ function $createChildren( if ($textContentRequiresDoubleLinebreakAtEnd(element)) { subTreeTextContent += DOUBLE_LINE_BREAK; } - // @ts-expect-error: internal field + const dom: HTMLElement & LexicalPrivateDOM = slot.element; dom.__lexicalTextContent = subTreeTextContent; subTreeTextContent = previousSubTreeTextContent + subTreeTextContent; } +type LastChildState = 'line-break' | 'decorator' | 'empty'; function isLastChildLineBreakOrDecorator( - childKey: NodeKey, + element: null | ElementNode, nodeMap: NodeMap, -): boolean { - const node = nodeMap.get(childKey); - return $isLineBreakNode(node) || ($isDecoratorNode(node) && node.isInline()); +): null | LastChildState { + if (element) { + const lastKey = element.__last; + if (lastKey) { + const node = nodeMap.get(lastKey); + if (node) { + return $isLineBreakNode(node) + ? 'line-break' + : $isDecoratorNode(node) && node.isInline() + ? 'decorator' + : null; + } + } + return 'empty'; + } + return null; } // If we end an element with a LineBreakNode, then we need to add an additional
function reconcileElementTerminatingLineBreak( prevElement: null | ElementNode, nextElement: ElementNode, - dom: HTMLElement, + dom: HTMLElement & LexicalPrivateDOM, ): void { - const prevLineBreak = - prevElement !== null && - (prevElement.__size === 0 || - isLastChildLineBreakOrDecorator( - prevElement.__last as NodeKey, - activePrevNodeMap, - )); - const nextLineBreak = - nextElement.__size === 0 || - isLastChildLineBreakOrDecorator( - nextElement.__last as NodeKey, - activeNextNodeMap, - ); - - if (prevLineBreak) { - if (!nextLineBreak) { - // @ts-expect-error: internal field - const element = dom.__lexicalLineBreak; - - if (element != null) { - try { - dom.removeChild(element); - } catch (error) { - if (typeof error === 'object' && error != null) { - const msg = `${error.toString()} Parent: ${dom.tagName}, child: ${ - element.tagName - }.`; - throw new Error(msg); - } else { - throw error; - } - } - } - - // @ts-expect-error: internal field - dom.__lexicalLineBreak = null; - } - } else if (nextLineBreak) { - const element = document.createElement('br'); - // @ts-expect-error: internal field - dom.__lexicalLineBreak = element; - dom.appendChild(element); + const prevLineBreak = isLastChildLineBreakOrDecorator( + prevElement, + activePrevNodeMap, + ); + const nextLineBreak = isLastChildLineBreakOrDecorator( + nextElement, + activeNextNodeMap, + ); + if (prevLineBreak !== nextLineBreak) { + nextElement.getDOMSlot(dom).setManagedLineBreak(nextLineBreak); } } @@ -388,12 +355,13 @@ function reconcileParagraphStyle(element: ElementNode): void { } } -function reconcileBlockDirection(element: ElementNode, dom: HTMLElement): void { +function reconcileBlockDirection( + element: ElementNode, + dom: HTMLElement & LexicalPrivateDOM, +): void { const previousSubTreeDirectionTextContent: string = - // @ts-expect-error: internal field - dom.__lexicalDirTextContent; - // @ts-expect-error: internal field - const previousDirection: string = dom.__lexicalDir; + dom.__lexicalDirTextContent || ''; + const previousDirection: string = dom.__lexicalDir || ''; if ( previousSubTreeDirectionTextContent !== subTreeDirectionedTextContent || @@ -454,9 +422,7 @@ function reconcileBlockDirection(element: ElementNode, dom: HTMLElement): void { } activeTextDirection = direction; - // @ts-expect-error: internal field dom.__lexicalDirTextContent = subTreeDirectionedTextContent; - // @ts-expect-error: internal field dom.__lexicalDir = direction; } } @@ -470,7 +436,7 @@ function $reconcileChildrenWithDirection( subTreeDirectionedTextContent = ''; subTreeTextFormat = null; subTreeTextStyle = ''; - $reconcileChildren(prevElement, nextElement, dom); + $reconcileChildren(prevElement, nextElement, nextElement.getDOMSlot(dom)); reconcileBlockDirection(nextElement, dom); reconcileParagraphFormat(nextElement); reconcileParagraphStyle(nextElement); @@ -497,21 +463,22 @@ function createChildrenArray( function $reconcileChildren( prevElement: ElementNode, nextElement: ElementNode, - dom: HTMLElement, + slot: ElementDOMSlot, ): void { const previousSubTreeTextContent = subTreeTextContent; const prevChildrenSize = prevElement.__size; const nextChildrenSize = nextElement.__size; subTreeTextContent = ''; + const dom: HTMLElement & LexicalPrivateDOM = slot.element; if (prevChildrenSize === 1 && nextChildrenSize === 1) { - const prevFirstChildKey = prevElement.__first as NodeKey; - const nextFrstChildKey = nextElement.__first as NodeKey; - if (prevFirstChildKey === nextFrstChildKey) { + const prevFirstChildKey: NodeKey = prevElement.__first!; + const nextFirstChildKey: NodeKey = nextElement.__first!; + if (prevFirstChildKey === nextFirstChildKey) { $reconcileNode(prevFirstChildKey, dom); } else { const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey); - const replacementDOM = $createNode(nextFrstChildKey, null, null); + const replacementDOM = $createNode(nextFirstChildKey, null); try { dom.replaceChild(replacementDOM, lastDOM); } catch (error) { @@ -520,7 +487,7 @@ function $reconcileChildren( dom.tagName }, new child: {tag: ${ replacementDOM.tagName - } key: ${nextFrstChildKey}}, old child: {tag: ${ + } key: ${nextFirstChildKey}}, old child: {tag: ${ lastDOM.tagName }, key: ${prevFirstChildKey}}.`; throw new Error(msg); @@ -530,7 +497,7 @@ function $reconcileChildren( } destroyNode(prevFirstChildKey, null); } - const nextChildNode = activeNextNodeMap.get(nextFrstChildKey); + const nextChildNode = activeNextNodeMap.get(nextFirstChildKey); if ($isTextNode(nextChildNode)) { if (subTreeTextFormat === null) { subTreeTextFormat = nextChildNode.getFormat(); @@ -542,6 +509,14 @@ function $reconcileChildren( } else { const prevChildren = createChildrenArray(prevElement, activePrevNodeMap); const nextChildren = createChildrenArray(nextElement, activeNextNodeMap); + invariant( + prevChildren.length === prevChildrenSize, + '$reconcileChildren: prevChildren.length !== prevChildrenSize', + ); + invariant( + nextChildren.length === nextChildrenSize, + '$reconcileChildren: nextChildren.length !== nextChildrenSize', + ); if (prevChildrenSize === 0) { if (nextChildrenSize !== 0) { @@ -550,15 +525,16 @@ function $reconcileChildren( nextElement, 0, nextChildrenSize - 1, - dom, - null, + slot, ); } } else if (nextChildrenSize === 0) { if (prevChildrenSize !== 0) { - // @ts-expect-error: internal field - const lexicalLineBreak = dom.__lexicalLineBreak; - const canUseFastPath = lexicalLineBreak == null; + const canUseFastPath = + slot.after == null && + slot.before == null && + (slot.element as HTMLElement & LexicalPrivateDOM) + .__lexicalLineBreak == null; destroyChildren( prevChildren, 0, @@ -578,7 +554,7 @@ function $reconcileChildren( nextChildren, prevChildrenSize, nextChildrenSize, - dom, + slot, ); } } @@ -587,7 +563,6 @@ function $reconcileChildren( subTreeTextContent += DOUBLE_LINE_BREAK; } - // @ts-expect-error: internal field dom.__lexicalTextContent = subTreeTextContent; subTreeTextContent = previousSubTreeTextContent + subTreeTextContent; } @@ -610,14 +585,16 @@ function $reconcileNode( treatAllNodesAsDirty || activeDirtyLeaves.has(key) || activeDirtyElements.has(key); - const dom = getElementByKeyOrThrow(activeEditor, key); + const dom: HTMLElement & LexicalPrivateDOM = getElementByKeyOrThrow( + activeEditor, + key, + ); // If the node key points to the same instance in both states // and isn't dirty, we just update the text content cache // and return the existing DOM Node. if (prevNode === nextNode && !isDirty) { if ($isElementNode(prevNode)) { - // @ts-expect-error: internal field const previousSubTreeTextContent = dom.__lexicalTextContent; if (previousSubTreeTextContent !== undefined) { @@ -625,7 +602,6 @@ function $reconcileNode( editorTextContent += previousSubTreeTextContent; } - // @ts-expect-error: internal field const previousSubTreeDirectionTextContent = dom.__lexicalDirTextContent; if (previousSubTreeDirectionTextContent !== undefined) { @@ -658,7 +634,7 @@ function $reconcileNode( // Update node. If it returns true, we need to unmount and re-create the node if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) { - const replacementDOM = $createNode(key, null, null); + const replacementDOM = $createNode(key, null); if (parentDOM === null) { invariant(false, 'reconcileNode: parentDOM is null'); @@ -745,10 +721,6 @@ function reconcileDecorator(key: NodeKey, decorator: unknown): void { pendingDecorators[key] = decorator; } -function getFirstChild(element: HTMLElement): Node | null { - return element.firstChild; -} - function getNextSibling(element: HTMLElement): Node | null { let nextSibling = element.nextSibling; if ( @@ -766,13 +738,13 @@ function $reconcileNodeChildren( nextChildren: Array, prevChildrenLength: number, nextChildrenLength: number, - dom: HTMLElement, + slot: ElementDOMSlot, ): void { const prevEndIndex = prevChildrenLength - 1; const nextEndIndex = nextChildrenLength - 1; let prevChildrenSet: Set | undefined; let nextChildrenSet: Set | undefined; - let siblingDOM: null | Node = getFirstChild(dom); + let siblingDOM: null | Node = slot.getFirstChild(); let prevIndex = 0; let nextIndex = 0; @@ -781,7 +753,7 @@ function $reconcileNodeChildren( const nextKey = nextChildren[nextIndex]; if (prevKey === nextKey) { - siblingDOM = getNextSibling($reconcileNode(nextKey, dom)); + siblingDOM = getNextSibling($reconcileNode(nextKey, slot.element)); prevIndex++; nextIndex++; } else { @@ -799,26 +771,21 @@ function $reconcileNodeChildren( if (!nextHasPrevKey) { // Remove prev siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey)); - destroyNode(prevKey, dom); + destroyNode(prevKey, slot.element); prevIndex++; } else if (!prevHasNextKey) { // Create next - $createNode(nextKey, dom, siblingDOM); + $createNode(nextKey, slot.withBefore(siblingDOM)); nextIndex++; } else { // Move next const childDOM = getElementByKeyOrThrow(activeEditor, nextKey); if (childDOM === siblingDOM) { - siblingDOM = getNextSibling($reconcileNode(nextKey, dom)); + siblingDOM = getNextSibling($reconcileNode(nextKey, slot.element)); } else { - if (siblingDOM != null) { - dom.insertBefore(childDOM, siblingDOM); - } else { - dom.appendChild(childDOM); - } - - $reconcileNode(nextKey, dom); + slot.withBefore(siblingDOM).insertChild(childDOM); + $reconcileNode(nextKey, slot.element); } prevIndex++; @@ -851,11 +818,10 @@ function $reconcileNodeChildren( nextElement, nextIndex, nextEndIndex, - dom, - insertDOM, + slot.withBefore(insertDOM), ); } else if (removeOldChildren && !appendNewChildren) { - destroyChildren(prevChildren, prevIndex, prevEndIndex, dom); + destroyChildren(prevChildren, prevIndex, prevEndIndex, slot.element); } } @@ -923,8 +889,7 @@ export function storeDOMWithKey( editor: LexicalEditor, ): void { const keyToDOMMap = editor._keyToDOMMap; - // @ts-ignore We intentionally add this to the Node. - dom['__lexicalKey_' + editor._key] = key; + setNodeKeyOnDOMNode(dom, editor, key); keyToDOMMap.set(key, dom); } diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 3af4b30d0db..fb7a62dbea0 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -2109,10 +2109,29 @@ function $internalResolveSelectionPoint( return null; } if ($isElementNode(resolvedElement)) { - resolvedOffset = Math.min( - resolvedElement.getChildrenSize(), - resolvedOffset, + const elementDOM = editor.getElementByKey(resolvedElement.getKey()); + invariant( + elementDOM !== null, + '$internalResolveSelectionPoint: node in DOM but not keyToDOMMap', ); + const slot = resolvedElement.getDOMSlot(elementDOM); + [resolvedElement, resolvedOffset] = slot.resolveChildIndex( + resolvedElement, + elementDOM, + dom, + offset, + ); + // This is just a typescript workaround, it is true but lost due to mutability + invariant( + $isElementNode(resolvedElement), + '$internalResolveSelectionPoint: resolvedElement is not an ElementNode', + ); + if ( + moveSelectionToEnd && + resolvedOffset >= resolvedElement.getChildrenSize() + ) { + resolvedOffset = Math.max(0, resolvedElement.getChildrenSize() - 1); + } let child = resolvedElement.getChildAtIndex(resolvedOffset); if ( $isElementNode(child) && @@ -2140,7 +2159,11 @@ function $internalResolveSelectionPoint( moveSelectionToEnd && !hasBlockCursor ) { - resolvedOffset++; + invariant($isElementNode(resolvedElement), 'invariant'); + resolvedOffset = Math.min( + resolvedElement.getChildrenSize(), + resolvedOffset + 1, + ); } } else { const index = resolvedElement.getIndexWithinParent(); @@ -2297,6 +2320,9 @@ function $internalResolveSelectionPoints( if (resolvedAnchorPoint === null) { return null; } + if (__DEV__) { + $validatePoint(editor, 'anchor', resolvedAnchorPoint); + } const resolvedFocusPoint = $internalResolveSelectionPoint( focusDOM, focusOffset, @@ -2306,6 +2332,9 @@ function $internalResolveSelectionPoints( if (resolvedFocusPoint === null) { return null; } + if (__DEV__) { + $validatePoint(editor, 'focus', resolvedAnchorPoint); + } if ( resolvedAnchorPoint.type === 'element' && resolvedFocusPoint.type === 'element' @@ -2475,6 +2504,51 @@ export function $internalCreateRangeSelection( ); } +function $validatePoint( + editor: LexicalEditor, + name: 'anchor' | 'focus', + point: PointType, +): void { + const node = $getNodeByKey(point.key); + invariant( + node !== undefined, + '$validatePoint: %s key %s not found in current editorState', + name, + point.key, + ); + if (point.type === 'text') { + invariant( + $isTextNode(node), + '$validatePoint: %s key %s is not a TextNode', + name, + point.key, + ); + const size = node.getTextContentSize(); + invariant( + point.offset <= size, + '$validatePoint: %s point.offset > node.getTextContentSize() (%s > %s)', + name, + String(point.offset), + String(size), + ); + } else { + invariant( + $isElementNode(node), + '$validatePoint: %s key %s is not an ElementNode', + name, + point.key, + ); + const size = node.getChildrenSize(); + invariant( + point.offset <= size, + '$validatePoint: %s point.offset > node.getChildrenSize() (%s > %s)', + name, + String(point.offset), + String(size), + ); + } +} + export function $getSelection(): null | BaseSelection { const editorState = getActiveEditorState(); return editorState._selection; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index b1a409a9f36..a4a3af63118 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -20,7 +20,12 @@ import type { Spread, } from './LexicalEditor'; import type {EditorState} from './LexicalEditorState'; -import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode'; +import type { + LexicalNode, + LexicalPrivateDOM, + NodeKey, + NodeMap, +} from './LexicalNode'; import type { BaseSelection, PointType, @@ -441,14 +446,30 @@ export function $getNodeFromDOMNode( editorState?: EditorState, ): LexicalNode | null { const editor = getActiveEditor(); - // @ts-ignore We intentionally add this to the Node. - const key = dom[`__lexicalKey_${editor._key}`]; + const key = getNodeKeyFromDOMNode(dom, editor); if (key !== undefined) { return $getNodeByKey(key, editorState); } return null; } +export function setNodeKeyOnDOMNode( + dom: Node, + editor: LexicalEditor, + key: NodeKey, +) { + const prop = `__lexicalKey_${editor._key}`; + (dom as Node & Record)[prop] = key; +} + +export function getNodeKeyFromDOMNode( + dom: Node, + editor: LexicalEditor, +): NodeKey | undefined { + const prop = `__lexicalKey_${editor._key}`; + return (dom as Node & Record)[prop]; +} + export function $getNearestNodeFromDOMNode( startingDOM: Node, editorState?: EditorState, @@ -537,7 +558,7 @@ export function $flushMutations(): void { export function $getNodeFromDOM(dom: Node): null | LexicalNode { const editor = getActiveEditor(); - const nodeKey = getNodeKeyFromDOM(dom, editor); + const nodeKey = getNodeKeyFromDOMTree(dom, editor); if (nodeKey === null) { const rootElement = editor.getRootElement(); if (dom === rootElement) { @@ -555,15 +576,14 @@ export function getTextNodeOffset( return moveSelectionToEnd ? node.getTextContentSize() : 0; } -function getNodeKeyFromDOM( +function getNodeKeyFromDOMTree( // Note that node here refers to a DOM Node, not an Lexical Node dom: Node, editor: LexicalEditor, ): NodeKey | null { let node: Node | null = dom; while (node != null) { - // @ts-ignore We intentionally add this to the Node. - const key: NodeKey = node[`__lexicalKey_${editor._key}`]; + const key = getNodeKeyFromDOMNode(node, editor); if (key !== undefined) { return key; } @@ -1562,13 +1582,11 @@ export function updateDOMBlockCursorElement( } } else { const child = elementNode.getChildAtIndex(offset); - if (needsBlockCursor(child)) { - const sibling = (child as LexicalNode).getPreviousSibling(); + if (child !== null && needsBlockCursor(child)) { + const sibling = child.getPreviousSibling(); if (sibling === null || needsBlockCursor(sibling)) { isBlockCursor = true; - insertBeforeElement = editor.getElementByKey( - (child as LexicalNode).__key, - ); + insertBeforeElement = editor.getElementByKey(child.__key); } } } @@ -1842,3 +1860,24 @@ export function setNodeIndentFromDOM( const indent = indentSize / 40; elementNode.setIndent(indent); } + +/** + * @internal + * + * Mark this node as unmanaged by lexical's mutation observer like + * decorator nodes + */ +export function setDOMUnmanaged(elementDom: HTMLElement): void { + const el: HTMLElement & LexicalPrivateDOM = elementDom; + el.__lexicalUnmanaged = true; +} + +/** + * @internal + * + * True if this DOM node was marked with {@link setDOMUnmanaged} + */ +export function isDOMUnmanaged(elementDom: Node): boolean { + const el: Node & LexicalPrivateDOM = elementDom; + return el.__lexicalUnmanaged === true; +} diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index 5292fdd5a5f..dff3b2adaee 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -796,8 +796,25 @@ export function html( return output; } -export function expectHtmlToBeEqual(expected: string, actual: string): void { - expect(prettifyHtml(expected)).toBe(prettifyHtml(actual)); +export function polyfillContentEditable() { + const div = document.createElement('div'); + div.contentEditable = 'true'; + if (/contenteditable/.test(div.outerHTML)) { + return; + } + Object.defineProperty(HTMLElement.prototype, 'contentEditable', { + get() { + return this.getAttribute('contenteditable'); + }, + + set(value) { + this.setAttribute('contenteditable', value); + }, + }); +} + +export function expectHtmlToBeEqual(actual: string, expected: string): void { + expect(prettifyHtml(actual)).toBe(prettifyHtml(expected)); } export function prettifyHtml(s: string): string { diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index d5c94e23453..2898a73a9b7 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -58,6 +58,7 @@ export type { TextPointType as TextPoint, } from './LexicalSelection'; export type { + ElementDOMSlot, ElementFormatType, SerializedElementNode, } from './nodes/LexicalElementNode'; @@ -182,6 +183,7 @@ export { getNearestEditorFromDOMNode, isBlockDomNode, isDocumentFragment, + isDOMUnmanaged, isHTMLAnchorElement, isHTMLElement, isInlineDomNode, @@ -189,6 +191,7 @@ export { isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, resetRandomKey, + setDOMUnmanaged, setNodeIndentFromDOM, } from './LexicalUtils'; export {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode'; diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index 65bc77aec5a..285a546545a 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -8,6 +8,7 @@ import type { DOMExportOutput, + LexicalPrivateDOM, NodeKey, SerializedLexicalNode, } from '../LexicalNode'; @@ -18,6 +19,7 @@ import type { } from '../LexicalSelection'; import type {KlassConstructor, LexicalEditor, Spread} from 'lexical'; +import {IS_IOS, IS_SAFARI} from 'shared/environment'; import invariant from 'shared/invariant'; import {$isTextNode, TextNode} from '../index'; @@ -68,6 +70,225 @@ export interface ElementNode { getTopLevelElementOrThrow(): ElementNode; } +/** + * A utility class for managing the DOM children of an ElementNode + */ +export class ElementDOMSlot { + element: HTMLElement; + before: Node | null; + after: Node | null; + constructor( + /** The element returned by createDOM */ + element: HTMLElement, + /** All managed children will be inserted before this node, if defined */ + before?: Node | undefined | null, + /** All managed children will be inserted after this node, if defined */ + after?: Node | undefined | null, + ) { + this.element = element; + this.before = before || null; + this.after = after || null; + } + /** + * Return a new ElementDOMSlot where all managed children will be inserted before this node + */ + withBefore(before: Node | undefined | null): ElementDOMSlot { + return new ElementDOMSlot(this.element, before, this.after); + } + /** + * Return a new ElementDOMSlot where all managed children will be inserted after this node + */ + withAfter(after: Node | undefined | null): ElementDOMSlot { + return new ElementDOMSlot(this.element, this.before, after); + } + /** + * Return a new ElementDOMSlot with an updated root element + */ + withElement(element: HTMLElement): ElementDOMSlot { + return new ElementDOMSlot(element, this.before, this.after); + } + /** + * Insert the given child before this.before and any reconciler managed line break node, + * or append it if this.before is not defined + */ + insertChild(dom: Node): this { + const before = this.before || this.getManagedLineBreak(); + invariant( + before === null || before.parentElement === this.element, + 'ElementDOMSlot.insertChild: before is not in element', + ); + this.element.insertBefore(dom, before); + return this; + } + /** + * Remove the managed child from this container, will throw if it was not already there + */ + removeChild(dom: Node): this { + invariant( + dom.parentElement === this.element, + 'ElementDOMSlot.removeChild: dom is not in element', + ); + this.element.removeChild(dom); + return this; + } + /** + * Replace managed child prevDom with dom. Will throw if prevDom is not a child + * + * @param dom The new node to replace prevDom + * @param prevDom the node that will be replaced + */ + replaceChild(dom: Node, prevDom: Node): this { + invariant( + prevDom.parentElement === this.element, + 'ElementDOMSlot.replaceChild: prevDom is not in element', + ); + this.element.replaceChild(dom, prevDom); + return this; + } + /** + * Returns the first managed child of this node, + * which will either be this.after.nextSibling or this.element.firstChild, + * and will never be this.before if it is defined. + */ + getFirstChild(): ChildNode | null { + const firstChild = this.after + ? this.after.nextSibling + : this.element.firstChild; + return firstChild === this.before || + firstChild === this.getManagedLineBreak() + ? null + : firstChild; + } + /** + * @internal + */ + getManagedLineBreak(): Exclude< + LexicalPrivateDOM['__lexicalLineBreak'], + undefined + > { + const element: HTMLElement & LexicalPrivateDOM = this.element; + return element.__lexicalLineBreak || null; + } + /** @internal */ + setManagedLineBreak( + lineBreakType: null | 'empty' | 'line-break' | 'decorator', + ): void { + if (lineBreakType === null) { + this.removeManagedLineBreak(); + } else { + const webkitHack = lineBreakType === 'decorator' && (IS_IOS || IS_SAFARI); + this.insertManagedLineBreak(webkitHack); + } + } + + /** @internal */ + removeManagedLineBreak(): void { + const br = this.getManagedLineBreak(); + if (br) { + const element: HTMLElement & LexicalPrivateDOM = this.element; + const sibling = br.nodeName === 'IMG' ? br.nextSibling : null; + if (sibling) { + element.removeChild(sibling); + } + element.removeChild(br); + element.__lexicalLineBreak = undefined; + } + } + /** @internal */ + insertManagedLineBreak(webkitHack: boolean): void { + const prevBreak = this.getManagedLineBreak(); + if (prevBreak) { + if (webkitHack === (prevBreak.nodeName === 'IMG')) { + return; + } + this.removeManagedLineBreak(); + } + const element: HTMLElement & LexicalPrivateDOM = this.element; + const before = this.before; + const br = document.createElement('br'); + element.insertBefore(br, before); + if (webkitHack) { + const img = document.createElement('img'); + img.setAttribute('data-lexical-linebreak', 'true'); + img.style.cssText = + 'display: inline !important; border: 0px !important; margin: 0px !important;'; + img.alt = ''; + element.insertBefore(img, br); + element.__lexicalLineBreak = img; + } else { + element.__lexicalLineBreak = br; + } + } + + /** + * @internal + * + * Returns the offset of the first child + */ + getFirstChildOffset(): number { + let i = 0; + for (let node = this.after; node !== null; node = node.previousSibling) { + i++; + } + return i; + } + + /** + * @internal + */ + resolveChildIndex( + element: ElementNode, + elementDOM: HTMLElement, + initialDOM: Node, + initialOffset: number, + ): [node: ElementNode, idx: number] { + if (initialDOM === this.element) { + const firstChildOffset = this.getFirstChildOffset(); + return [ + element, + Math.min( + firstChildOffset + element.getChildrenSize(), + Math.max(firstChildOffset, initialOffset), + ), + ]; + } + // The resolved offset must be before or after the children + const initialPath = indexPath(elementDOM, initialDOM); + initialPath.push(initialOffset); + const elementPath = indexPath(elementDOM, this.element); + let offset = element.getIndexWithinParent(); + for (let i = 0; i < elementPath.length; i++) { + const target = initialPath[i]; + const source = elementPath[i]; + if (target === undefined || target < source) { + break; + } else if (target > source) { + offset += 1; + break; + } + } + return [element.getParentOrThrow(), offset]; + } +} + +function indexPath(root: HTMLElement, child: Node): number[] { + const path: number[] = []; + let node: Node | null = child; + for (; node !== root && node !== null; node = child.parentNode) { + let i = 0; + for ( + let sibling = node.previousSibling; + sibling !== null; + sibling = node.previousSibling + ) { + i++; + } + path.push(i); + } + invariant(node === root, 'indexPath: root is not a parent of child'); + return path.reverse(); +} + /** @noInheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class ElementNode extends LexicalNode { @@ -406,6 +627,13 @@ export class ElementNode extends LexicalNode { const nodesToInsertLength = nodesToInsert.length; const oldSize = this.getChildrenSize(); const writableSelf = this.getWritable(); + invariant( + start + deleteCount <= oldSize, + 'ElementNode.splice: start + deleteCount > oldSize (%s + %s > %s)', + String(start), + String(deleteCount), + String(oldSize), + ); const writableSelfKey = writableSelf.__key; const nodesToInsertKeys = []; const nodesToRemoveKeys = []; @@ -528,6 +756,17 @@ export class ElementNode extends LexicalNode { return writableSelf; } + /** + * @internal + * + * An experimental API that an ElementNode can override to control where its + * children are inserted into the DOM, this is useful to add a wrapping node + * or accessory nodes before or after the children. The root of the node returned + * by createDOM must still be exactly one HTMLElement. + */ + getDOMSlot(element: HTMLElement): ElementDOMSlot { + return new ElementDOMSlot(element); + } exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); if (element && isHTMLElement(element)) { @@ -633,6 +872,32 @@ export class ElementNode extends LexicalNode { canMergeWhenEmpty(): boolean { return false; } + + /** @internal */ + reconcileObservedMutation(dom: HTMLElement, editor: LexicalEditor): void { + const slot = this.getDOMSlot(dom); + let currentDOM = slot.getFirstChild(); + for ( + let currentNode = this.getFirstChild(); + currentNode; + currentNode = currentNode.getNextSibling() + ) { + const correctDOM = editor.getElementByKey(currentNode.getKey()); + + if (correctDOM === null) { + continue; + } + + if (currentDOM == null) { + slot.insertChild(correctDOM); + currentDOM = correctDOM; + } else if (currentDOM !== correctDOM) { + slot.replaceChild(correctDOM, currentDOM); + } + + currentDOM = currentDOM.nextSibling; + } + } } export function $isElementNode( diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalElementNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalElementNode.test.tsx index 21e9ed3c899..6736ce72edc 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalElementNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalElementNode.test.tsx @@ -7,10 +7,13 @@ */ import { + $applyNodeReplacement, $createTextNode, $getRoot, $getSelection, $isRangeSelection, + createEditor, + ElementDOMSlot, ElementNode, LexicalEditor, LexicalNode, @@ -25,6 +28,7 @@ import { $createTestElementNode, createTestEditor, } from '../../../__tests__/utils'; +import {SerializedElementNode} from '../../LexicalElementNode'; describe('LexicalElementNode tests', () => { let container: HTMLElement; @@ -633,3 +637,87 @@ describe('LexicalElementNode tests', () => { }); }); }); + +describe('getDOMSlot tests', () => { + let container: HTMLElement; + let editor: LexicalEditor; + + beforeEach(async () => { + container = document.createElement('div'); + document.body.appendChild(container); + editor = createEditor({ + nodes: [WrapperElementNode], + onError: (error) => { + throw error; + }, + }); + editor.setRootElement(container); + }); + + afterEach(() => { + document.body.removeChild(container); + // @ts-ignore + container = null; + }); + + class WrapperElementNode extends ElementNode { + static getType() { + return 'wrapper'; + } + static clone(node: WrapperElementNode): WrapperElementNode { + return new WrapperElementNode(node.__key); + } + createDOM() { + const el = document.createElement('main'); + el.appendChild(document.createElement('section')); + return el; + } + updateDOM() { + return false; + } + getDOMSlot(dom: HTMLElement): ElementDOMSlot { + return super.getDOMSlot(dom).withElement(dom.querySelector('section')!); + } + exportJSON(): SerializedElementNode { + throw new Error('Not implemented'); + } + static importJSON(): WrapperElementNode { + throw new Error('Not implemented'); + } + } + function $createWrapperElementNode(): WrapperElementNode { + return $applyNodeReplacement(new WrapperElementNode()); + } + + test('can create wrapper', () => { + let wrapper: WrapperElementNode; + editor.update( + () => { + wrapper = $createWrapperElementNode().append( + $createTextNode('test text').setMode('token'), + ); + $getRoot().clear().append(wrapper); + }, + {discrete: true}, + ); + expect(container.innerHTML).toBe( + `
test text
`, + ); + editor.update( + () => { + wrapper.append($createTextNode('more text').setMode('token')); + }, + {discrete: true}, + ); + expect(container.innerHTML).toBe( + `
test textmore text
`, + ); + editor.update( + () => { + wrapper.clear(); + }, + {discrete: true}, + ); + expect(container.innerHTML).toBe(`

`); + }); +}); From c14d676cce17410acece23b2ebae70c45ee0babb Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:47:25 +0100 Subject: [PATCH 089/133] [lexical-examples] Chore: Add DOMExportOutputMap type to the exportMap (#6827) Co-authored-by: Bob Ippolito --- examples/react-rich/package-lock.json | 628 ++++++++++++++------------ examples/react-rich/src/App.tsx | 3 +- 2 files changed, 331 insertions(+), 300 deletions(-) diff --git a/examples/react-rich/package-lock.json b/examples/react-rich/package-lock.json index 34c37ffb987..8033fbb43e2 100644 --- a/examples/react-rich/package-lock.json +++ b/examples/react-rich/package-lock.json @@ -1,15 +1,15 @@ { "name": "@lexical/react-rich-example", - "version": "0.15.0", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/react-rich-example", - "version": "0.15.0", + "version": "0.20.0", "dependencies": { - "@lexical/react": "0.15.0", - "lexical": "0.15.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -789,38 +789,41 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.15.0.tgz", - "integrity": "sha512-binCltK7KiURQJFogvueYfmDNEKynN/lmZrCLFp2xBjEIajqw4WtOVLJZ33engdqNlvj0JqrxrWxbKG+yvUwrg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.20.0.tgz", + "integrity": "sha512-oHmb9kSVHjeFCd2q8VrEXW22doUHMJ6cGXqo7Ican7Ljl4/9OgRWr+cq55yntoSaJfCrRYkTiZCLDejF2ciSiA==", + "license": "MIT", "dependencies": { - "@lexical/html": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/code": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.15.0.tgz", - "integrity": "sha512-n185gjinGhz/M4BW1ayNPYAEgwW4T/NEFl2Wey/O+07W3zvh9k9ai7RjWd0c8Qzqc4DLlqvibvWPebWObQHA4w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.20.0.tgz", + "integrity": "sha512-zFsVGuzIn4CQxEnlW4AG/Hq6cyATVZ4fZTxozE/f5oK4vDPvnY/goRxrzSuAMX73A/HRX3kTEzMDcm4taRM3Mg==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" } }, "node_modules/@lexical/devtools-core": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.15.0.tgz", - "integrity": "sha512-kK/IVEiQyqs2DsY4QRYFaFiKQMpaAukAl8PXmNeGTZ7cfFVsP29E4n0/pjY+oxmiRvxbO1s2i14q58nfuhj4VQ==", - "dependencies": { - "@lexical/html": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/mark": "0.15.0", - "@lexical/table": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.20.0.tgz", + "integrity": "sha512-/CnL+Dfpzw4koy2BTdUICkvrCkMIYG8Y73KB/S1Bt5UzJpD+PV300puWJ0NvUvAj24H78r73jxvK2QUG67Tdaw==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", @@ -828,133 +831,145 @@ } }, "node_modules/@lexical/dragon": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.15.0.tgz", - "integrity": "sha512-hg2rGmxVJF7wmN6psuKw3EyhcNF7DtOYwUCBpjFZVshzAjsNEBfEnqhiMkSVSlN4+WOfM7LS+B88PTKPcnFGbQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.20.0.tgz", + "integrity": "sha512-3DAHF8mSKiPZtXCqu2P8ynSwS3fGXzg4G/V0lXNjBxhmozjzUzWZRWIWtmTlWdEu9GXsoyeM3agcaxyDPJJwkA==", + "license": "MIT", "dependencies": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "node_modules/@lexical/hashtag": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.15.0.tgz", - "integrity": "sha512-EP6KKvS6BY/8Vh1MLQYeOcYaxnvrLsUkvXXr+Fg8N477Us54Ju69pPO563mbWt7/bpnL9Sh0fbk82JtxqPWpSg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.20.0.tgz", + "integrity": "sha512-ldOP/d9tA6V9qvLyr3mRYkcYY5ySOHJ2BFOW/jZPxQcj6lWafS8Lk7XdMUpHHDjRpY2Hizsi5MHJkIqFglYXbw==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/history": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.15.0.tgz", - "integrity": "sha512-r+pzR2k/51AL6l8UfXeVe/GWPIeWY1kEOuKx9nsYB9tmAkTF66tTFz33DJIMWBVtAHWN7Dcdv0/yy6q8R6CAUQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.20.0.tgz", + "integrity": "sha512-dXtIS31BU6RmLX2KwLAi1EgGl+USeyi+rshh19azACXHPFqONZgPd2t21LOLSFn7C1/W+cSp/kqVDlQVbZUZRA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/html": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.15.0.tgz", - "integrity": "sha512-x/sfGvibwo8b5Vso4ppqNyS/fVve6Rn+TmvP/0eWOaa0I3aOQ57ulfcK6p/GTe+ZaEi8vW64oZPdi8XDgwSRaA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.20.0.tgz", + "integrity": "sha512-ob7QHkEv+mhaZjlurDj90UmEyN9G4rzBPR5QV42PLnu1qMSviMEdI5V3a5/A5aFf/FDDQ+0GAgWBFnA/MEDczQ==", + "license": "MIT", "dependencies": { - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/link": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.15.0.tgz", - "integrity": "sha512-KBV/zWk5FxqZGNcq3IKGBDCcS4t0uteU1osAIG+pefo4waTkOOgibxxEJDop2QR5wtjkYva3Qp0D8ZyJDMMMlw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.20.0.tgz", + "integrity": "sha512-zicDcfgRZPRFZ8WOZv5er0Aqkde+i7QoFVkLQD4dNLLORjoMSJOISJH6VEdjBl3k7QJTxbfrt+xT5d/ZsAN5GA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/list": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.15.0.tgz", - "integrity": "sha512-JuF4k7uo4rZFOSZGrmkxo1+sUrwTKNBhhJAiCgtM+6TO90jppxzCFNKur81yPzF1+g4GWLC9gbjzKb52QPb6cQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.20.0.tgz", + "integrity": "sha512-ufSse8ui3ooUe0HA/yF/9STrG8wYhIDLMRhELOw80GFCkPJaxs6yRvjfmJooH5IC88rpUJ5XXFFiZKfGxEZLEw==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/mark": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.15.0.tgz", - "integrity": "sha512-cdePA98sOJRc4/HHqcOcPBFq4UDwzaFJOK1N1E6XUGcXH1GU8zHtV1ElTgmbsGkyjBRwhR+OqKm9eso1PBOUkg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.20.0.tgz", + "integrity": "sha512-1P2izmkgZ4VDp+49rWO1KfWivL5aA30y5kkYbFZ/CS05fgbO7ogMjLSajpz+RN/zzW79v3q4YfikrMgaD23InA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/markdown": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.15.0.tgz", - "integrity": "sha512-wu1EP758l452BovDa7i9ZAeWuFj+YY0bc2mNc08nfZ9GqdGMej1JIguY4CwIROCYVizprL9Ocn0avH1uv9b8fA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.20.0.tgz", + "integrity": "sha512-ZoGsECejp9z6MEvc8l81b1h1aWbB3sTq6xOFeUTbDL5vKpA67z5CmQQLi0uZWrygrbO9dSE3Q/JGcodUrczxbw==", + "license": "MIT", "dependencies": { - "@lexical/code": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/rich-text": "0.15.0", - "@lexical/text": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/offset": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.15.0.tgz", - "integrity": "sha512-VO1f3m8+RRdRjuXMtCBhi1COVKRC2LhP8AFYxnFlvbV+Waz9R5xB9pqFFUe4RtyqyTLmOUj6+LtsUFhq+23voQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.20.0.tgz", + "integrity": "sha512-VMhxsxxDGnpVw0jgC8UlDf0Q2RHIHbS49uZgs3l9nP+O+G8s3b76Ta4Tb+iJOK2FY6874/TcQMbSuXGhfpQk8A==", + "license": "MIT", "dependencies": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "node_modules/@lexical/overflow": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.15.0.tgz", - "integrity": "sha512-9qKVCvh9Oka+bzR3th+UWdTEeMZXYy1ZxWbjSxefRMgQxzCvqSuVioK/065gPbvGga9EfvgLLLBDXZm8ISbJQA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.20.0.tgz", + "integrity": "sha512-z4lElzLm1FVifc7bzBZN4VNKeTuwygpyHQvCJVWXzF2Kbvex43PEYMi8u4A83idVqbmzbyBLASwUJS0voLoPLw==", + "license": "MIT", "dependencies": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "node_modules/@lexical/plain-text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.15.0.tgz", - "integrity": "sha512-yeK466mXb4xaCCJouGzEHQs59fScHxF8Asq0azNyJmkhQWYrU7WdckHf2xj8ItZFFPyj7lvwKRDYnoy4HQD7Mg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.20.0.tgz", + "integrity": "sha512-LvoC+9mm2Im1iO8GgtgaqSfW0T3mIE5GQl1xGxbVNdANmtHmBgRAJn2KfQm1XHZP6zydLRMhZkzC+jfInh2yfQ==", + "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/react": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.15.0.tgz", - "integrity": "sha512-TWDp/F9cKwjGreLzIdHKlPUeTn275rR6j1VXrBffNwC5ovxWcKLVRg502eY5xvRQH3lkKQpFgIFbJW4KTvhFsQ==", - "dependencies": { - "@lexical/clipboard": "0.15.0", - "@lexical/code": "0.15.0", - "@lexical/devtools-core": "0.15.0", - "@lexical/dragon": "0.15.0", - "@lexical/hashtag": "0.15.0", - "@lexical/history": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/mark": "0.15.0", - "@lexical/markdown": "0.15.0", - "@lexical/overflow": "0.15.0", - "@lexical/plain-text": "0.15.0", - "@lexical/rich-text": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/table": "0.15.0", - "@lexical/text": "0.15.0", - "@lexical/utils": "0.15.0", - "@lexical/yjs": "0.15.0", - "lexical": "0.15.0", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.20.0.tgz", + "integrity": "sha512-5QbN5AFtZ9efXxU/M01ADhUZgthR0e8WKi5K/w5EPpWtYFDPQnUte3rKUjYJ7uwG1iwcvaCpuMbxJjHQ+i6pDQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -963,59 +978,67 @@ } }, "node_modules/@lexical/rich-text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.15.0.tgz", - "integrity": "sha512-76tXh/eeEOHl91HpFEXCc/tUiLrsa9RcSyvCzRZahk5zqYvQPXma/AUfRzuSMf2kLwDEoauKAVqNFQcbPhqwpQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.20.0.tgz", + "integrity": "sha512-BR1pACdMA+Ymef0f5EN1y+9yP8w7S+9MgmBP1yjr3w4KdqRnfSaGWyxwcHU8eA+zu16QfivpB6501VJ90YeuXw==", + "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/selection": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.15.0.tgz", - "integrity": "sha512-S+AQC6eJiQYSa5zOPuecN85prCT0Bcb8miOdJaE17Zh+vgdUH5gk9I0tEBeG5T7tkSpq6lFiEqs2FZSfaHflbQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.20.0.tgz", + "integrity": "sha512-YnkH5UCMNN/em95or/6uwAV31vcENh1Roj+JOg5KD+gJuA7VGdDCy0vZl/o0+1badXozeZ2VRxXNC6JSK7T4+A==", + "license": "MIT", "dependencies": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "node_modules/@lexical/table": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.15.0.tgz", - "integrity": "sha512-3IRBg8IoIHetqKozRQbJQ2aPyG0ziXZ+lc8TOIAGs6METW/wxntaV+rTNrODanKAgvk2iJTIyfFkYjsqS9+VFg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.20.0.tgz", + "integrity": "sha512-qHuK2rvQUoQDx62YpvJE3Ev4yK9kjRFo79IDBapxrhoXg/wCGQOjMBzVD3G5PWkhyl/GDnww80GwYjLloQLQzg==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.15.0.tgz", - "integrity": "sha512-WsAkAt9T1RH1iDrVuWeoRUeMCOAWar5oSFtnQ4m9vhT/zuf5b8efK87GiqCH00ZAn4DGzOuAfyXlMFqBVCQdkQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.20.0.tgz", + "integrity": "sha512-Fu64i5CIlEOlgucSdp9XFqB2XqoRsw4at76n93+6RF4+LgGDnu4nLXQVCVxNmLcGyh2WgczuTpnk5P2mHNAIUA==", + "license": "MIT", "dependencies": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "node_modules/@lexical/utils": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.15.0.tgz", - "integrity": "sha512-/6954LDmTcVFgexhy5WOZDa4TxNQOEZNrf8z7TRAFiAQkihcME/GRoq1en5cbXoVNF8jv5AvNyyc7x0MByRJ6A==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.20.0.tgz", + "integrity": "sha512-sXIa2nowrNxY8VcjjuxZbJ/HovIql8bmInNaxBR03JAYfqMiL5I5/dYgjOQJV49NJnuR1uTY2GwVxVTXCTFUCw==", + "license": "MIT", "dependencies": { - "@lexical/list": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/table": "0.15.0", - "lexical": "0.15.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/yjs": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.15.0.tgz", - "integrity": "sha512-Rf4AIu620Cq90li6GU58gkzlGRdntHP4ZeZrbJ3ToW7vEEnkW6Wl9/HhO647GG4OL5w46M0iWvx1b1b8xjYT1w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.20.0.tgz", + "integrity": "sha512-TiHNhu2VkhXN69V+fXVS3xjOQ6aLnheQUGwOAhuFkDPL3VLCb0yl2Mgydpayn+3Grwii4ZBHcF7oCC84GiU5bw==", + "license": "MIT", "dependencies": { - "@lexical/offset": "0.15.0", - "lexical": "0.15.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -1556,6 +1579,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", "peer": true, "funding": { "type": "GitHub Sponsors ❤", @@ -1592,14 +1616,16 @@ } }, "node_modules/lexical": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.15.0.tgz", - "integrity": "sha512-/7HrPAmtgsc1F+qpv5bFwoQZ6CbH/w3mPPL2AW5P75/QYrqKz4bhvJrc2jozIX0GxtuT/YUYT7w+1sZMtUWbOg==" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.20.0.tgz", + "integrity": "sha512-lJEHLFACXqRf3u/VlIOu9T7MJ51O4la92uOBwiS9Sx+juDK3Nrru5Vgl1aUirV1qK8XEM3h6Org2HcrsrzZ3ZA==", + "license": "MIT" }, "node_modules/lib0": { - "version": "0.2.94", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.94.tgz", - "integrity": "sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==", + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "license": "MIT", "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" @@ -1705,6 +1731,7 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1948,12 +1975,13 @@ "dev": true }, "node_modules/yjs": { - "version": "13.6.15", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.15.tgz", - "integrity": "sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==", + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "license": "MIT", "peer": true, "dependencies": { - "lib0": "^0.2.86" + "lib0": "^0.2.98" }, "engines": { "node": ">=16.0.0", @@ -2424,225 +2452,227 @@ } }, "@lexical/clipboard": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.15.0.tgz", - "integrity": "sha512-binCltK7KiURQJFogvueYfmDNEKynN/lmZrCLFp2xBjEIajqw4WtOVLJZ33engdqNlvj0JqrxrWxbKG+yvUwrg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.20.0.tgz", + "integrity": "sha512-oHmb9kSVHjeFCd2q8VrEXW22doUHMJ6cGXqo7Ican7Ljl4/9OgRWr+cq55yntoSaJfCrRYkTiZCLDejF2ciSiA==", "requires": { - "@lexical/html": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/code": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.15.0.tgz", - "integrity": "sha512-n185gjinGhz/M4BW1ayNPYAEgwW4T/NEFl2Wey/O+07W3zvh9k9ai7RjWd0c8Qzqc4DLlqvibvWPebWObQHA4w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.20.0.tgz", + "integrity": "sha512-zFsVGuzIn4CQxEnlW4AG/Hq6cyATVZ4fZTxozE/f5oK4vDPvnY/goRxrzSuAMX73A/HRX3kTEzMDcm4taRM3Mg==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" } }, "@lexical/devtools-core": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.15.0.tgz", - "integrity": "sha512-kK/IVEiQyqs2DsY4QRYFaFiKQMpaAukAl8PXmNeGTZ7cfFVsP29E4n0/pjY+oxmiRvxbO1s2i14q58nfuhj4VQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.20.0.tgz", + "integrity": "sha512-/CnL+Dfpzw4koy2BTdUICkvrCkMIYG8Y73KB/S1Bt5UzJpD+PV300puWJ0NvUvAj24H78r73jxvK2QUG67Tdaw==", "requires": { - "@lexical/html": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/mark": "0.15.0", - "@lexical/table": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/dragon": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.15.0.tgz", - "integrity": "sha512-hg2rGmxVJF7wmN6psuKw3EyhcNF7DtOYwUCBpjFZVshzAjsNEBfEnqhiMkSVSlN4+WOfM7LS+B88PTKPcnFGbQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.20.0.tgz", + "integrity": "sha512-3DAHF8mSKiPZtXCqu2P8ynSwS3fGXzg4G/V0lXNjBxhmozjzUzWZRWIWtmTlWdEu9GXsoyeM3agcaxyDPJJwkA==", "requires": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "@lexical/hashtag": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.15.0.tgz", - "integrity": "sha512-EP6KKvS6BY/8Vh1MLQYeOcYaxnvrLsUkvXXr+Fg8N477Us54Ju69pPO563mbWt7/bpnL9Sh0fbk82JtxqPWpSg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.20.0.tgz", + "integrity": "sha512-ldOP/d9tA6V9qvLyr3mRYkcYY5ySOHJ2BFOW/jZPxQcj6lWafS8Lk7XdMUpHHDjRpY2Hizsi5MHJkIqFglYXbw==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/history": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.15.0.tgz", - "integrity": "sha512-r+pzR2k/51AL6l8UfXeVe/GWPIeWY1kEOuKx9nsYB9tmAkTF66tTFz33DJIMWBVtAHWN7Dcdv0/yy6q8R6CAUQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.20.0.tgz", + "integrity": "sha512-dXtIS31BU6RmLX2KwLAi1EgGl+USeyi+rshh19azACXHPFqONZgPd2t21LOLSFn7C1/W+cSp/kqVDlQVbZUZRA==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/html": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.15.0.tgz", - "integrity": "sha512-x/sfGvibwo8b5Vso4ppqNyS/fVve6Rn+TmvP/0eWOaa0I3aOQ57ulfcK6p/GTe+ZaEi8vW64oZPdi8XDgwSRaA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.20.0.tgz", + "integrity": "sha512-ob7QHkEv+mhaZjlurDj90UmEyN9G4rzBPR5QV42PLnu1qMSviMEdI5V3a5/A5aFf/FDDQ+0GAgWBFnA/MEDczQ==", "requires": { - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/link": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.15.0.tgz", - "integrity": "sha512-KBV/zWk5FxqZGNcq3IKGBDCcS4t0uteU1osAIG+pefo4waTkOOgibxxEJDop2QR5wtjkYva3Qp0D8ZyJDMMMlw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.20.0.tgz", + "integrity": "sha512-zicDcfgRZPRFZ8WOZv5er0Aqkde+i7QoFVkLQD4dNLLORjoMSJOISJH6VEdjBl3k7QJTxbfrt+xT5d/ZsAN5GA==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/list": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.15.0.tgz", - "integrity": "sha512-JuF4k7uo4rZFOSZGrmkxo1+sUrwTKNBhhJAiCgtM+6TO90jppxzCFNKur81yPzF1+g4GWLC9gbjzKb52QPb6cQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.20.0.tgz", + "integrity": "sha512-ufSse8ui3ooUe0HA/yF/9STrG8wYhIDLMRhELOw80GFCkPJaxs6yRvjfmJooH5IC88rpUJ5XXFFiZKfGxEZLEw==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/mark": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.15.0.tgz", - "integrity": "sha512-cdePA98sOJRc4/HHqcOcPBFq4UDwzaFJOK1N1E6XUGcXH1GU8zHtV1ElTgmbsGkyjBRwhR+OqKm9eso1PBOUkg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.20.0.tgz", + "integrity": "sha512-1P2izmkgZ4VDp+49rWO1KfWivL5aA30y5kkYbFZ/CS05fgbO7ogMjLSajpz+RN/zzW79v3q4YfikrMgaD23InA==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/markdown": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.15.0.tgz", - "integrity": "sha512-wu1EP758l452BovDa7i9ZAeWuFj+YY0bc2mNc08nfZ9GqdGMej1JIguY4CwIROCYVizprL9Ocn0avH1uv9b8fA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.20.0.tgz", + "integrity": "sha512-ZoGsECejp9z6MEvc8l81b1h1aWbB3sTq6xOFeUTbDL5vKpA67z5CmQQLi0uZWrygrbO9dSE3Q/JGcodUrczxbw==", "requires": { - "@lexical/code": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/rich-text": "0.15.0", - "@lexical/text": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/offset": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.15.0.tgz", - "integrity": "sha512-VO1f3m8+RRdRjuXMtCBhi1COVKRC2LhP8AFYxnFlvbV+Waz9R5xB9pqFFUe4RtyqyTLmOUj6+LtsUFhq+23voQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.20.0.tgz", + "integrity": "sha512-VMhxsxxDGnpVw0jgC8UlDf0Q2RHIHbS49uZgs3l9nP+O+G8s3b76Ta4Tb+iJOK2FY6874/TcQMbSuXGhfpQk8A==", "requires": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "@lexical/overflow": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.15.0.tgz", - "integrity": "sha512-9qKVCvh9Oka+bzR3th+UWdTEeMZXYy1ZxWbjSxefRMgQxzCvqSuVioK/065gPbvGga9EfvgLLLBDXZm8ISbJQA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.20.0.tgz", + "integrity": "sha512-z4lElzLm1FVifc7bzBZN4VNKeTuwygpyHQvCJVWXzF2Kbvex43PEYMi8u4A83idVqbmzbyBLASwUJS0voLoPLw==", "requires": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "@lexical/plain-text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.15.0.tgz", - "integrity": "sha512-yeK466mXb4xaCCJouGzEHQs59fScHxF8Asq0azNyJmkhQWYrU7WdckHf2xj8ItZFFPyj7lvwKRDYnoy4HQD7Mg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.20.0.tgz", + "integrity": "sha512-LvoC+9mm2Im1iO8GgtgaqSfW0T3mIE5GQl1xGxbVNdANmtHmBgRAJn2KfQm1XHZP6zydLRMhZkzC+jfInh2yfQ==", "requires": { - "@lexical/clipboard": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/react": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.15.0.tgz", - "integrity": "sha512-TWDp/F9cKwjGreLzIdHKlPUeTn275rR6j1VXrBffNwC5ovxWcKLVRg502eY5xvRQH3lkKQpFgIFbJW4KTvhFsQ==", - "requires": { - "@lexical/clipboard": "0.15.0", - "@lexical/code": "0.15.0", - "@lexical/devtools-core": "0.15.0", - "@lexical/dragon": "0.15.0", - "@lexical/hashtag": "0.15.0", - "@lexical/history": "0.15.0", - "@lexical/link": "0.15.0", - "@lexical/list": "0.15.0", - "@lexical/mark": "0.15.0", - "@lexical/markdown": "0.15.0", - "@lexical/overflow": "0.15.0", - "@lexical/plain-text": "0.15.0", - "@lexical/rich-text": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/table": "0.15.0", - "@lexical/text": "0.15.0", - "@lexical/utils": "0.15.0", - "@lexical/yjs": "0.15.0", - "lexical": "0.15.0", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.20.0.tgz", + "integrity": "sha512-5QbN5AFtZ9efXxU/M01ADhUZgthR0e8WKi5K/w5EPpWtYFDPQnUte3rKUjYJ7uwG1iwcvaCpuMbxJjHQ+i6pDQ==", + "requires": { + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.15.0.tgz", - "integrity": "sha512-76tXh/eeEOHl91HpFEXCc/tUiLrsa9RcSyvCzRZahk5zqYvQPXma/AUfRzuSMf2kLwDEoauKAVqNFQcbPhqwpQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.20.0.tgz", + "integrity": "sha512-BR1pACdMA+Ymef0f5EN1y+9yP8w7S+9MgmBP1yjr3w4KdqRnfSaGWyxwcHU8eA+zu16QfivpB6501VJ90YeuXw==", "requires": { - "@lexical/clipboard": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/selection": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.15.0.tgz", - "integrity": "sha512-S+AQC6eJiQYSa5zOPuecN85prCT0Bcb8miOdJaE17Zh+vgdUH5gk9I0tEBeG5T7tkSpq6lFiEqs2FZSfaHflbQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.20.0.tgz", + "integrity": "sha512-YnkH5UCMNN/em95or/6uwAV31vcENh1Roj+JOg5KD+gJuA7VGdDCy0vZl/o0+1badXozeZ2VRxXNC6JSK7T4+A==", "requires": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "@lexical/table": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.15.0.tgz", - "integrity": "sha512-3IRBg8IoIHetqKozRQbJQ2aPyG0ziXZ+lc8TOIAGs6METW/wxntaV+rTNrODanKAgvk2iJTIyfFkYjsqS9+VFg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.20.0.tgz", + "integrity": "sha512-qHuK2rvQUoQDx62YpvJE3Ev4yK9kjRFo79IDBapxrhoXg/wCGQOjMBzVD3G5PWkhyl/GDnww80GwYjLloQLQzg==", "requires": { - "@lexical/utils": "0.15.0", - "lexical": "0.15.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/text": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.15.0.tgz", - "integrity": "sha512-WsAkAt9T1RH1iDrVuWeoRUeMCOAWar5oSFtnQ4m9vhT/zuf5b8efK87GiqCH00ZAn4DGzOuAfyXlMFqBVCQdkQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.20.0.tgz", + "integrity": "sha512-Fu64i5CIlEOlgucSdp9XFqB2XqoRsw4at76n93+6RF4+LgGDnu4nLXQVCVxNmLcGyh2WgczuTpnk5P2mHNAIUA==", "requires": { - "lexical": "0.15.0" + "lexical": "0.20.0" } }, "@lexical/utils": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.15.0.tgz", - "integrity": "sha512-/6954LDmTcVFgexhy5WOZDa4TxNQOEZNrf8z7TRAFiAQkihcME/GRoq1en5cbXoVNF8jv5AvNyyc7x0MByRJ6A==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.20.0.tgz", + "integrity": "sha512-sXIa2nowrNxY8VcjjuxZbJ/HovIql8bmInNaxBR03JAYfqMiL5I5/dYgjOQJV49NJnuR1uTY2GwVxVTXCTFUCw==", "requires": { - "@lexical/list": "0.15.0", - "@lexical/selection": "0.15.0", - "@lexical/table": "0.15.0", - "lexical": "0.15.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/yjs": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.15.0.tgz", - "integrity": "sha512-Rf4AIu620Cq90li6GU58gkzlGRdntHP4ZeZrbJ3ToW7vEEnkW6Wl9/HhO647GG4OL5w46M0iWvx1b1b8xjYT1w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.20.0.tgz", + "integrity": "sha512-TiHNhu2VkhXN69V+fXVS3xjOQ6aLnheQUGwOAhuFkDPL3VLCb0yl2Mgydpayn+3Grwii4ZBHcF7oCC84GiU5bw==", "requires": { - "@lexical/offset": "0.15.0", - "lexical": "0.15.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" } }, "@rollup/rollup-android-arm-eabi": { @@ -3021,14 +3051,14 @@ "dev": true }, "lexical": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.15.0.tgz", - "integrity": "sha512-/7HrPAmtgsc1F+qpv5bFwoQZ6CbH/w3mPPL2AW5P75/QYrqKz4bhvJrc2jozIX0GxtuT/YUYT7w+1sZMtUWbOg==" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.20.0.tgz", + "integrity": "sha512-lJEHLFACXqRf3u/VlIOu9T7MJ51O4la92uOBwiS9Sx+juDK3Nrru5Vgl1aUirV1qK8XEM3h6Org2HcrsrzZ3ZA==" }, "lib0": { - "version": "0.2.94", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.94.tgz", - "integrity": "sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==", + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", "peer": true, "requires": { "isomorphic.js": "^0.2.4" @@ -3223,12 +3253,12 @@ "dev": true }, "yjs": { - "version": "13.6.15", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.15.tgz", - "integrity": "sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==", + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", "peer": true, "requires": { - "lib0": "^0.2.86" + "lib0": "^0.2.98" } } } diff --git a/examples/react-rich/src/App.tsx b/examples/react-rich/src/App.tsx index 206d5624c19..8c16e827171 100644 --- a/examples/react-rich/src/App.tsx +++ b/examples/react-rich/src/App.tsx @@ -16,6 +16,7 @@ import { $isTextNode, DOMConversionMap, DOMExportOutput, + DOMExportOutputMap, Klass, LexicalEditor, LexicalNode, @@ -53,7 +54,7 @@ const removeStylesExportDOM = ( return output; }; -const exportMap = new Map< +const exportMap: DOMExportOutputMap = new Map< Klass, (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput >([ From 79a2e1542bbb3592feb31434e34182f621bfb98e Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 14 Nov 2024 15:21:29 +0000 Subject: [PATCH 090/133] Link flow types (#6833) --- packages/lexical-link/flow/LexicalLink.js.flow | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/lexical-link/flow/LexicalLink.js.flow b/packages/lexical-link/flow/LexicalLink.js.flow index 6c9a441f7ad..cab496485ac 100644 --- a/packages/lexical-link/flow/LexicalLink.js.flow +++ b/packages/lexical-link/flow/LexicalLink.js.flow @@ -13,13 +13,22 @@ import type { NodeKey, RangeSelection, LexicalCommand, + SerializedElementNode, } from 'lexical'; import {addClassNamesToElement} from '@lexical/utils'; import {$isElementNode, ElementNode} from 'lexical'; -export type LinkAttributes = { +export type LinkAttributes = $ReadOnly<{ rel?: null | string, target?: null | string, title?: null | string, +}>; +export type SerializedLinkNode = { + ...SerializedElementNode, + rel?: null | string, + target?: null | string, + title?: null | string, + url: string, + ... }; declare export class LinkNode extends ElementNode { __url: string; @@ -36,6 +45,7 @@ declare export class LinkNode extends ElementNode { config: EditorConfig, ): boolean; static importDOM(): DOMConversionMap | null; + exportJSON(): SerializedLinkNode; getURL(): string; setURL(url: string): void; getTarget(): null | string; @@ -60,6 +70,11 @@ declare export function $createLinkNode( declare export function $isLinkNode( node: ?LexicalNode, ): node is LinkNode; +export type SerializedAutoLinkNode = { + ...SerializedLinkNode, + isUnlinked: boolean, + ... +}; declare export class AutoLinkNode extends LinkNode { static getType(): string; // $FlowFixMe clone method inheritance From 14a4154389f1b27075a440b1a578d5ceedef0293 Mon Sep 17 00:00:00 2001 From: Maksim Horbachevsky Date: Thu, 14 Nov 2024 16:11:19 -0500 Subject: [PATCH 091/133] Prevent initial value fn to be called on rerender (#6835) --- packages/lexical-react/src/useLexicalSubscription.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/lexical-react/src/useLexicalSubscription.tsx b/packages/lexical-react/src/useLexicalSubscription.tsx index c8230c40033..9cd3eda81f4 100644 --- a/packages/lexical-react/src/useLexicalSubscription.tsx +++ b/packages/lexical-react/src/useLexicalSubscription.tsx @@ -29,8 +29,10 @@ export function useLexicalSubscription( () => subscription(editor), [editor, subscription], ); - const valueRef = useRef(initializedSubscription.initialValueFn()); - const [value, setValue] = useState(valueRef.current); + const [value, setValue] = useState(() => + initializedSubscription.initialValueFn(), + ); + const valueRef = useRef(value); useLayoutEffect(() => { const {initialValueFn, subscribe} = initializedSubscription; const currentValue = initialValueFn(); From 12fe9aa15c7b0dc76eb6af8b3018ef32c8867fd4 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 15 Nov 2024 09:58:13 -0800 Subject: [PATCH 092/133] [lexical-table] Bug Fix: Fix down arrow key handling in TableObserver (#6839) --- .../lexical-table/src/LexicalTableSelectionHelpers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index f629d1feb0d..740af8e121f 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -782,8 +782,14 @@ export function applyTableHandlers( const firstCell = firstRow.getFirstChild(); if ( $isTableCellNode(firstCell) && - !$findMatchingParent(anchorCell, (node) => node.is(firstCell)) + tableNode.is( + $findMatchingParent( + anchorCell, + (node) => node.is(tableNode) || node.is(firstCell), + ), + ) ) { + // The selection moved to the table, but not in the first cell firstCell.selectStart(); return true; } From e2396e441e48d87c8114dd8a41ae89892a79108d Mon Sep 17 00:00:00 2001 From: Oluwasanya Olaoluwa Date: Fri, 15 Nov 2024 20:18:02 +0100 Subject: [PATCH 093/133] [lexical-link] Test: Removing link from node(children) (#6817) Co-authored-by: Bob Ippolito --- .../__tests__/unit/LexicalLinkNode.test.ts | 68 +++++++++++++++++++ .../lexical/src/__tests__/utils/index.tsx | 2 + 2 files changed, 70 insertions(+) diff --git a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts index 3ad6cbad8b8..94582207ffc 100644 --- a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts +++ b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts @@ -13,7 +13,10 @@ import { LinkNode, SerializedLinkNode, } from '@lexical/link'; +import {$createMarkNode, $isMarkNode} from '@lexical/mark'; import { + $createParagraphNode, + $createTextNode, $getRoot, $selectAll, ParagraphNode, @@ -409,5 +412,70 @@ describe('LexicalLinkNode tests', () => { const link = paragraph.children[0] as SerializedLinkNode; expect(link.title).toBe('Lexical Website'); }); + + test('$toggleLink correctly removes link when textnode has children(like marknode)', async () => { + const {editor} = testEnv; + await editor.update(() => { + const paragraph = $createParagraphNode(); + const precedingText = $createTextNode('some '); // space after + const textNode = $createTextNode('text'); + + paragraph.append(precedingText, textNode); + + const linkNode = $createLinkNode('https://example.com/foo', { + rel: 'noreferrer', + }); + textNode.insertAfter(linkNode); + linkNode.append(textNode); + + const markNode = $createMarkNode(['knetk']); + textNode.insertBefore(markNode); + markNode.append(textNode); + $getRoot().append(paragraph); + }); + + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const [textNode, linkNode] = paragraph.getChildren(); + + // Check first text node + expect(textNode.getTextContent()).toBe('some '); + + // Check link node and its nested structure + if ($isLinkNode(linkNode)) { + expect(linkNode.getURL()).toBe('https://example.com/foo'); + expect(linkNode.getRel()).toBe('noreferrer'); + + // Check mark node nested inside link + const markNode = linkNode.getFirstChild(); + if ($isMarkNode(markNode)) { + expect(markNode.getType()).toBe('mark'); + expect(markNode.getIDs()).toEqual(['knetk']); + expect(markNode.getTextContent()).toBe('text'); + } + } + }); + + await editor.update(() => { + $selectAll(); + $toggleLink(null); + }); + + // Verify structure after link removal + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const [textNode, markNode] = paragraph.getChildren(); + + // Check text node remains unchanged + expect(textNode.getTextContent()).toBe('some '); + + // Check mark node is preserved and moved up to paragraph level + if ($isMarkNode(markNode)) { + expect(markNode.getType()).toBe('mark'); + expect(markNode.getIDs()).toEqual(['knetk']); + expect(markNode.getTextContent()).toBe('text'); + } + }); + }); }); }); diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index dff3b2adaee..4bb096f874d 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -11,6 +11,7 @@ import {HashtagNode} from '@lexical/hashtag'; import {createHeadlessEditor} from '@lexical/headless'; import {AutoLinkNode, LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; +import {MarkNode} from '@lexical/mark'; import {OverflowNode} from '@lexical/overflow'; import { InitialConfigType, @@ -486,6 +487,7 @@ const DEFAULT_NODES: NonNullable = [ TestInlineElementNode, TestShadowRootNode, TestTextNode, + MarkNode, ]; export function TestComposer({ From ab758a701e4f74cebfa1ebc71aa34061d8052083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EB=8F=99=ED=98=84=28Ivan=29?= <79711744+ivaaaaann@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:43:22 +0900 Subject: [PATCH 094/133] [lexical-react] Fix(lexical-react): ContentEditable props type rename (#6837) --- .../lexical-react/src/LexicalContentEditable.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 657ba4fed3a..0463ffb4a7a 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -15,7 +15,7 @@ import {forwardRef, Ref, useLayoutEffect, useState} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; -export type Props = Omit & +export type ContentEditableProps = Omit & ( | { 'aria-placeholder'?: void; @@ -29,10 +29,19 @@ export type Props = Omit & } ); +/** + * @deprecated This type has been renamed to `ContentEditableProps` to provide a clearer and more descriptive name. + * For backward compatibility, this type is still exported as `Props`, but it is recommended to migrate to using `ContentEditableProps` instead. + * + * @note This alias is maintained for compatibility purposes but may be removed in future versions. + * Please update your codebase to use `ContentEditableProps` to ensure long-term maintainability. + */ +export type Props = ContentEditableProps; + export const ContentEditable = forwardRef(ContentEditableImpl); function ContentEditableImpl( - props: Props, + props: ContentEditableProps, ref: Ref, ): JSX.Element { const {placeholder, ...rest} = props; From 29928b03057123e1dee2a8d2fbc2977bbd24c3f6 Mon Sep 17 00:00:00 2001 From: Hadi Hamid <113849746+hadiham@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:24:32 -0500 Subject: [PATCH 095/133] [lexical-utils] Bug Fix: Add missing Flow type declarations (#6841) --- .../lexical-utils/flow/LexicalUtils.js.flow | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 5bdb9c29815..3f73f5a1db9 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -48,6 +48,9 @@ declare export function $getNextSiblingOrParentSibling( node: LexicalNode, ): null | [LexicalNode, number]; declare export function $getDepth(node: LexicalNode): number; +declare export function $getNextRightPreorderNode( + startingNode: LexicalNode, +): LexicalNode | null; declare export function $getNearestNodeOfType( node: LexicalNode, klass: Class, @@ -90,7 +93,23 @@ declare export function $wrapNodeInElement( createElementNode: () => ElementNode, ): ElementNode; +declare export function objectKlassEquals( + object: mixed, + objectClass: Class, +): boolean; + +declare export function $filter( + nodes: Array, + filterFn: (node: LexicalNode) => null | T, +): Array; + +declare export function $insertFirst(parent: ElementNode, node: LexicalNode): void; + declare export function $splitNode( node: ElementNode, offset: number, ): [ElementNode | null, ElementNode]; + +declare export function calculateZoomLevel(element: Element | null): number; + +declare export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean; From 80b0b8987238e17ac1f906640f8d6cacc795bf58 Mon Sep 17 00:00:00 2001 From: Syed Umar Anis Date: Wed, 20 Nov 2024 11:19:32 +1100 Subject: [PATCH 096/133] Table Action Menu - fix UI issue with Merge Cells item (#6830) --- .../src/plugins/TableActionMenuPlugin/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 43f1f8aa5d2..2a3fc2bdff1 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -472,7 +472,7 @@ function TableActionMenu({ className="item" onClick={() => mergeTableCellsAtSelection()} data-test-id="table-merge-cells"> - Merge cells + Merge cells ); } else if (canUnmergeCell) { @@ -482,7 +482,7 @@ function TableActionMenu({ className="item" onClick={() => unmergeTableCellsAtSelection()} data-test-id="table-unmerge-cells"> - Unmerge cells + Unmerge cells ); } From ff626bc56ca7d62d64349ffb8fd403ee7ecc68f7 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 20 Nov 2024 15:54:52 +0800 Subject: [PATCH 097/133] [*] Chore: npm upgrade cross-spawn (#6848) --- package-lock.json | 104 +++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index b147a804740..25e868bc531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13159,10 +13159,11 @@ } }, "node_modules/cross-env/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -14257,10 +14258,11 @@ } }, "node_modules/default-browser/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -17657,10 +17659,11 @@ "dev": true }, "node_modules/eslint/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -18157,9 +18160,10 @@ } }, "node_modules/execa/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -18874,10 +18878,11 @@ } }, "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -28570,10 +28575,11 @@ } }, "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -28716,10 +28722,11 @@ } }, "node_modules/nypm/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -31655,9 +31662,10 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/react-dev-utils/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -48500,9 +48508,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -49223,9 +49231,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -51248,9 +51256,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -51895,9 +51903,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -52412,9 +52420,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -58850,9 +58858,9 @@ }, "dependencies": { "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "requires": { "nice-try": "^1.0.4", @@ -58953,9 +58961,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -60905,9 +60913,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", From 93b756ba302c6552fe8d71b77a8062a68e2c95de Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo <82163647+Shopiley@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:45:18 +0100 Subject: [PATCH 098/133] [lexical-link] Test: Appending inline element nodes to ListNode (#6826) --- .../__tests__/unit/LexicalListNode.test.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/lexical-list/src/__tests__/unit/LexicalListNode.test.ts b/packages/lexical-list/src/__tests__/unit/LexicalListNode.test.ts index 6abcbbd4cb7..18c7010d56b 100644 --- a/packages/lexical-list/src/__tests__/unit/LexicalListNode.test.ts +++ b/packages/lexical-list/src/__tests__/unit/LexicalListNode.test.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. * */ -import {ParagraphNode, TextNode} from 'lexical'; +import {$createLinkNode, $isLinkNode, LinkNode} from '@lexical/link'; +import {$getRoot, ParagraphNode, TextNode} from 'lexical'; import {initializeUnitTest} from 'lexical/src/__tests__/utils'; import { @@ -260,6 +261,45 @@ describe('LexicalListNode tests', () => { }); }); + test('ListNode.append() should wrap an InlineNode in a ListItemNode without converting it to TextNode', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + const linkNode = $createLinkNode('https://lexical.dev/'); + + listNode.append(linkNode); + + const root = $getRoot(); + root.append(listNode); + }); + + editor.read(() => { + const root = $getRoot(); + + const listNode = root.getFirstChild(); + expect(listNode).not.toBeNull(); + expect($isListNode(listNode)).toBe(true); + + if ($isListNode(listNode)) { + const firstChild = listNode.getFirstChild(); + expect($isListItemNode(firstChild)).toBe(true); + + if ($isListItemNode(firstChild)) { + const wrappedNode = firstChild?.getFirstChild(); + expect(wrappedNode).not.toBeNull(); + expect($isLinkNode(wrappedNode)).toBe(true); + + expect((wrappedNode as LinkNode).getURL()).toBe( + 'https://lexical.dev/', + ); + } else { + expect($isListItemNode(firstChild)).toBe(true); + } + } + }); + }); + test('$createListNode()', async () => { const {editor} = testEnv; From b73bfc3c161a67caf168d29dc7a0ff67391bd629 Mon Sep 17 00:00:00 2001 From: Dani Lauzurica <37160556+dani-lp@users.noreply.github.com> Date: Thu, 21 Nov 2024 02:00:47 +0100 Subject: [PATCH 099/133] [Lexical] Bug Fix: backspace bug when deleting nodes with `canInsertTextAfter` set to false (#6268) Co-authored-by: Bob Ippolito --- packages/lexical/src/LexicalEvents.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 4a177d19498..b6c46fbdeeb 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -591,14 +591,20 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void { // Chromium Android at the moment seems to ignore the preventDefault // on 'deleteContentBackward' and still deletes the content. Which leads // to multiple deletions. So we let the browser handle the deletion in this case. - const selectedNodeText = selection.anchor.getNode().getTextContent(); + const selectedNode = selection.anchor.getNode(); + const selectedNodeText = selectedNode.getTextContent(); + // When the target node has `canInsertTextAfter` set to false, the first deletion + // doesn't have an effect, so we need to handle it with Lexical. + const selectedNodeCanInsertTextAfter = + selectedNode.canInsertTextAfter(); const hasSelectedAllTextInNode = selection.anchor.offset === 0 && selection.focus.offset === selectedNodeText.length; const shouldLetBrowserHandleDelete = IS_ANDROID_CHROME && isSelectionAnchorSameAsFocus && - !hasSelectedAllTextInNode; + !hasSelectedAllTextInNode && + selectedNodeCanInsertTextAfter; if (!shouldLetBrowserHandleDelete) { dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true); } From d12586c68b653efd02c732af8141c87f000e644e Mon Sep 17 00:00:00 2001 From: Sachin Mahato <93040506+Sachin-Mahato@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:26:28 +0530 Subject: [PATCH 100/133] Feature: Deprecate $nodesOfType function (#6855) Co-authored-by: Bob Ippolito --- packages/lexical/src/LexicalUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index a4a3af63118..dc3baa6fd4f 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1148,7 +1148,9 @@ export function setMutatedNode( mutatedNodesByType.set(nodeKey, isMove ? 'updated' : mutation); } } - +/** + * @deprecated Use {@link LexicalEditor.registerMutationListener} with `skipInitialization: false` instead. + */ export function $nodesOfType(klass: Klass): Array { const klassType = klass.getType(); const editorState = getActiveEditorState(); From fe4f5b8cb408476f39d6689ae8886be1d1322df1 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 22 Nov 2024 16:36:48 +0800 Subject: [PATCH 101/133] [__tests__] npm upgrade cross-spawn (#6856) --- .../lexical-esm-astro-react/package-lock.json | 368 ++++++++++-------- 1 file changed, 207 insertions(+), 161 deletions(-) diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json index 26ccfd3cdc4..6432ca3af37 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package-lock.json @@ -1,21 +1,21 @@ { "name": "lexical-esm-astro-react", - "version": "0.17.1", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lexical-esm-astro-react", - "version": "0.17.1", + "version": "0.20.0", "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.17.1", - "@lexical/utils": "0.17.1", + "@lexical/react": "0.20.0", + "@lexical/utils": "0.20.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.17.1", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" @@ -993,38 +993,41 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.17.1.tgz", - "integrity": "sha512-OVqnEfWX8XN5xxuMPo6BfgGKHREbz++D5V5ISOiml0Z8fV/TQkdgwqbBJcUdJHGRHWSUwdK7CWGs/VALvVvZyw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.20.0.tgz", + "integrity": "sha512-oHmb9kSVHjeFCd2q8VrEXW22doUHMJ6cGXqo7Ican7Ljl4/9OgRWr+cq55yntoSaJfCrRYkTiZCLDejF2ciSiA==", + "license": "MIT", "dependencies": { - "@lexical/html": "0.17.1", - "@lexical/list": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/code": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.17.1.tgz", - "integrity": "sha512-ZspfTm6g6dN3nAb4G5bPp3SqxzdkB/bjGfa0uRKMU6/eBKtrMUgZsGxt0a8JRZ1eq2TZrQhx+l1ceRoLXii/bQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.20.0.tgz", + "integrity": "sha512-zFsVGuzIn4CQxEnlW4AG/Hq6cyATVZ4fZTxozE/f5oK4vDPvnY/goRxrzSuAMX73A/HRX3kTEzMDcm4taRM3Mg==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" } }, "node_modules/@lexical/devtools-core": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.17.1.tgz", - "integrity": "sha512-SzL1EX9Rt5GptIo87t6nDxAc9TtYtl6DyAPNz/sCltspdd69KQgs23sTRa26/tkNFCS1jziRN7vpN3mlnmm5wA==", - "dependencies": { - "@lexical/html": "0.17.1", - "@lexical/link": "0.17.1", - "@lexical/mark": "0.17.1", - "@lexical/table": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.20.0.tgz", + "integrity": "sha512-/CnL+Dfpzw4koy2BTdUICkvrCkMIYG8Y73KB/S1Bt5UzJpD+PV300puWJ0NvUvAj24H78r73jxvK2QUG67Tdaw==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", @@ -1032,133 +1035,145 @@ } }, "node_modules/@lexical/dragon": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.17.1.tgz", - "integrity": "sha512-lhBRKP7RlhiVCLtF0qiNqmMhEO6cQB43sMe7d4bvuY1G2++oKY/XAJPg6QJZdXRrCGRQ6vZ26QRNhRPmCxL5Ng==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.20.0.tgz", + "integrity": "sha512-3DAHF8mSKiPZtXCqu2P8ynSwS3fGXzg4G/V0lXNjBxhmozjzUzWZRWIWtmTlWdEu9GXsoyeM3agcaxyDPJJwkA==", + "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.20.0" } }, "node_modules/@lexical/hashtag": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.17.1.tgz", - "integrity": "sha512-XtP0BI8vEewAe7tzq9MC49UPUvuChuNJI/jqFp+ezZlt/RUq0BClQCOPuSlrTJhluvE2rWnUnOnVMk8ILRvggQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.20.0.tgz", + "integrity": "sha512-ldOP/d9tA6V9qvLyr3mRYkcYY5ySOHJ2BFOW/jZPxQcj6lWafS8Lk7XdMUpHHDjRpY2Hizsi5MHJkIqFglYXbw==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/history": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.17.1.tgz", - "integrity": "sha512-OU/ohajz4FXchUhghsWC7xeBPypFe50FCm5OePwo767G7P233IztgRKIng2pTT4zhCPW7S6Mfl53JoFHKehpWA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.20.0.tgz", + "integrity": "sha512-dXtIS31BU6RmLX2KwLAi1EgGl+USeyi+rshh19azACXHPFqONZgPd2t21LOLSFn7C1/W+cSp/kqVDlQVbZUZRA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/html": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.17.1.tgz", - "integrity": "sha512-yGG+K2DXl7Wn2DpNuZ0Y3uCHJgfHkJN3/MmnFb4jLnH1FoJJiuy7WJb/BRRh9H+6xBJ9v70iv+kttDJ0u1xp5w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.20.0.tgz", + "integrity": "sha512-ob7QHkEv+mhaZjlurDj90UmEyN9G4rzBPR5QV42PLnu1qMSviMEdI5V3a5/A5aFf/FDDQ+0GAgWBFnA/MEDczQ==", + "license": "MIT", "dependencies": { - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/link": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.17.1.tgz", - "integrity": "sha512-qFJEKBesZAtR8kfJfIVXRFXVw6dwcpmGCW7duJbtBRjdLjralOxrlVKyFhW9PEXGhi4Mdq2Ux16YnnDncpORdQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.20.0.tgz", + "integrity": "sha512-zicDcfgRZPRFZ8WOZv5er0Aqkde+i7QoFVkLQD4dNLLORjoMSJOISJH6VEdjBl3k7QJTxbfrt+xT5d/ZsAN5GA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/list": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.17.1.tgz", - "integrity": "sha512-k9ZnmQuBvW+xVUtWJZwoGtiVG2cy+hxzkLGU4jTq1sqxRIoSeGcjvhFAK8JSEj4i21SgkB1FmkWXoYK5kbwtRA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.20.0.tgz", + "integrity": "sha512-ufSse8ui3ooUe0HA/yF/9STrG8wYhIDLMRhELOw80GFCkPJaxs6yRvjfmJooH5IC88rpUJ5XXFFiZKfGxEZLEw==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/mark": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.17.1.tgz", - "integrity": "sha512-V82SSRjvygmV+ZMwVpy5gwgr2ZDrJpl3TvEDO+G5I4SDSjbgvua8hO4dKryqiDVlooxQq9dsou0GrZ9Qtm6rYg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.20.0.tgz", + "integrity": "sha512-1P2izmkgZ4VDp+49rWO1KfWivL5aA30y5kkYbFZ/CS05fgbO7ogMjLSajpz+RN/zzW79v3q4YfikrMgaD23InA==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/markdown": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.17.1.tgz", - "integrity": "sha512-uexR9snyT54jfQTrbr/GZAtzX+8Oyykr4p1HS0vCVL1KU5MDuP2PoyFfOv3rcfB2TASc+aYiINhU2gSXzwCHNg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.20.0.tgz", + "integrity": "sha512-ZoGsECejp9z6MEvc8l81b1h1aWbB3sTq6xOFeUTbDL5vKpA67z5CmQQLi0uZWrygrbO9dSE3Q/JGcodUrczxbw==", + "license": "MIT", "dependencies": { - "@lexical/code": "0.17.1", - "@lexical/link": "0.17.1", - "@lexical/list": "0.17.1", - "@lexical/rich-text": "0.17.1", - "@lexical/text": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/offset": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.17.1.tgz", - "integrity": "sha512-fX0ZSIFWwUKAjxf6l21vyXFozJGExKWyWxA+EMuOloNAGotHnAInxep0Mt8t/xcvHs7luuyQUxEPw7YrTJP7aw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.20.0.tgz", + "integrity": "sha512-VMhxsxxDGnpVw0jgC8UlDf0Q2RHIHbS49uZgs3l9nP+O+G8s3b76Ta4Tb+iJOK2FY6874/TcQMbSuXGhfpQk8A==", + "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.20.0" } }, "node_modules/@lexical/overflow": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.17.1.tgz", - "integrity": "sha512-oElVDq486R3rO2+Zz0EllXJGpW3tN0tfcH+joZ5h36+URKuNeKddqkJuDRvgSLOr9l8Jhtv3+/YKduPJVKMz6w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.20.0.tgz", + "integrity": "sha512-z4lElzLm1FVifc7bzBZN4VNKeTuwygpyHQvCJVWXzF2Kbvex43PEYMi8u4A83idVqbmzbyBLASwUJS0voLoPLw==", + "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.20.0" } }, "node_modules/@lexical/plain-text": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.17.1.tgz", - "integrity": "sha512-CSvi4j1a4ame0OAvOKUCCmn2XrNsWcST4lExGTa9Ei/VIh8IZ+a97h4Uby8T3lqOp10x+oiizYWzY30pb9QaBg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.20.0.tgz", + "integrity": "sha512-LvoC+9mm2Im1iO8GgtgaqSfW0T3mIE5GQl1xGxbVNdANmtHmBgRAJn2KfQm1XHZP6zydLRMhZkzC+jfInh2yfQ==", + "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/react": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.17.1.tgz", - "integrity": "sha512-DI4k25tO0E1WyozrjaLgKMOmLjOB7+39MT4eZN9brPlU7g+w0wzdGbTZUPgPmFGIKPK+MSLybCwAJCK97j8HzQ==", - "dependencies": { - "@lexical/clipboard": "0.17.1", - "@lexical/code": "0.17.1", - "@lexical/devtools-core": "0.17.1", - "@lexical/dragon": "0.17.1", - "@lexical/hashtag": "0.17.1", - "@lexical/history": "0.17.1", - "@lexical/link": "0.17.1", - "@lexical/list": "0.17.1", - "@lexical/mark": "0.17.1", - "@lexical/markdown": "0.17.1", - "@lexical/overflow": "0.17.1", - "@lexical/plain-text": "0.17.1", - "@lexical/rich-text": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/table": "0.17.1", - "@lexical/text": "0.17.1", - "@lexical/utils": "0.17.1", - "@lexical/yjs": "0.17.1", - "lexical": "0.17.1", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.20.0.tgz", + "integrity": "sha512-5QbN5AFtZ9efXxU/M01ADhUZgthR0e8WKi5K/w5EPpWtYFDPQnUte3rKUjYJ7uwG1iwcvaCpuMbxJjHQ+i6pDQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -1167,59 +1182,67 @@ } }, "node_modules/@lexical/rich-text": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.17.1.tgz", - "integrity": "sha512-T3kvj4P1OpedX9jvxN3WN8NP1Khol6mCW2ScFIRNRz2dsXgyN00thH1Q1J/uyu7aKyGS7rzcY0rb1Pz1qFufqQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.20.0.tgz", + "integrity": "sha512-BR1pACdMA+Ymef0f5EN1y+9yP8w7S+9MgmBP1yjr3w4KdqRnfSaGWyxwcHU8eA+zu16QfivpB6501VJ90YeuXw==", + "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/selection": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.17.1.tgz", - "integrity": "sha512-qBKVn+lMV2YIoyRELNr1/QssXx/4c0id9NCB/BOuYlG8du5IjviVJquEF56NEv2t0GedDv4BpUwkhXT2QbNAxA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.20.0.tgz", + "integrity": "sha512-YnkH5UCMNN/em95or/6uwAV31vcENh1Roj+JOg5KD+gJuA7VGdDCy0vZl/o0+1badXozeZ2VRxXNC6JSK7T4+A==", + "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.20.0" } }, "node_modules/@lexical/table": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.17.1.tgz", - "integrity": "sha512-2fUYPmxhyuMQX3MRvSsNaxbgvwGNJpHaKx1Ldc+PT2MvDZ6ALZkfsxbi0do54Q3i7dOon8/avRp4TuVaCnqvoA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.20.0.tgz", + "integrity": "sha512-qHuK2rvQUoQDx62YpvJE3Ev4yK9kjRFo79IDBapxrhoXg/wCGQOjMBzVD3G5PWkhyl/GDnww80GwYjLloQLQzg==", + "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/text": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.17.1.tgz", - "integrity": "sha512-zD2pAGXaMfPpT8PeNrx3+n0+jGnQORHyn0NEBO+hnyacKfUq5z5sI6Gebsq5NwH789bRadmJM5LvX5w8fsuv6w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.20.0.tgz", + "integrity": "sha512-Fu64i5CIlEOlgucSdp9XFqB2XqoRsw4at76n93+6RF4+LgGDnu4nLXQVCVxNmLcGyh2WgczuTpnk5P2mHNAIUA==", + "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.20.0" } }, "node_modules/@lexical/utils": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.17.1.tgz", - "integrity": "sha512-jCQER5EsvhLNxKH3qgcpdWj/necUb82Xjp8qWQ3c0tyL07hIRm2tDRA/s9mQmvcP855HEZSmGVmR5SKtkcEAVg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.20.0.tgz", + "integrity": "sha512-sXIa2nowrNxY8VcjjuxZbJ/HovIql8bmInNaxBR03JAYfqMiL5I5/dYgjOQJV49NJnuR1uTY2GwVxVTXCTFUCw==", + "license": "MIT", "dependencies": { - "@lexical/list": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/table": "0.17.1", - "lexical": "0.17.1" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "node_modules/@lexical/yjs": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.17.1.tgz", - "integrity": "sha512-9mn5PDtaH5uLMH6hQ59EAx5FkRzmJJFcVs3E6zSIbtgkG3UASR3CFEfgsLKTjl/GC5NnTGuMck+jXaupDVBhOg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.20.0.tgz", + "integrity": "sha512-TiHNhu2VkhXN69V+fXVS3xjOQ6aLnheQUGwOAhuFkDPL3VLCb0yl2Mgydpayn+3Grwii4ZBHcF7oCC84GiU5bw==", + "license": "MIT", "dependencies": { - "@lexical/offset": "0.17.1", - "lexical": "0.17.1" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -2476,9 +2499,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3456,6 +3480,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", "peer": true, "funding": { "type": "GitHub Sponsors ❤", @@ -3527,14 +3552,16 @@ } }, "node_modules/lexical": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.17.1.tgz", - "integrity": "sha512-72/MhR7jqmyqD10bmJw8gztlCm4KDDT+TPtU4elqXrEvHoO5XENi34YAEUD9gIkPfqSwyLa9mwAX1nKzIr5xEA==" + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.20.0.tgz", + "integrity": "sha512-lJEHLFACXqRf3u/VlIOu9T7MJ51O4la92uOBwiS9Sx+juDK3Nrru5Vgl1aUirV1qK8XEM3h6Org2HcrsrzZ3ZA==", + "license": "MIT" }, "node_modules/lib0": { - "version": "0.2.97", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.97.tgz", - "integrity": "sha512-Q4d1ekgvufi9FiHkkL46AhecfNjznSL9MRNoJRQ76gBHS9OqU2ArfQK0FvBpuxgWeJeNI0LVgAYMIpsGeX4gYg==", + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "license": "MIT", "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" @@ -5129,15 +5156,17 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "license": "MIT", "optional": true, + "peer": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -7516,6 +7545,22 @@ "prettier": "2.8.7" } }, + "node_modules/yaml-language-server/node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "license": "MIT", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/yaml-language-server/node_modules/request-light": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", @@ -7625,12 +7670,13 @@ } }, "node_modules/yjs": { - "version": "13.6.19", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.19.tgz", - "integrity": "sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==", + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "license": "MIT", "peer": true, "dependencies": { - "lib0": "^0.2.86" + "lib0": "^0.2.98" }, "engines": { "node": ">=16.0.0", From d9014f328a92a3486fef2b345555c9772f1ec53c Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo <82163647+Shopiley@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:04:53 +0100 Subject: [PATCH 102/133] [Lexical] Chore: Update default skipInitialization to false for registerMutationListener (#6857) --- packages/lexical-website/docs/concepts/listeners.md | 2 +- packages/lexical/src/LexicalEditor.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lexical-website/docs/concepts/listeners.md b/packages/lexical-website/docs/concepts/listeners.md index 834db37c0bf..ef95f1c6d0b 100644 --- a/packages/lexical-website/docs/concepts/listeners.md +++ b/packages/lexical-website/docs/concepts/listeners.md @@ -84,7 +84,7 @@ handle external UI state and UI features relating to specific types of node. If any existing nodes are in the DOM, and skipInitialization is not true, the listener will be called immediately with an updateTag of 'registerMutationListener' where all nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option -(default is currently true for backwards compatibility in 0.17.x but will change to false in 0.18.0). +(whose default was previously true for backwards compatibility with <=0.16.1 but has been changed to false as of 0.21.0). ```js const removeMutationListener = editor.registerMutationListener( diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 174b18cb62c..9223e571544 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -216,13 +216,13 @@ export interface MutationListenerOptions { /** * Skip the initial call of the listener with pre-existing DOM nodes. * - * The default is currently true for backwards compatibility with <= 0.16.1 - * but this default is expected to change to false in 0.17.0. + * The default was previously true for backwards compatibility with <= 0.16.1 + * but this default has been changed to false as of 0.21.0. */ skipInitialization?: boolean; } -const DEFAULT_SKIP_INITIALIZATION = true; +const DEFAULT_SKIP_INITIALIZATION = false; export type UpdateListener = (arg0: { dirtyElements: Map; @@ -844,7 +844,7 @@ export class LexicalEditor { * If any existing nodes are in the DOM, and skipInitialization is not true, the listener * will be called immediately with an updateTag of 'registerMutationListener' where all * nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option - * (default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0). + * (whose default was previously true for backwards compatibility with <=0.16.1 but has been changed to false as of 0.21.0). * * @param klass - The class of the node that you want to listen to mutations on. * @param listener - The logic you want to run when the node is mutated. From fcb76667080c49d67148f480d549ed7dbf647e4e Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 24 Nov 2024 10:58:06 -0800 Subject: [PATCH 103/133] [lexical-table] Bug Fix: Resolve table selection issue when the mouse crosses over a portal (#6834) --- package-lock.json | 61 +- packages/lexical-clipboard/src/clipboard.ts | 5 +- .../__tests__/e2e/Tables.spec.mjs | 5 - .../4697-repeated-table-selection.spec.mjs | 2 + .../__tests__/utils/index.mjs | 56 +- packages/lexical-playground/package.json | 3 +- packages/lexical-playground/src/index.css | 13 +- .../src/plugins/CommentPlugin/index.tsx | 3 +- .../FloatingLinkEditorPlugin/index.tsx | 3 +- .../FloatingTextFormatToolbarPlugin/index.tsx | 5 +- .../src/plugins/ImagesPlugin/index.tsx | 5 +- .../src/plugins/InlineImagePlugin/index.tsx | 5 +- .../plugins/TableActionMenuPlugin/index.tsx | 154 ++-- .../src/plugins/TestRecorderPlugin/index.tsx | 13 +- packages/lexical-playground/src/setupEnv.ts | 4 + packages/lexical-playground/vite.config.ts | 2 + .../lexical-playground/vite.prod.config.ts | 2 + .../viteCopyExcalidrawAssets.ts | 56 ++ .../src/LexicalTypeaheadMenuPlugin.tsx | 3 +- .../lexical-table/flow/LexicalTable.js.flow | 10 +- .../lexical-table/src/LexicalTableObserver.ts | 362 +++++----- .../src/LexicalTableSelection.ts | 306 ++++---- .../src/LexicalTableSelectionHelpers.ts | 668 ++++++++++++------ .../lexical-table/src/LexicalTableUtils.ts | 126 +++- packages/lexical/src/LexicalEvents.ts | 6 +- packages/lexical/src/LexicalUtils.ts | 7 + packages/lexical/src/index.ts | 1 + packages/shared/viteModuleResolution.ts | 3 +- 28 files changed, 1252 insertions(+), 637 deletions(-) create mode 100644 packages/lexical-playground/viteCopyExcalidrawAssets.ts diff --git a/package-lock.json b/package-lock.json index 25e868bc531..99142fd872d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36908,6 +36908,38 @@ "vite": "^2" } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.1.0.tgz", + "integrity": "sha512-n8lEOIVM00Y/zronm0RG8RdPyFd0SAAFR0sii3NWmgG3PSCyYMsvUNRQTlb3onp1XeMrKIDwCrPGxthKvqX9OQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -39137,7 +39169,8 @@ "@vitejs/plugin-react": "^4.2.1", "rollup-plugin-copy": "^3.5.0", "vite": "^5.2.11", - "vite-plugin-replace": "^0.1.1" + "vite-plugin-replace": "^0.1.1", + "vite-plugin-static-copy": "^2.1.0" } }, "packages/lexical-react": { @@ -56239,6 +56272,7 @@ "rollup-plugin-copy": "^3.5.0", "vite": "^5.2.11", "vite-plugin-replace": "^0.1.1", + "vite-plugin-static-copy": "^2.1.0", "y-websocket": "^1.5.4", "yjs": ">=13.5.42" } @@ -64710,6 +64744,31 @@ "integrity": "sha512-v+okl3JNt2pf1jDYijw+WPVt6h9FWa/atTi+qnSFBqmKThLTDhlesx0r3bh+oFPmxRJmis5tNx9HtN6lGFoqWg==", "dev": true }, + "vite-plugin-static-copy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.1.0.tgz", + "integrity": "sha512-n8lEOIVM00Y/zronm0RG8RdPyFd0SAAFR0sii3NWmgG3PSCyYMsvUNRQTlb3onp1XeMrKIDwCrPGxthKvqX9OQ==", + "dev": true, + "requires": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, "vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index 3de6860e998..9f9155be596 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -22,6 +22,7 @@ import { BaseSelection, COMMAND_PRIORITY_CRITICAL, COPY_COMMAND, + getDOMSelection, isSelectionWithinEditor, LexicalEditor, LexicalNode, @@ -29,12 +30,8 @@ import { SerializedElementNode, SerializedTextNode, } from 'lexical'; -import {CAN_USE_DOM} from 'shared/canUseDOM'; import invariant from 'shared/invariant'; -const getDOMSelection = (targetWindow: Window | null): Selection | null => - CAN_USE_DOM ? (targetWindow || window).getSelection() : null; - export interface LexicalClipboardData { 'text/html'?: string | undefined; 'application/x-lexical-editor'?: string | undefined; diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index d9362c28aed..689897d45be 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -2274,7 +2274,6 @@ test.describe.parallel('Tables', () => {


`, ); - await unmergeTableCell(page); await assertHTML( page, @@ -3447,8 +3446,6 @@ test.describe.parallel('Tables', () => { `, }); - await page.pause(); - await assertHTML( page, html` @@ -3537,8 +3534,6 @@ test.describe.parallel('Tables', () => { `, }); - await page.pause(); - await assertHTML( page, html` diff --git a/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs b/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs index 9711bab390b..00c34ff7c27 100644 --- a/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs @@ -38,6 +38,7 @@ test.describe('Regression test #4697', () => { false, false, ); + await page.pause(); await selectCellsFromTableCords( page, @@ -46,6 +47,7 @@ test.describe('Regression test #4697', () => { false, false, ); + await page.pause(); await assertTableSelectionCoordinates(page, { anchor: {x: 2, y: 1}, diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 9f43f61e5fd..b8b79e9e76c 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -396,7 +396,7 @@ async function assertSelectionOnPageOrFrame(page, expected) { focusOffset: fixOffset(focusNode, focusOffset), focusPath: getPathFromNode(focusNode), }; - }, expected); + }); expect(selection.anchorPath).toEqual(expected.anchorPath); expect(selection.focusPath).toEqual(expected.focusPath); if (Array.isArray(expected.anchorOffset)) { @@ -738,9 +738,6 @@ export async function dragMouse( fromX += fromBoundingBox.width; fromY += fromBoundingBox.height; } - await page.mouse.move(fromX, fromY); - await page.mouse.down(); - let toX = toBoundingBox.x; let toY = toBoundingBox.y; if (positionEnd === 'middle') { @@ -751,13 +748,9 @@ export async function dragMouse( toY += toBoundingBox.height; } - if (slow) { - //simulate more than 1 mouse move event to replicate human slow dragging - await page.mouse.move((fromX + toX) / 2, (fromY + toY) / 2); - } - - await page.mouse.move(toX, toY); - + await page.mouse.move(fromX, fromY); + await page.mouse.down(); + await page.mouse.move(toX, toY, slow ? 10 : 1); if (mouseUp) { await page.mouse.up(); } @@ -907,72 +900,75 @@ export async function selectCellsFromTableCords( }:nth-child(${secondCords.x + 1})`, ); - // Focus on inside the iFrame or the boundingBox() below returns null. await firstRowFirstColumnCell.click(); + await page.keyboard.down('Shift'); + await secondRowSecondCell.click(); + await page.keyboard.up('Shift'); - await dragMouse( + // const firstBox = await firstRowFirstColumnCell.boundingBox(); + // const secondBox = await secondRowSecondCell.boundingBox(); + // await dragMouse(page, firstBox, secondBox, 'middle', 'middle', true, true); +} + +export async function clickTableCellActiveButton(page) { + await click( page, - await firstRowFirstColumnCell.boundingBox(), - await secondRowSecondCell.boundingBox(), - 'middle', - 'middle', - true, - true, + '.table-cell-action-button-container--active > .table-cell-action-button', ); } export async function insertTableRowAbove(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-insert-row-above"]'); } export async function insertTableRowBelow(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-insert-row-below"]'); } export async function insertTableColumnBefore(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-insert-column-before"]'); } export async function insertTableColumnAfter(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-insert-column-after"]'); } export async function mergeTableCells(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-merge-cells"]'); } export async function unmergeTableCell(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-unmerge-cells"]'); } export async function toggleColumnHeader(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-column-header"]'); } export async function deleteTableRows(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-delete-rows"]'); } export async function deleteTableColumns(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-delete-columns"]'); } export async function deleteTable(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-delete"]'); } export async function setBackgroundColor(page) { - await click(page, '.table-cell-action-button-container'); + await clickTableCellActiveButton(page); await click(page, '.item[data-test-id="table-background-color"]'); } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 244dbe2a692..7c298206228 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -45,7 +45,8 @@ "@vitejs/plugin-react": "^4.2.1", "rollup-plugin-copy": "^3.5.0", "vite": "^5.2.11", - "vite-plugin-replace": "^0.1.1" + "vite-plugin-replace": "^0.1.1", + "vite-plugin-static-copy": "^2.1.0" }, "sideEffects": false } diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 31718a446af..87aeb604a82 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -1307,14 +1307,23 @@ i.page-break, left: 0; will-change: transform; } +.table-cell-action-button-container.table-cell-action-button-container--active { + pointer-events: auto; + opacity: 1; +} +.table-cell-action-button-container.table-cell-action-button-container--inactive { + pointer-events: none; + opacity: 0; +} .table-cell-action-button { - background-color: none; display: flex; justify-content: center; align-items: center; border: 0; - position: relative; + position: absolute; + top: 10px; + right: 10px; border-radius: 15px; color: #222; display: inline-block; diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx index 67aa66662c5..2367d0a165e 100644 --- a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx @@ -47,6 +47,7 @@ import { CLEAR_EDITOR_COMMAND, COMMAND_PRIORITY_EDITOR, createCommand, + getDOMSelection, KEY_ESCAPE_COMMAND, } from 'lexical'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -923,7 +924,7 @@ export default function CommentPlugin({ editor.registerCommand( INSERT_INLINE_COMMAND, () => { - const domSelection = window.getSelection(); + const domSelection = getDOMSelection(editor._window); if (domSelection !== null) { domSelection.removeAllRanges(); } diff --git a/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx index 4dd0cb2fce7..d3a3fef97ed 100644 --- a/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx @@ -24,6 +24,7 @@ import { COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, + getDOMSelection, KEY_ESCAPE_COMMAND, LexicalEditor, SELECTION_CHANGE_COMMAND, @@ -77,7 +78,7 @@ function FloatingLinkEditor({ } } const editorElem = editorRef.current; - const nativeSelection = window.getSelection(); + const nativeSelection = getDOMSelection(editor._window); const activeElement = document.activeElement; if (editorElem === null) { diff --git a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx index 8c9795b09ef..2404f88dca9 100644 --- a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -19,6 +19,7 @@ import { $isTextNode, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, + getDOMSelection, LexicalEditor, SELECTION_CHANGE_COMMAND, } from 'lexical'; @@ -113,7 +114,7 @@ function TextFormatFloatingToolbar({ const selection = $getSelection(); const popupCharStylesEditorElem = popupCharStylesEditorRef.current; - const nativeSelection = window.getSelection(); + const nativeSelection = getDOMSelection(editor._window); if (popupCharStylesEditorElem === null) { return; @@ -293,7 +294,7 @@ function useFloatingTextFormatToolbar( return; } const selection = $getSelection(); - const nativeSelection = window.getSelection(); + const nativeSelection = getDOMSelection(editor._window); const rootElement = editor.getRootElement(); if ( diff --git a/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx b/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx index cceabbc52e9..b2c2120220e 100644 --- a/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx @@ -22,12 +22,12 @@ import { DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, + getDOMSelection, LexicalCommand, LexicalEditor, } from 'lexical'; import {useEffect, useRef, useState} from 'react'; import * as React from 'react'; -import {CAN_USE_DOM} from 'shared/canUseDOM'; import landscapeImage from '../../images/landscape.jpg'; import yellowFlowerImage from '../../images/yellow-flower.jpg'; @@ -44,9 +44,6 @@ import TextInput from '../../ui/TextInput'; export type InsertImagePayload = Readonly; -const getDOMSelection = (targetWindow: Window | null): Selection | null => - CAN_USE_DOM ? (targetWindow || window).getSelection() : null; - export const INSERT_IMAGE_COMMAND: LexicalCommand = createCommand('INSERT_IMAGE_COMMAND'); diff --git a/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx b/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx index c09c7987aa8..98cea7f6888 100644 --- a/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx @@ -26,12 +26,12 @@ import { DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, + getDOMSelection, LexicalCommand, LexicalEditor, } from 'lexical'; import * as React from 'react'; import {useEffect, useRef, useState} from 'react'; -import {CAN_USE_DOM} from 'shared/canUseDOM'; import { $createInlineImageNode, @@ -47,9 +47,6 @@ import TextInput from '../../ui/TextInput'; export type InsertInlineImagePayload = Readonly; -const getDOMSelection = (targetWindow: Window | null): Selection | null => - CAN_USE_DOM ? (targetWindow || window).getSelection() : null; - export const INSERT_INLINE_IMAGE_COMMAND: LexicalCommand = createCommand('INSERT_INLINE_IMAGE_COMMAND'); diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 2a3fc2bdff1..2e17272cbf7 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -28,9 +28,11 @@ import { getTableObserverFromTableElement, TableCellHeaderStates, TableCellNode, + TableObserver, TableRowNode, TableSelection, } from '@lexical/table'; +import {mergeRegister} from '@lexical/utils'; import { $createParagraphNode, $getRoot, @@ -39,6 +41,9 @@ import { $isParagraphNode, $isRangeSelection, $isTextNode, + COMMAND_PRIORITY_CRITICAL, + getDOMSelection, + SELECTION_CHANGE_COMMAND, } from 'lexical'; import * as React from 'react'; import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react'; @@ -242,7 +247,7 @@ function TableActionMenu({ const tableObserver = getTableObserverFromTableElement(tableElement); if (tableObserver !== null) { - tableObserver.clearHighlight(); + tableObserver.$clearHighlight(); } tableNode.markDirty(); @@ -630,8 +635,8 @@ function TableCellActionMenuContainer({ }): JSX.Element { const [editor] = useLexicalComposerContext(); - const menuButtonRef = useRef(null); - const menuRootRef = useRef(null); + const menuButtonRef = useRef(null); + const menuRootRef = useRef(null); const [isMenuOpen, setIsMenuOpen] = useState(false); const [tableCellNode, setTableMenuCellNode] = useState( @@ -643,15 +648,23 @@ function TableCellActionMenuContainer({ const $moveMenu = useCallback(() => { const menu = menuButtonRef.current; const selection = $getSelection(); - const nativeSelection = window.getSelection(); + const nativeSelection = getDOMSelection(editor._window); const activeElement = document.activeElement; + function disable() { + if (menu) { + menu.classList.remove('table-cell-action-button-container--active'); + menu.classList.add('table-cell-action-button-container--inactive'); + } + setTableMenuCellNode(null); + } if (selection == null || menu == null) { - setTableMenuCellNode(null); - return; + return disable(); } const rootElement = editor.getRootElement(); + let tableObserver: TableObserver | null = null; + let tableCellParentNodeDOM: HTMLElement | null = null; if ( $isRangeSelection(selection) && @@ -664,56 +677,111 @@ function TableCellActionMenuContainer({ ); if (tableCellNodeFromSelection == null) { - setTableMenuCellNode(null); - return; + return disable(); } - const tableCellParentNodeDOM = editor.getElementByKey( + tableCellParentNodeDOM = editor.getElementByKey( tableCellNodeFromSelection.getKey(), ); - if (tableCellParentNodeDOM == null) { - setTableMenuCellNode(null); - return; + if ( + tableCellParentNodeDOM == null || + !tableCellNodeFromSelection.isAttached() + ) { + return disable(); } + const tableNode = $getTableNodeFromLexicalNodeOrThrow( + tableCellNodeFromSelection, + ); + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNode.getKey()), + ); + + invariant( + tableElement !== null, + 'TableActionMenu: Expected to find tableElement in DOM', + ); + + tableObserver = getTableObserverFromTableElement(tableElement); setTableMenuCellNode(tableCellNodeFromSelection); + } else if ($isTableSelection(selection)) { + const anchorNode = $getTableCellNodeFromLexicalNode( + selection.anchor.getNode(), + ); + invariant( + $isTableCellNode(anchorNode), + 'TableSelection anchorNode must be a TableCellNode', + ); + const tableNode = $getTableNodeFromLexicalNodeOrThrow(anchorNode); + const tableElement = getTableElement( + tableNode, + editor.getElementByKey(tableNode.getKey()), + ); + invariant( + tableElement !== null, + 'TableActionMenu: Expected to find tableElement in DOM', + ); + tableObserver = getTableObserverFromTableElement(tableElement); + tableCellParentNodeDOM = editor.getElementByKey(anchorNode.getKey()); } else if (!activeElement) { - setTableMenuCellNode(null); + return disable(); } - }, [editor]); - - useEffect(() => { - return editor.registerUpdateListener(() => { - editor.getEditorState().read(() => { - $moveMenu(); - }); - }); - }); + if (tableObserver === null || tableCellParentNodeDOM === null) { + return disable(); + } + const enabled = !tableObserver || !tableObserver.isSelecting; + menu.classList.toggle( + 'table-cell-action-button-container--active', + enabled, + ); + menu.classList.toggle( + 'table-cell-action-button-container--inactive', + !enabled, + ); + if (enabled) { + const tableCellRect = tableCellParentNodeDOM.getBoundingClientRect(); + const anchorRect = anchorElem.getBoundingClientRect(); + const top = tableCellRect.top - anchorRect.top; + const left = tableCellRect.right - anchorRect.left; + menu.style.transform = `translate(${left}px, ${top}px)`; + } + }, [editor, anchorElem]); useEffect(() => { - const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null; - - if (menuButtonDOM != null && tableCellNode != null) { - const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey()); - - if (tableCellNodeDOM != null) { - const tableCellRect = tableCellNodeDOM.getBoundingClientRect(); - const menuRect = menuButtonDOM.getBoundingClientRect(); - const anchorRect = anchorElem.getBoundingClientRect(); - - const top = tableCellRect.top - anchorRect.top + 4; - const left = - tableCellRect.right - menuRect.width - 10 - anchorRect.left; - - menuButtonDOM.style.opacity = '1'; - menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`; - } else { - menuButtonDOM.style.opacity = '0'; - menuButtonDOM.style.transform = 'translate(-10000px, -10000px)'; + // We call the $moveMenu callback every time the selection changes, + // once up front, and once after each mouseUp + let timeoutId: ReturnType | undefined = undefined; + const callback = () => { + timeoutId = undefined; + editor.getEditorState().read($moveMenu); + }; + const delayedCallback = () => { + if (timeoutId === undefined) { + timeoutId = setTimeout(callback, 0); } - } - }, [menuButtonRef, tableCellNode, editor, anchorElem]); + return false; + }; + return mergeRegister( + editor.registerUpdateListener(delayedCallback), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + delayedCallback, + COMMAND_PRIORITY_CRITICAL, + ), + editor.registerRootListener((rootElement, prevRootElement) => { + if (prevRootElement) { + prevRootElement.removeEventListener('mouseup', delayedCallback); + } + if (rootElement) { + rootElement.addEventListener('mouseup', delayedCallback); + delayedCallback(); + } + }), + () => clearTimeout(timeoutId), + ); + }); const prevTableCellDOM = useRef(tableCellNode); diff --git a/packages/lexical-playground/src/plugins/TestRecorderPlugin/index.tsx b/packages/lexical-playground/src/plugins/TestRecorderPlugin/index.tsx index 838568397f3..c97b3de3248 100644 --- a/packages/lexical-playground/src/plugins/TestRecorderPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TestRecorderPlugin/index.tsx @@ -9,7 +9,12 @@ import type {BaseSelection, LexicalEditor} from 'lexical'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + getDOMSelection, +} from 'lexical'; import * as React from 'react'; import {useCallback, useEffect, useRef, useState} from 'react'; import {IS_APPLE} from 'shared/environment'; @@ -167,7 +172,7 @@ function useTestRecorder( const generateTestContent = useCallback(() => { const rootElement = editor.getRootElement(); - const browserSelection = window.getSelection(); + const browserSelection = getDOMSelection(editor._window); if ( rootElement == null || @@ -322,7 +327,7 @@ ${steps.map(formatStep).join(`\n`)} dirtyElements.size === 0 && !skipNextSelectionChange ) { - const browserSelection = window.getSelection(); + const browserSelection = getDOMSelection(editor._window); if ( browserSelection && (browserSelection.anchorNode == null || @@ -379,7 +384,7 @@ ${steps.map(formatStep).join(`\n`)} if (!isRecording) { return; } - const browserSelection = window.getSelection(); + const browserSelection = getDOMSelection(getCurrentEditor()._window); if ( browserSelection === null || browserSelection.anchorNode == null || diff --git a/packages/lexical-playground/src/setupEnv.ts b/packages/lexical-playground/src/setupEnv.ts index 076cd430a9c..abf4b21ded3 100644 --- a/packages/lexical-playground/src/setupEnv.ts +++ b/packages/lexical-playground/src/setupEnv.ts @@ -30,5 +30,9 @@ export default (() => { // @ts-expect-error delete window.InputEvent.prototype.getTargetRanges; } + + // @ts-ignore + window.EXCALIDRAW_ASSET_PATH = process.env.EXCALIDRAW_ASSET_PATH; + return INITIAL_SETTINGS; })(); diff --git a/packages/lexical-playground/vite.config.ts b/packages/lexical-playground/vite.config.ts index 12490da5cf2..82018c7c74b 100644 --- a/packages/lexical-playground/vite.config.ts +++ b/packages/lexical-playground/vite.config.ts @@ -15,6 +15,7 @@ import {replaceCodePlugin} from 'vite-plugin-replace'; import moduleResolution from '../shared/viteModuleResolution'; import viteCopyEsm from './viteCopyEsm'; +import viteCopyExcalidrawAssets from './viteCopyExcalidrawAssets'; const require = createRequire(import.meta.url); @@ -76,6 +77,7 @@ export default defineConfig(({command}) => { presets: [['@babel/preset-react', {runtime: 'automatic'}]], }), react(), + ...viteCopyExcalidrawAssets(), viteCopyEsm(), commonjs({ // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514) diff --git a/packages/lexical-playground/vite.prod.config.ts b/packages/lexical-playground/vite.prod.config.ts index 97fc5a760cd..59c8f368f05 100644 --- a/packages/lexical-playground/vite.prod.config.ts +++ b/packages/lexical-playground/vite.prod.config.ts @@ -14,6 +14,7 @@ import {replaceCodePlugin} from 'vite-plugin-replace'; import moduleResolution from '../shared/viteModuleResolution'; import viteCopyEsm from './viteCopyEsm'; +import viteCopyExcalidrawAssets from './viteCopyExcalidrawAssets'; // https://vitejs.dev/config/ export default defineConfig({ @@ -69,6 +70,7 @@ export default defineConfig({ presets: [['@babel/preset-react', {runtime: 'automatic'}]], }), react(), + ...viteCopyExcalidrawAssets(), viteCopyEsm(), commonjs({ // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514) diff --git a/packages/lexical-playground/viteCopyExcalidrawAssets.ts b/packages/lexical-playground/viteCopyExcalidrawAssets.ts new file mode 100644 index 00000000000..3e19da05892 --- /dev/null +++ b/packages/lexical-playground/viteCopyExcalidrawAssets.ts @@ -0,0 +1,56 @@ +/** + * 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 {Plugin} from 'vite'; + +import {createRequire} from 'node:module'; +import * as path from 'node:path'; +import {normalizePath} from 'vite'; +import {Target, viteStaticCopy} from 'vite-plugin-static-copy'; + +const require = createRequire(import.meta.url); + +export default function viteCopyExcalidrawAssets(): Plugin[] { + const targets: Target[] = [ + 'excalidraw-assets', + 'excalidraw-assets-dev', + ].flatMap((assetDir) => { + const srcDir = path.join( + require.resolve('@excalidraw/excalidraw'), + '..', + 'dist', + assetDir, + ); + return [ + { + dest: `${assetDir}/`, + src: [path.join(srcDir, '*.js'), path.join(srcDir, '*.woff2')].map( + normalizePath, + ), + }, + { + dest: `${assetDir}/locales/`, + src: [path.join(srcDir, 'locales', '*.js')].map(normalizePath), + }, + ]; + }); + return [ + { + config() { + return { + define: { + 'process.env.EXCALIDRAW_ASSET_PATH': JSON.stringify('/'), + }, + }; + }, + name: 'viteCopyExcalidrawAssets', + }, + ...viteStaticCopy({ + targets, + }), + ]; +} diff --git a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx index c8958d7aa51..da497dc9aad 100644 --- a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx @@ -21,6 +21,7 @@ import { COMMAND_PRIORITY_LOW, CommandListenerPriority, createCommand, + getDOMSelection, LexicalCommand, LexicalEditor, RangeSelection, @@ -53,7 +54,7 @@ function tryToPositionRange( range: Range, editorWindow: Window, ): boolean { - const domSelection = editorWindow.getSelection(); + const domSelection = getDOMSelection(editorWindow); if (domSelection === null || !domSelection.isCollapsed) { return false; } diff --git a/packages/lexical-table/flow/LexicalTable.js.flow b/packages/lexical-table/flow/LexicalTable.js.flow index 19014ebd897..2674a125f50 100644 --- a/packages/lexical-table/flow/LexicalTable.js.flow +++ b/packages/lexical-table/flow/LexicalTable.js.flow @@ -286,11 +286,11 @@ declare export class TableObserver { getTable(): TableDOMTable; removeListeners(): void; trackTable(): void; - clearHighlight(): void; - setFocusCellForSelection(cell: TableDOMCell): void; - setAnchorCellForSelection(cell: TableDOMCell): void; - formatCells(type: TextFormatType): void; - clearText(): void; + $clearHighlight(): void; + $setFocusCellForSelection(cell: TableDOMCell, ignoreStart?: boolean): void; + $setAnchorCellForSelection(cell: TableDOMCell): void; + $formatCells(type: TextFormatType): void; + $clearText(): void; } /** diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 9d3ffbbc690..0d148257121 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -24,6 +24,7 @@ import { $isElementNode, $isParagraphNode, $setSelection, + getDOMSelection, SELECTION_CHANGE_COMMAND, } from 'lexical'; import invariant from 'shared/invariant'; @@ -32,13 +33,13 @@ import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; import {$isTableNode, TableNode} from './LexicalTableNode'; import { $createTableSelection, + $createTableSelectionFrom, $isTableSelection, type TableSelection, } from './LexicalTableSelection'; import { $findTableNode, $updateDOMForSelection, - getDOMSelection, getTable, getTableElement, HTMLTableElementWithWithTableSelectionState, @@ -105,6 +106,7 @@ export class TableObserver { shouldCheckSelection: boolean; abortController: AbortController; listenerOptions: {signal: AbortSignal}; + nextFocus: {focusCell: TableDOMCell; override: boolean} | null; constructor(editor: LexicalEditor, tableNodeKey: string) { this.isHighlightingCells = false; @@ -130,6 +132,7 @@ export class TableObserver { this.shouldCheckSelection = false; this.abortController = new AbortController(); this.listenerOptions = {signal: this.abortController.signal}; + this.nextFocus = null; this.trackTable(); } @@ -198,7 +201,7 @@ export class TableObserver { ); } - clearHighlight() { + $clearHighlight(): void { const editor = this.editor; this.isHighlightingCells = false; this.anchorX = -1; @@ -212,61 +215,54 @@ export class TableObserver { this.focusCell = null; this.hasHijackedSelectionStyles = false; - this.enableHighlightStyle(); + this.$enableHighlightStyle(); - editor.update(() => { - const {tableNode, tableElement} = this.$lookup(); - const grid = getTable(tableNode, tableElement); - $updateDOMForSelection(editor, grid, null); + const {tableNode, tableElement} = this.$lookup(); + const grid = getTable(tableNode, tableElement); + $updateDOMForSelection(editor, grid, null); + if ($getSelection() !== null) { $setSelection(null); editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); - }); + } } - enableHighlightStyle() { + $enableHighlightStyle() { const editor = this.editor; - editor.getEditorState().read( - () => { - const {tableElement} = this.$lookup(); + const {tableElement} = this.$lookup(); - removeClassNamesFromElement( - tableElement, - editor._config.theme.tableSelection, - ); - tableElement.classList.remove('disable-selection'); - this.hasHijackedSelectionStyles = false; - }, - {editor}, + removeClassNamesFromElement( + tableElement, + editor._config.theme.tableSelection, ); + tableElement.classList.remove('disable-selection'); + this.hasHijackedSelectionStyles = false; } - disableHighlightStyle() { - const editor = this.editor; - editor.getEditorState().read( - () => { - const {tableElement} = this.$lookup(); - addClassNamesToElement( - tableElement, - editor._config.theme.tableSelection, - ); - this.hasHijackedSelectionStyles = true; - }, - {editor}, + $disableHighlightStyle() { + const {tableElement} = this.$lookup(); + addClassNamesToElement( + tableElement, + this.editor._config.theme.tableSelection, ); + this.hasHijackedSelectionStyles = true; } - updateTableTableSelection(selection: TableSelection | null): void { - if (selection !== null && selection.tableKey === this.tableNodeKey) { + $updateTableTableSelection(selection: TableSelection | null): void { + if (selection !== null) { + invariant( + selection.tableKey === this.tableNodeKey, + "TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('%s' !== '%s')", + selection.tableKey, + this.tableNodeKey, + ); const editor = this.editor; this.tableSelection = selection; this.isHighlightingCells = true; - this.disableHighlightStyle(); + this.$disableHighlightStyle(); + this.updateDOMSelection(); $updateDOMForSelection(editor, this.table, this.tableSelection); - } else if (selection == null) { - this.clearHighlight(); } else { - this.tableNodeKey = selection.tableKey; - this.updateTableTableSelection(selection); + this.$clearHighlight(); } } @@ -292,168 +288,208 @@ export class TableObserver { } return false; } - setFocusCellForSelection(cell: TableDOMCell, ignoreStart = false) { - const editor = this.editor; - editor.update(() => { - const {tableNode} = this.$lookup(); - - const cellX = cell.x; - const cellY = cell.y; - this.focusCell = cell; - - if (this.anchorCell !== null) { - const domSelection = getDOMSelection(editor._window); - // Collapse the selection - if (domSelection) { - domSelection.setBaseAndExtent( - this.anchorCell.elem, - 0, - this.focusCell.elem, - 0, - ); - } - } - if ( - !this.isHighlightingCells && - (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart) - ) { - this.isHighlightingCells = true; - this.disableHighlightStyle(); - } else if (cellX === this.focusX && cellY === this.focusY) { - return; + /** + * @internal + * When handling mousemove events we track what the focus cell should be, but + * the DOM selection may end up somewhere else entirely. We don't have an elegant + * way to handle this after the DOM selection has been resolved in a + * SELECTION_CHANGE_COMMAND callback. + */ + setNextFocus( + nextFocus: null | {focusCell: TableDOMCell; override: boolean}, + ): void { + this.nextFocus = nextFocus; + } + + /** @internal */ + getAndClearNextFocus(): { + focusCell: TableDOMCell; + override: boolean; + } | null { + const {nextFocus} = this; + if (nextFocus !== null) { + this.nextFocus = null; + } + return nextFocus; + } + + /** @internal */ + updateDOMSelection() { + if (this.anchorCell !== null && this.focusCell !== null) { + const domSelection = getDOMSelection(this.editor._window); + // We are not using a native selection for tables, and if we + // set one then the reconciler will undo it. + // TODO - it would make sense to have one so that native + // copy/paste worked. Right now we have to emulate with + // keyboard events but it won't fire if trigged from the menu + if (domSelection && domSelection.rangeCount > 0) { + domSelection.removeAllRanges(); } + } + } + + $setFocusCellForSelection(cell: TableDOMCell, ignoreStart = false): boolean { + const editor = this.editor; + const {tableNode} = this.$lookup(); - this.focusX = cellX; - this.focusY = cellY; + const cellX = cell.x; + const cellY = cell.y; + this.focusCell = cell; - if (this.isHighlightingCells) { - const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + if ( + !this.isHighlightingCells && + (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart) + ) { + this.isHighlightingCells = true; + this.$disableHighlightStyle(); + } else if (cellX === this.focusX && cellY === this.focusY) { + return false; + } - if ( - this.tableSelection != null && - this.anchorCellNodeKey != null && - $isTableCellNode(focusTableCellNode) && - tableNode.is($findTableNode(focusTableCellNode)) - ) { - const focusNodeKey = focusTableCellNode.getKey(); + this.focusX = cellX; + this.focusY = cellY; - this.tableSelection = - this.tableSelection.clone() || $createTableSelection(); + if (this.isHighlightingCells) { + const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem); - this.focusCellNodeKey = focusNodeKey; - this.tableSelection.set( - this.tableNodeKey, - this.anchorCellNodeKey, - this.focusCellNodeKey, - ); + if ( + this.tableSelection != null && + this.anchorCellNodeKey != null && + $isTableCellNode(focusTableCellNode) && + tableNode.is($findTableNode(focusTableCellNode)) + ) { + this.focusCellNodeKey = focusTableCellNode.getKey(); + this.tableSelection = $createTableSelectionFrom( + tableNode, + this.$getAnchorTableCellOrThrow(), + focusTableCellNode, + ); - $setSelection(this.tableSelection); + $setSelection(this.tableSelection); - editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); + editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); - $updateDOMForSelection(editor, this.table, this.tableSelection); - } + $updateDOMForSelection(editor, this.table, this.tableSelection); + return true; } - }); + } + return false; } - setAnchorCellForSelection(cell: TableDOMCell) { + $getAnchorTableCell(): TableCellNode | null { + return this.anchorCellNodeKey + ? $getNodeByKey(this.anchorCellNodeKey) + : null; + } + $getAnchorTableCellOrThrow(): TableCellNode { + const anchorTableCell = this.$getAnchorTableCell(); + invariant( + anchorTableCell !== null, + 'TableObserver anchorTableCell is null', + ); + return anchorTableCell; + } + + $getFocusTableCell(): TableCellNode | null { + return this.focusCellNodeKey ? $getNodeByKey(this.focusCellNodeKey) : null; + } + + $getFocusTableCellOrThrow(): TableCellNode { + const focusTableCell = this.$getFocusTableCell(); + invariant(focusTableCell !== null, 'TableObserver focusTableCell is null'); + return focusTableCell; + } + + $setAnchorCellForSelection(cell: TableDOMCell) { this.isHighlightingCells = false; this.anchorCell = cell; this.anchorX = cell.x; this.anchorY = cell.y; - this.editor.update(() => { - const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem); - if ($isTableCellNode(anchorTableCellNode)) { - const anchorNodeKey = anchorTableCellNode.getKey(); - this.tableSelection = - this.tableSelection != null - ? this.tableSelection.clone() - : $createTableSelection(); - this.anchorCellNodeKey = anchorNodeKey; - } - }); + if ($isTableCellNode(anchorTableCellNode)) { + const anchorNodeKey = anchorTableCellNode.getKey(); + this.tableSelection = + this.tableSelection != null + ? this.tableSelection.clone() + : $createTableSelection(); + this.anchorCellNodeKey = anchorNodeKey; + } } - formatCells(type: TextFormatType) { - this.editor.update(() => { - const selection = $getSelection(); + $formatCells(type: TextFormatType) { + const selection = $getSelection(); - if (!$isTableSelection(selection)) { - invariant(false, 'Expected grid selection'); - } + invariant($isTableSelection(selection), 'Expected Table selection'); - const formatSelection = $createRangeSelection(); + const formatSelection = $createRangeSelection(); - const anchor = formatSelection.anchor; - const focus = formatSelection.focus; + const anchor = formatSelection.anchor; + const focus = formatSelection.focus; - const cellNodes = selection.getNodes().filter($isTableCellNode); - const paragraph = cellNodes[0].getFirstChild(); - const alignFormatWith = $isParagraphNode(paragraph) - ? paragraph.getFormatFlags(type, null) - : null; + const cellNodes = selection.getNodes().filter($isTableCellNode); + invariant(cellNodes.length > 0, 'No table cells present'); + const paragraph = cellNodes[0].getFirstChild(); + const alignFormatWith = $isParagraphNode(paragraph) + ? paragraph.getFormatFlags(type, null) + : null; - cellNodes.forEach((cellNode: TableCellNode) => { - anchor.set(cellNode.getKey(), 0, 'element'); - focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element'); - formatSelection.formatText(type, alignFormatWith); - }); + cellNodes.forEach((cellNode: TableCellNode) => { + anchor.set(cellNode.getKey(), 0, 'element'); + focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element'); + formatSelection.formatText(type, alignFormatWith); + }); - $setSelection(selection); + $setSelection(selection); - this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); - }); + this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); } - clearText() { - const editor = this.editor; - editor.update(() => { - const tableNode = $getNodeByKey(this.tableNodeKey); + $clearText() { + const {editor} = this; + const tableNode = $getNodeByKey(this.tableNodeKey); - if (!$isTableNode(tableNode)) { - throw new Error('Expected TableNode.'); - } + if (!$isTableNode(tableNode)) { + throw new Error('Expected TableNode.'); + } - const selection = $getSelection(); + const selection = $getSelection(); - if (!$isTableSelection(selection)) { - invariant(false, 'Expected grid selection'); - } + if (!$isTableSelection(selection)) { + invariant(false, 'Expected grid selection'); + } - const selectedNodes = selection.getNodes().filter($isTableCellNode); + const selectedNodes = selection.getNodes().filter($isTableCellNode); - if (selectedNodes.length === this.table.columns * this.table.rows) { - tableNode.selectPrevious(); - // Delete entire table - tableNode.remove(); - const rootNode = $getRoot(); - rootNode.selectStart(); - return; - } + if (selectedNodes.length === this.table.columns * this.table.rows) { + tableNode.selectPrevious(); + // Delete entire table + tableNode.remove(); + const rootNode = $getRoot(); + rootNode.selectStart(); + return; + } - selectedNodes.forEach((cellNode) => { - if ($isElementNode(cellNode)) { - const paragraphNode = $createParagraphNode(); - const textNode = $createTextNode(); - paragraphNode.append(textNode); - cellNode.append(paragraphNode); - cellNode.getChildren().forEach((child) => { - if (child !== paragraphNode) { - child.remove(); - } - }); - } - }); + selectedNodes.forEach((cellNode) => { + if ($isElementNode(cellNode)) { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode(); + paragraphNode.append(textNode); + cellNode.append(paragraphNode); + cellNode.getChildren().forEach((child) => { + if (child !== paragraphNode) { + child.remove(); + } + }); + } + }); - $updateDOMForSelection(editor, this.table, null); + $updateDOMForSelection(editor, this.table, null); - $setSelection(null); + $setSelection(null); - editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); - }); + editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); } } diff --git a/packages/lexical-table/src/LexicalTableSelection.ts b/packages/lexical-table/src/LexicalTableSelection.ts index 03c8543cb73..185d95100f3 100644 --- a/packages/lexical-table/src/LexicalTableSelection.ts +++ b/packages/lexical-table/src/LexicalTableSelection.ts @@ -9,24 +9,31 @@ import {$findMatchingParent} from '@lexical/utils'; import { $createPoint, - $getNodeByKey, + $getSelection, $isElementNode, $isParagraphNode, $normalizeSelection__EXPERIMENTAL, BaseSelection, + ElementNode, isCurrentlyReadOnlyMode, LexicalNode, NodeKey, PointType, TEXT_TYPE_TO_FORMAT, TextFormatType, + TextNode, } from 'lexical'; import invariant from 'shared/invariant'; import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; -import {$isTableNode} from './LexicalTableNode'; -import {$isTableRowNode} from './LexicalTableRowNode'; -import {$computeTableMap, $getTableCellNodeRect} from './LexicalTableUtils'; +import {$isTableNode, TableNode} from './LexicalTableNode'; +import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode'; +import {$findTableNode} from './LexicalTableSelectionHelpers'; +import { + $computeTableCellRectBoundary, + $computeTableMap, + $getTableCellNodeRect, +} from './LexicalTableUtils'; export type TableSelectionShape = { fromX: number; @@ -42,6 +49,62 @@ export type TableMapValueType = { }; export type TableMapType = Array>; +function $getCellNodes(tableSelection: TableSelection): { + anchorCell: TableCellNode; + anchorNode: TextNode | ElementNode; + anchorRow: TableRowNode; + anchorTable: TableNode; + focusCell: TableCellNode; + focusNode: TextNode | ElementNode; + focusRow: TableRowNode; + focusTable: TableNode; +} { + const [ + [anchorNode, anchorCell, anchorRow, anchorTable], + [focusNode, focusCell, focusRow, focusTable], + ] = (['anchor', 'focus'] as const).map( + (k): [ElementNode | TextNode, TableCellNode, TableRowNode, TableNode] => { + const node = tableSelection[k].getNode(); + const cellNode = $findMatchingParent(node, $isTableCellNode); + invariant( + $isTableCellNode(cellNode), + 'Expected TableSelection %s to be (or a child of) TableCellNode, got key %s of type %s', + k, + node.getKey(), + node.getType(), + ); + const rowNode = cellNode.getParent(); + invariant( + $isTableRowNode(rowNode), + 'Expected TableSelection %s cell parent to be a TableRowNode', + k, + ); + const tableNode = rowNode.getParent(); + invariant( + $isTableNode(tableNode), + 'Expected TableSelection %s row parent to be a TableNode', + k, + ); + return [node, cellNode, rowNode, tableNode]; + }, + ); + // TODO: nested tables may violate this + invariant( + anchorTable.is(focusTable), + 'Expected TableSelection anchor and focus to be in the same table', + ); + return { + anchorCell, + anchorNode, + anchorRow, + anchorTable, + focusCell, + focusNode, + focusRow, + focusTable, + }; +} + export class TableSelection implements BaseSelection { tableKey: NodeKey; anchor: PointType; @@ -63,6 +126,23 @@ export class TableSelection implements BaseSelection { return [this.anchor, this.focus]; } + /** + * {@link $createTableSelection} unfortunately makes it very easy to create + * nonsense selections, so we have a method to see if the selection probably + * makes sense. + * + * @returns true if the TableSelection is (probably) valid + */ + isValid(): boolean { + return ( + this.tableKey !== 'root' && + this.anchor.key !== 'root' && + this.anchor.type === 'element' && + this.focus.key !== 'root' && + this.focus.type === 'element' + ); + } + /** * Returns whether the Selection is "backwards", meaning the focus * logically precedes the anchor in the EditorState. @@ -81,10 +161,8 @@ export class TableSelection implements BaseSelection { } is(selection: null | BaseSelection): boolean { - if (!$isTableSelection(selection)) { - return false; - } return ( + $isTableSelection(selection) && this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus) @@ -92,7 +170,12 @@ export class TableSelection implements BaseSelection { } set(tableKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey): void { - this.dirty = true; + // note: closure compiler's acorn does not support ||= + this.dirty = + this.dirty || + tableKey !== this.tableKey || + anchorCellKey !== this.anchor.key || + focusCellKey !== this.focus.key; this.tableKey = tableKey; this.anchor.key = anchorCellKey; this.focus.key = focusCellKey; @@ -100,7 +183,11 @@ export class TableSelection implements BaseSelection { } clone(): TableSelection { - return new TableSelection(this.tableKey, this.anchor, this.focus); + return new TableSelection( + this.tableKey, + $createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), + $createPoint(this.focus.key, this.focus.offset, this.focus.type), + ); } isCollapsed(): boolean { @@ -155,23 +242,13 @@ export class TableSelection implements BaseSelection { // TODO Deprecate this method. It's confusing when used with colspan|rowspan getShape(): TableSelectionShape { - const anchorCellNode = $getNodeByKey(this.anchor.key); - invariant( - $isTableCellNode(anchorCellNode), - 'Expected TableSelection anchor to be (or a child of) TableCellNode', - ); - const anchorCellNodeRect = $getTableCellNodeRect(anchorCellNode); + const {anchorCell, focusCell} = $getCellNodes(this); + const anchorCellNodeRect = $getTableCellNodeRect(anchorCell); invariant( anchorCellNodeRect !== null, 'getCellRect: expected to find AnchorNode', ); - - const focusCellNode = $getNodeByKey(this.focus.key); - invariant( - $isTableCellNode(focusCellNode), - 'Expected TableSelection focus to be (or a child of) TableCellNode', - ); - const focusCellNodeRect = $getTableCellNodeRect(focusCellNode); + const focusCellNodeRect = $getTableCellNodeRect(focusCell); invariant( focusCellNodeRect !== null, 'getCellRect: expected to find focusCellNode', @@ -204,34 +281,15 @@ export class TableSelection implements BaseSelection { } getNodes(): Array { + if (!this.isValid()) { + return []; + } const cachedNodes = this._cachedNodes; if (cachedNodes !== null) { return cachedNodes; } - const anchorNode = this.anchor.getNode(); - const focusNode = this.focus.getNode(); - const anchorCell = $findMatchingParent(anchorNode, $isTableCellNode); - // todo replace with triplet - const focusCell = $findMatchingParent(focusNode, $isTableCellNode); - invariant( - $isTableCellNode(anchorCell), - 'Expected TableSelection anchor to be (or a child of) TableCellNode', - ); - invariant( - $isTableCellNode(focusCell), - 'Expected TableSelection focus to be (or a child of) TableCellNode', - ); - const anchorRow = anchorCell.getParent(); - invariant( - $isTableRowNode(anchorRow), - 'Expected anchorCell to have a parent TableRowNode', - ); - const tableNode = anchorRow.getParent(); - invariant( - $isTableNode(tableNode), - 'Expected tableNode to have a parent TableNode', - ); + const {anchorTable: tableNode, anchorCell, focusCell} = $getCellNodes(this); const focusCellGrid = focusCell.getParents()[1]; if (focusCellGrid !== tableNode) { @@ -261,82 +319,15 @@ export class TableSelection implements BaseSelection { anchorCell, focusCell, ); - - let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn); - let minRow = Math.min(cellAMap.startRow, cellBMap.startRow); - let maxColumn = Math.max( - cellAMap.startColumn + cellAMap.cell.__colSpan - 1, - cellBMap.startColumn + cellBMap.cell.__colSpan - 1, - ); - let maxRow = Math.max( - cellAMap.startRow + cellAMap.cell.__rowSpan - 1, - cellBMap.startRow + cellBMap.cell.__rowSpan - 1, - ); - let exploredMinColumn = minColumn; - let exploredMinRow = minRow; - let exploredMaxColumn = minColumn; - let exploredMaxRow = minRow; - function expandBoundary(mapValue: TableMapValueType): void { - const { - cell, - startColumn: cellStartColumn, - startRow: cellStartRow, - } = mapValue; - minColumn = Math.min(minColumn, cellStartColumn); - minRow = Math.min(minRow, cellStartRow); - maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1); - maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1); - } - while ( - minColumn < exploredMinColumn || - minRow < exploredMinRow || - maxColumn > exploredMaxColumn || - maxRow > exploredMaxRow - ) { - if (minColumn < exploredMinColumn) { - // Expand on the left - const rowDiff = exploredMaxRow - exploredMinRow; - const previousColumn = exploredMinColumn - 1; - for (let i = 0; i <= rowDiff; i++) { - expandBoundary(map[exploredMinRow + i][previousColumn]); - } - exploredMinColumn = previousColumn; - } - if (minRow < exploredMinRow) { - // Expand on top - const columnDiff = exploredMaxColumn - exploredMinColumn; - const previousRow = exploredMinRow - 1; - for (let i = 0; i <= columnDiff; i++) { - expandBoundary(map[previousRow][exploredMinColumn + i]); - } - exploredMinRow = previousRow; - } - if (maxColumn > exploredMaxColumn) { - // Expand on the right - const rowDiff = exploredMaxRow - exploredMinRow; - const nextColumn = exploredMaxColumn + 1; - for (let i = 0; i <= rowDiff; i++) { - expandBoundary(map[exploredMinRow + i][nextColumn]); - } - exploredMaxColumn = nextColumn; - } - if (maxRow > exploredMaxRow) { - // Expand on the bottom - const columnDiff = exploredMaxColumn - exploredMinColumn; - const nextRow = exploredMaxRow + 1; - for (let i = 0; i <= columnDiff; i++) { - expandBoundary(map[nextRow][exploredMinColumn + i]); - } - exploredMaxRow = nextRow; - } - } + const {minColumn, maxColumn, minRow, maxRow} = + $computeTableCellRectBoundary(map, cellAMap, cellBMap); // We use a Map here because merged cells in the grid would otherwise // show up multiple times in the nodes array const nodeMap: Map = new Map([ [tableNode.getKey(), tableNode], ]); - let lastRow = null; + let lastRow: null | TableRowNode = null; for (let i = minRow; i <= maxRow; i++) { for (let j = minColumn; j <= maxColumn; j++) { const {cell} = map[i][j]; @@ -347,12 +338,13 @@ export class TableSelection implements BaseSelection { ); if (currentRow !== lastRow) { nodeMap.set(currentRow.getKey(), currentRow); + lastRow = currentRow; } - nodeMap.set(cell.getKey(), cell); - for (const child of $getChildrenRecursively(cell)) { - nodeMap.set(child.getKey(), child); + if (!nodeMap.has(cell.getKey())) { + $visitRecursively(cell, (childNode) => { + nodeMap.set(childNode.getKey(), childNode); + }); } - lastRow = currentRow; } } const nodes = Array.from(nodeMap.values()); @@ -381,26 +373,76 @@ export function $isTableSelection(x: unknown): x is TableSelection { } export function $createTableSelection(): TableSelection { + // TODO this is a suboptimal design, it doesn't make sense to have + // a table selection that isn't associated with a table. This + // constructor should have required argumnets and in __DEV__ we + // should check that they point to a table and are element points to + // cell nodes of that table. const anchor = $createPoint('root', 0, 'element'); const focus = $createPoint('root', 0, 'element'); return new TableSelection('root', anchor, focus); } -export function $getChildrenRecursively(node: LexicalNode): Array { - const nodes = []; - const stack = [node]; - while (stack.length > 0) { - const currentNode = stack.pop(); +export function $createTableSelectionFrom( + tableNode: TableNode, + anchorCell: TableCellNode, + focusCell: TableCellNode, +): TableSelection { + const tableNodeKey = tableNode.getKey(); + const anchorCellKey = anchorCell.getKey(); + const focusCellKey = focusCell.getKey(); + if (__DEV__) { invariant( - currentNode !== undefined, - "Stack.length > 0; can't be undefined", + tableNode.isAttached(), + '$createTableSelectionFrom: tableNode %s is not attached', + tableNodeKey, ); - if ($isElementNode(currentNode)) { - stack.unshift(...currentNode.getChildren()); - } - if (currentNode !== node) { - nodes.push(currentNode); + invariant( + tableNode.is($findTableNode(anchorCell)), + '$createTableSelectionFrom: anchorCell %s is not in table %s', + anchorCellKey, + tableNodeKey, + ); + invariant( + tableNode.is($findTableNode(focusCell)), + '$createTableSelectionFrom: focusCell %s is not in table %s', + focusCellKey, + tableNodeKey, + ); + // TODO: Check for rectangular grid + } + const prevSelection = $getSelection(); + const nextSelection = $isTableSelection(prevSelection) + ? prevSelection.clone() + : $createTableSelection(); + nextSelection.set( + tableNode.getKey(), + anchorCell.getKey(), + focusCell.getKey(), + ); + return nextSelection; +} + +/** + * Depth first visitor + * @param node The starting node + * @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored + */ +export function $visitRecursively( + node: LexicalNode, + $visit: (childNode: LexicalNode) => boolean | undefined | void, +): void { + const stack = [[node]]; + for ( + let currentArray = stack.at(-1); + currentArray !== undefined && stack.length > 0; + currentArray = stack.at(-1) + ) { + const currentNode = currentArray.pop(); + if (currentNode === undefined) { + stack.pop(); + } else if ($visit(currentNode) !== false && $isElementNode(currentNode)) { + stack.push(currentNode.getChildren()); } } - return nodes; } diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 740af8e121f..2a1e4945037 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -51,6 +51,7 @@ import { FOCUS_COMMAND, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, + getDOMSelection, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, @@ -63,7 +64,7 @@ import { SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, } from 'lexical'; -import {CAN_USE_DOM} from 'shared/canUseDOM'; +import {IS_FIREFOX} from 'shared/environment'; import invariant from 'shared/invariant'; import {$isTableCellNode} from './LexicalTableCellNode'; @@ -75,15 +76,16 @@ import { import {TableDOMTable, TableObserver} from './LexicalTableObserver'; import {$isTableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; -import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils'; +import { + $computeTableCellRectBoundary, + $computeTableCellRectSpans, + $computeTableMap, + $getNodeTriplet, + TableCellRectBoundary, +} from './LexicalTableUtils'; const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection'; -export const getDOMSelection = ( - targetWindow: Window | null, -): Selection | null => - CAN_USE_DOM ? (targetWindow || window).getSelection() : null; - const isMouseDownOnEvent = (event: MouseEvent) => { return (event.buttons & 1) === 1; }; @@ -106,6 +108,44 @@ export function getTableElement( return element; } +export function getEditorWindow(editor: LexicalEditor): Window | null { + return editor._window; +} + +export function $findParentTableCellNodeInTable( + tableNode: LexicalNode, + node: LexicalNode | null, +): TableCellNode | null { + for ( + let currentNode = node, lastTableCellNode: TableCellNode | null = null; + currentNode !== null; + currentNode = currentNode.getParent() + ) { + if (tableNode.is(currentNode)) { + return lastTableCellNode; + } else if ($isTableCellNode(currentNode)) { + lastTableCellNode = currentNode; + } + } + return null; +} + +const ARROW_KEY_COMMANDS_WITH_DIRECTION = [ + [KEY_ARROW_DOWN_COMMAND, 'down'], + [KEY_ARROW_UP_COMMAND, 'up'], + [KEY_ARROW_LEFT_COMMAND, 'backward'], + [KEY_ARROW_RIGHT_COMMAND, 'forward'], +] as const; +const DELETE_TEXT_COMMANDS = [ + DELETE_WORD_COMMAND, + DELETE_LINE_COMMAND, + DELETE_CHARACTER_COMMAND, +] as const; +const DELETE_KEY_COMMANDS = [ + KEY_BACKSPACE_COMMAND, + KEY_DELETE_COMMAND, +] as const; + export function applyTableHandlers( tableNode: TableNode, element: HTMLElement, @@ -113,13 +153,13 @@ export function applyTableHandlers( hasTabHandler: boolean, ): TableObserver { const rootElement = editor.getRootElement(); - - if (rootElement === null) { - throw new Error('No root element.'); - } + const editorWindow = getEditorWindow(editor); + invariant( + rootElement !== null && editorWindow !== null, + 'applyTableHandlers: editor has no root element set', + ); const tableObserver = new TableObserver(editor, tableNode.getKey()); - const editorWindow = editor._window || window; const tableElement = getTableElement(tableNode, element); attachTableObserverToTableElement(tableElement, tableObserver); @@ -128,6 +168,9 @@ export function applyTableHandlers( ); const createMouseHandlers = () => { + if (tableObserver.isSelecting) { + return; + } const onMouseUp = () => { tableObserver.isSelecting = false; editorWindow.removeEventListener('mouseup', onMouseUp); @@ -135,57 +178,104 @@ export function applyTableHandlers( }; const onMouseMove = (moveEvent: MouseEvent) => { - // delaying mousemove handler to allow selectionchange handler from LexicalEvents.ts to be executed first - setTimeout(() => { - if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) { - tableObserver.isSelecting = false; - editorWindow.removeEventListener('mouseup', onMouseUp); - editorWindow.removeEventListener('mousemove', onMouseMove); - return; - } - const focusCell = getDOMCellFromTarget(moveEvent.target as Node); - if ( - focusCell !== null && - (tableObserver.anchorX !== focusCell.x || - tableObserver.anchorY !== focusCell.y) - ) { - moveEvent.preventDefault(); - tableObserver.setFocusCellForSelection(focusCell); + if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) { + tableObserver.isSelecting = false; + editorWindow.removeEventListener('mouseup', onMouseUp); + editorWindow.removeEventListener('mousemove', onMouseMove); + return; + } + const override = !tableElement.contains(moveEvent.target as Node); + let focusCell: null | TableDOMCell = null; + if (!override) { + focusCell = getDOMCellFromTarget(moveEvent.target as Node); + } else { + for (const el of document.elementsFromPoint( + moveEvent.clientX, + moveEvent.clientY, + )) { + focusCell = tableElement.contains(el) + ? getDOMCellFromTarget(el) + : null; + if (focusCell) { + break; + } } - }, 0); + } + if ( + focusCell && + (tableObserver.focusCell === null || + focusCell.elem !== tableObserver.focusCell.elem) + ) { + tableObserver.setNextFocus({focusCell, override}); + editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); + } }; - return {onMouseMove, onMouseUp}; + tableObserver.isSelecting = true; + editorWindow.addEventListener( + 'mouseup', + onMouseUp, + tableObserver.listenerOptions, + ); + editorWindow.addEventListener( + 'mousemove', + onMouseMove, + tableObserver.listenerOptions, + ); }; const onMouseDown = (event: MouseEvent) => { - setTimeout(() => { - if (event.button !== 0) { - return; - } + if (event.button !== 0) { + return; + } - if (!editorWindow) { - return; - } + if (!editorWindow) { + return; + } - const anchorCell = getDOMCellFromTarget(event.target as Node); - if (anchorCell !== null) { - stopEvent(event); - tableObserver.setAnchorCellForSelection(anchorCell); - } + const targetCell = getDOMCellFromTarget(event.target as Node); + if (targetCell !== null) { + editor.update(() => { + const prevSelection = $getPreviousSelection(); + // We can't trust Firefox to do the right thing with the selection and + // we don't have a proper state machine to do this "correctly" but + // if we go ahead and make the table selection now it will work + if ( + IS_FIREFOX && + event.shiftKey && + $isSelectionInTable(prevSelection, tableNode) && + ($isRangeSelection(prevSelection) || $isTableSelection(prevSelection)) + ) { + const prevAnchorNode = prevSelection.anchor.getNode(); + const prevAnchorCell = $findParentTableCellNodeInTable( + tableNode, + prevSelection.anchor.getNode(), + ); + if (prevAnchorCell) { + tableObserver.$setAnchorCellForSelection( + $getObserverCellFromCellNodeOrThrow( + tableObserver, + prevAnchorCell, + ), + ); + tableObserver.$setFocusCellForSelection(targetCell); + stopEvent(event); + } else { + const newSelection = tableNode.isBefore(prevAnchorNode) + ? tableNode.selectStart() + : tableNode.selectEnd(); + newSelection.anchor.set( + prevSelection.anchor.key, + prevSelection.anchor.offset, + prevSelection.anchor.type, + ); + } + } else { + tableObserver.$setAnchorCellForSelection(targetCell); + } + }); + } - const {onMouseUp, onMouseMove} = createMouseHandlers(); - tableObserver.isSelecting = true; - editorWindow.addEventListener( - 'mouseup', - onMouseUp, - tableObserver.listenerOptions, - ); - editorWindow.addEventListener( - 'mousemove', - onMouseMove, - tableObserver.listenerOptions, - ); - }, 0); + createMouseHandlers(); }; tableElement.addEventListener( 'mousedown', @@ -207,7 +297,7 @@ export function applyTableHandlers( selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target) ) { - tableObserver.clearHighlight(); + tableObserver.$clearHighlight(); } }); }; @@ -218,40 +308,16 @@ export function applyTableHandlers( tableObserver.listenerOptions, ); - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_ARROW_DOWN_COMMAND, - (event) => - $handleArrowKey(editor, event, 'down', tableNode, tableObserver), - COMMAND_PRIORITY_HIGH, - ), - ); - - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_ARROW_UP_COMMAND, - (event) => $handleArrowKey(editor, event, 'up', tableNode, tableObserver), - COMMAND_PRIORITY_HIGH, - ), - ); - - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_ARROW_LEFT_COMMAND, - (event) => - $handleArrowKey(editor, event, 'backward', tableNode, tableObserver), - COMMAND_PRIORITY_HIGH, - ), - ); - - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_ARROW_RIGHT_COMMAND, - (event) => - $handleArrowKey(editor, event, 'forward', tableNode, tableObserver), - COMMAND_PRIORITY_HIGH, - ), - ); + for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) { + tableObserver.listenersToRemove.add( + editor.registerCommand( + command, + (event) => + $handleArrowKey(editor, event, direction, tableNode, tableObserver), + COMMAND_PRIORITY_HIGH, + ), + ); + } tableObserver.listenersToRemove.add( editor.registerCommand( @@ -259,11 +325,11 @@ export function applyTableHandlers( (event) => { const selection = $getSelection(); if ($isTableSelection(selection)) { - const focusCellNode = $findMatchingParent( + const focusCellNode = $findParentTableCellNodeInTable( + tableNode, selection.focus.getNode(), - $isTableCellNode, ); - if ($isTableCellNode(focusCellNode)) { + if (focusCellNode !== null) { stopEvent(event); focusCellNode.selectEnd(); return true; @@ -284,13 +350,13 @@ export function applyTableHandlers( } if ($isTableSelection(selection)) { - tableObserver.clearText(); + tableObserver.$clearText(); return true; } else if ($isRangeSelection(selection)) { - const tableCellNode = $findMatchingParent( + const tableCellNode = $findParentTableCellNodeInTable( + tableNode, selection.anchor.getNode(), - (n) => $isTableCellNode(n), ); if (!$isTableCellNode(tableCellNode)) { @@ -307,7 +373,7 @@ export function applyTableHandlers( (isFocusInside && !isAnchorInside); if (selectionContainsPartialTable) { - tableObserver.clearText(); + tableObserver.$clearText(); return true; } @@ -342,17 +408,15 @@ export function applyTableHandlers( return false; }; - [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND].forEach( - (command) => { - tableObserver.listenersToRemove.add( - editor.registerCommand( - command, - deleteTextHandler(command), - COMMAND_PRIORITY_CRITICAL, - ), - ); - }, - ); + for (const command of DELETE_TEXT_COMMANDS) { + tableObserver.listenersToRemove.add( + editor.registerCommand( + command, + deleteTextHandler(command), + COMMAND_PRIORITY_CRITICAL, + ), + ); + } const $deleteCellHandler = ( event: KeyboardEvent | ClipboardEvent | null, @@ -390,7 +454,7 @@ export function applyTableHandlers( event.preventDefault(); event.stopPropagation(); } - tableObserver.clearText(); + tableObserver.$clearText(); return true; } @@ -398,21 +462,15 @@ export function applyTableHandlers( return false; }; - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_BACKSPACE_COMMAND, - $deleteCellHandler, - COMMAND_PRIORITY_CRITICAL, - ), - ); - - tableObserver.listenersToRemove.add( - editor.registerCommand( - KEY_DELETE_COMMAND, - $deleteCellHandler, - COMMAND_PRIORITY_CRITICAL, - ), - ); + for (const command of DELETE_KEY_COMMANDS) { + tableObserver.listenersToRemove.add( + editor.registerCommand( + command, + $deleteCellHandler, + COMMAND_PRIORITY_CRITICAL, + ), + ); + } tableObserver.listenersToRemove.add( editor.registerCommand( @@ -456,7 +514,7 @@ export function applyTableHandlers( } if ($isTableSelection(selection)) { - tableObserver.formatCells(payload); + tableObserver.$formatCells(payload); return true; } else if ($isRangeSelection(selection)) { @@ -499,19 +557,27 @@ export function applyTableHandlers( anchorNode, focusNode, ); - const maxRow = Math.max(anchorCell.startRow, focusCell.startRow); + const maxRow = Math.max( + anchorCell.startRow + anchorCell.cell.__rowSpan - 1, + focusCell.startRow + focusCell.cell.__rowSpan - 1, + ); const maxColumn = Math.max( - anchorCell.startColumn, - focusCell.startColumn, + anchorCell.startColumn + anchorCell.cell.__colSpan - 1, + focusCell.startColumn + focusCell.cell.__colSpan - 1, ); const minRow = Math.min(anchorCell.startRow, focusCell.startRow); const minColumn = Math.min( anchorCell.startColumn, focusCell.startColumn, ); + const visited = new Set(); for (let i = minRow; i <= maxRow; i++) { for (let j = minColumn; j <= maxColumn; j++) { const cell = tableMap[i][j].cell; + if (visited.has(cell)) { + continue; + } + visited.add(cell); cell.setFormat(formatType); const cellChildren = cell.getChildren(); @@ -540,7 +606,7 @@ export function applyTableHandlers( } if ($isTableSelection(selection)) { - tableObserver.clearHighlight(); + tableObserver.$clearHighlight(); return false; } else if ($isRangeSelection(selection)) { @@ -625,20 +691,6 @@ export function applyTableHandlers( ), ); - function getObserverCellFromCellNode( - tableCellNode: TableCellNode, - ): TableDOMCell { - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableObserver.table, - ); - return tableNode.getDOMCellFromCordsOrThrow( - currentCords.x, - currentCords.y, - tableObserver.table, - ); - } - tableObserver.listenersToRemove.add( editor.registerCommand( SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, @@ -766,11 +818,39 @@ export function applyTableHandlers( () => { const selection = $getSelection(); const prevSelection = $getPreviousSelection(); + const nextFocus = tableObserver.getAndClearNextFocus(); + if (nextFocus !== null) { + const {focusCell} = nextFocus; + if ( + $isTableSelection(selection) && + selection.tableKey === tableObserver.tableNodeKey + ) { + if ( + focusCell.x === tableObserver.focusX && + focusCell.y === tableObserver.focusY + ) { + // The selection is already the correct table selection + return false; + } else { + tableObserver.$setFocusCellForSelection(focusCell); + return true; + } + } else if ( + focusCell !== tableObserver.anchorCell && + $isSelectionInTable(selection, tableNode) + ) { + // The selection has crossed cells + tableObserver.$setFocusCellForSelection(focusCell); + return true; + } + } + const shouldCheckSelection = + tableObserver.getAndClearShouldCheckSelection(); // If they pressed the down arrow with the selection outside of the // table, and then the selection ends up in the table but not in the // first cell, then move the selection to the first cell. if ( - tableObserver.getAndClearShouldCheckSelection() && + shouldCheckSelection && $isRangeSelection(prevSelection) && $isRangeSelection(selection) && selection.isCollapsed() @@ -810,11 +890,11 @@ export function applyTableHandlers( const isFocusInside = !!( focusCellNode && tableNode.is($findTableNode(focusCellNode)) ); - const isPartialyWithinTable = isAnchorInside !== isFocusInside; + const isPartiallyWithinTable = isAnchorInside !== isFocusInside; const isWithinTable = isAnchorInside && isFocusInside; const isBackward = selection.isBackward(); - if (isPartialyWithinTable) { + if (isPartiallyWithinTable) { const newSelection = selection.clone(); if (isFocusInside) { const [tableMap] = $computeTableMap( @@ -855,23 +935,21 @@ export function applyTableHandlers( $addHighlightStyleToTable(editor, tableObserver); } else if (isWithinTable) { // Handle case when selection spans across multiple cells but still - // has range selection, then we convert it into grid selection + // has range selection, then we convert it into table selection if (!anchorCellNode.is(focusCellNode)) { - tableObserver.setAnchorCellForSelection( - getObserverCellFromCellNode(anchorCellNode), + tableObserver.$setAnchorCellForSelection( + $getObserverCellFromCellNodeOrThrow( + tableObserver, + anchorCellNode, + ), ); - tableObserver.setFocusCellForSelection( - getObserverCellFromCellNode(focusCellNode), + tableObserver.$setFocusCellForSelection( + $getObserverCellFromCellNodeOrThrow( + tableObserver, + focusCellNode, + ), true, ); - if (!tableObserver.isSelecting) { - setTimeout(() => { - const {onMouseUp, onMouseMove} = createMouseHandlers(); - tableObserver.isSelecting = true; - editorWindow.addEventListener('mouseup', onMouseUp); - editorWindow.addEventListener('mousemove', onMouseMove); - }, 0); - } } } } else if ( @@ -881,7 +959,7 @@ export function applyTableHandlers( selection.tableKey === tableNode.getKey() ) { // if selection goes outside of the table we need to change it to Range selection - const domSelection = getDOMSelection(editor._window); + const domSelection = getDOMSelection(editorWindow); if ( domSelection && domSelection.anchorNode && @@ -932,13 +1010,13 @@ export function applyTableHandlers( $isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey ) { - tableObserver.updateTableTableSelection(selection); + tableObserver.$updateTableTableSelection(selection); } else if ( !$isTableSelection(selection) && $isTableSelection(prevSelection) && prevSelection.tableKey === tableObserver.tableNodeKey ) { - tableObserver.updateTableTableSelection(null); + tableObserver.$updateTableTableSelection(null); } return false; } @@ -1195,7 +1273,7 @@ export function $addHighlightStyleToTable( editor: LexicalEditor, tableSelection: TableObserver, ) { - tableSelection.disableHighlightStyle(); + tableSelection.$disableHighlightStyle(); $forEachTableCell(tableSelection.table, (cell) => { cell.highlighted = true; $addHighlightToDOM(editor, cell); @@ -1206,7 +1284,7 @@ export function $removeHighlightStyleToTable( editor: LexicalEditor, tableObserver: TableObserver, ) { - tableObserver.enableHighlightStyle(); + tableObserver.$enableHighlightStyle(); $forEachTableCell(tableObserver.table, (cell) => { const elem = cell.elem; cell.highlighted = false; @@ -1288,53 +1366,170 @@ const selectTableNodeInDirection = ( } }; -const adjustFocusNodeInDirection = ( - tableObserver: TableObserver, - tableNode: TableNode, - x: number, - y: number, - direction: Direction, -): boolean => { - const isForward = direction === 'forward'; +type Corner = ['minColumn' | 'maxColumn', 'minRow' | 'maxRow']; +function getCorner( + rect: TableCellRectBoundary, + cellValue: TableMapValueType, +): Corner | null { + let colName: 'minColumn' | 'maxColumn'; + let rowName: 'minRow' | 'maxRow'; + if (cellValue.startColumn === rect.minColumn) { + colName = 'minColumn'; + } else if ( + cellValue.startColumn + cellValue.cell.__colSpan - 1 === + rect.maxColumn + ) { + colName = 'maxColumn'; + } else { + return null; + } + if (cellValue.startRow === rect.minRow) { + rowName = 'minRow'; + } else if ( + cellValue.startRow + cellValue.cell.__rowSpan - 1 === + rect.maxRow + ) { + rowName = 'maxRow'; + } else { + return null; + } + return [colName, rowName]; +} - switch (direction) { - case 'backward': - case 'forward': - if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) { - tableObserver.setFocusCellForSelection( - tableNode.getDOMCellFromCordsOrThrow( - x + (isForward ? 1 : -1), - y, - tableObserver.table, - ), - ); - } +function getCornerOrThrow( + rect: TableCellRectBoundary, + cellValue: TableMapValueType, +): Corner { + const corner = getCorner(rect, cellValue); + invariant( + corner !== null, + 'getCornerOrThrow: cell %s is not at a corner of rect', + cellValue.cell.getKey(), + ); + return corner; +} - return true; - case 'up': - if (y !== 0) { - tableObserver.setFocusCellForSelection( - tableNode.getDOMCellFromCordsOrThrow(x, y - 1, tableObserver.table), - ); +function oppositeCorner([colName, rowName]: Corner): Corner { + return [ + colName === 'minColumn' ? 'maxColumn' : 'minColumn', + rowName === 'minRow' ? 'maxRow' : 'minRow', + ]; +} - return true; - } else { - return false; - } - case 'down': - if (y !== tableObserver.table.rows - 1) { - tableObserver.setFocusCellForSelection( - tableNode.getDOMCellFromCordsOrThrow(x, y + 1, tableObserver.table), - ); +function cellAtCornerOrThrow( + tableMap: TableMapType, + rect: TableCellRectBoundary, + [colName, rowName]: Corner, +): TableMapValueType { + const rowNum = rect[rowName]; + const rowMap = tableMap[rowNum]; + invariant( + rowMap !== undefined, + 'cellAtCornerOrThrow: %s = %s missing in tableMap', + rowName, + String(rowNum), + ); + const colNum = rect[colName]; + const cell = rowMap[colNum]; + invariant( + cell !== undefined, + 'cellAtCornerOrThrow: %s = %s missing in tableMap', + colName, + String(colNum), + ); + return cell; +} - return true; - } else { - return false; - } - default: - return false; +function $extractRectCorners( + tableMap: TableMapType, + anchorCellValue: TableMapValueType, + newFocusCellValue: TableMapValueType, +) { + // We are sure that the focus now either contracts or expands the rect + // but both the anchor and focus might be moved to ensure a rectangle + // given a potentially ragged merge shape + const rect = $computeTableCellRectBoundary( + tableMap, + anchorCellValue, + newFocusCellValue, + ); + const anchorCorner = getCorner(rect, anchorCellValue); + if (anchorCorner) { + return [ + cellAtCornerOrThrow(tableMap, rect, anchorCorner), + cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner)), + ]; } -}; + const newFocusCorner = getCorner(rect, newFocusCellValue); + if (newFocusCorner) { + return [ + cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)), + cellAtCornerOrThrow(tableMap, rect, newFocusCorner), + ]; + } + // TODO this doesn't have to be arbitrary, use the closest corner instead + const newAnchorCorner: Corner = ['minColumn', 'minRow']; + return [ + cellAtCornerOrThrow(tableMap, rect, newAnchorCorner), + cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner)), + ]; +} + +function $adjustFocusInDirection( + tableObserver: TableObserver, + tableMap: TableMapType, + anchorCellValue: TableMapValueType, + focusCellValue: TableMapValueType, + direction: Direction, +): boolean { + const rect = $computeTableCellRectBoundary( + tableMap, + anchorCellValue, + focusCellValue, + ); + const spans = $computeTableCellRectSpans(tableMap, rect); + const {topSpan, leftSpan, bottomSpan, rightSpan} = spans; + const anchorCorner = getCornerOrThrow(rect, anchorCellValue); + const [focusColumn, focusRow] = oppositeCorner(anchorCorner); + let fCol = rect[focusColumn]; + let fRow = rect[focusRow]; + if (direction === 'forward') { + fCol += focusColumn === 'maxColumn' ? 1 : leftSpan; + } else if (direction === 'backward') { + fCol -= focusColumn === 'minColumn' ? 1 : rightSpan; + } else if (direction === 'down') { + fRow += focusRow === 'maxRow' ? 1 : topSpan; + } else if (direction === 'up') { + fRow -= focusRow === 'minRow' ? 1 : bottomSpan; + } + const targetRowMap = tableMap[fRow]; + if (targetRowMap === undefined) { + return false; + } + const newFocusCellValue = targetRowMap[fCol]; + if (newFocusCellValue === undefined) { + return false; + } + // We can be certain that anchorCellValue and newFocusCellValue are + // contained within the desired selection, but we are not certain if + // they need to be expanded or not to maintain a rectangular shape + const [finalAnchorCell, finalFocusCell] = $extractRectCorners( + tableMap, + anchorCellValue, + newFocusCellValue, + ); + const anchorDOM = $getObserverCellFromCellNodeOrThrow( + tableObserver, + finalAnchorCell.cell, + )!; + const focusDOM = $getObserverCellFromCellNodeOrThrow( + tableObserver, + finalFocusCell.cell, + ); + tableObserver.$setAnchorCellForSelection(anchorDOM); + tableObserver.$setFocusCellForSelection(focusDOM, true); + return true; +} function $isSelectionInTable( selection: null | BaseSelection, @@ -1545,8 +1740,8 @@ function $handleArrowKey( lastCellCoords.y, tableObserver.table, ); - tableObserver.setAnchorCellForSelection(firstCellDOM); - tableObserver.setFocusCellForSelection(lastCellDOM, true); + tableObserver.$setAnchorCellForSelection(firstCellDOM); + tableObserver.$setFocusCellForSelection(lastCellDOM, true); return true; } } @@ -1650,7 +1845,13 @@ function $handleArrowKey( if ( isExitingTableAnchor(anchorType, anchorOffset, anchorNode, direction) ) { - return $handleTableExit(event, anchorNode, tableNode, direction); + return $handleTableExit( + event, + anchorNode, + anchorCellNode, + tableNode, + direction, + ); } return false; @@ -1666,7 +1867,7 @@ function $handleArrowKey( if (anchor.type === 'element') { edgeSelectionRect = anchorDOM.getBoundingClientRect(); } else { - const domSelection = window.getSelection(); + const domSelection = getDOMSelection(getEditorWindow(editor)); if (domSelection === null || domSelection.rangeCount === 0) { return false; } @@ -1709,8 +1910,8 @@ function $handleArrowKey( cords.y, tableObserver.table, ); - tableObserver.setAnchorCellForSelection(cell); - tableObserver.setFocusCellForSelection(cell, true); + tableObserver.$setAnchorCellForSelection(cell); + tableObserver.$setFocusCellForSelection(cell, true); } else { return selectTableNodeInDirection( tableObserver, @@ -1751,7 +1952,7 @@ function $handleArrowKey( ) { return false; } - tableObserver.updateTableTableSelection(selection); + tableObserver.$updateTableTableSelection(selection); const grid = getTable(tableNodeFromSelection, tableElement); const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid); @@ -1760,17 +1961,21 @@ function $handleArrowKey( cordsAnchor.y, grid, ); - tableObserver.setAnchorCellForSelection(anchorCell); + tableObserver.$setAnchorCellForSelection(anchorCell); stopEvent(event); if (event.shiftKey) { - const cords = tableNode.getCordsFromCellNode(focusCellNode, grid); - return adjustFocusNodeInDirection( + const [tableMap, anchorValue, focusValue] = $computeTableMap( + tableNode, + anchorCellNode, + focusCellNode, + ); + return $adjustFocusInDirection( tableObserver, - tableNodeFromSelection, - cords.x, - cords.y, + tableMap, + anchorValue, + focusValue, direction, ); } else { @@ -1856,13 +2061,10 @@ function $isExitingTableTextAnchor( function $handleTableExit( event: KeyboardEvent, anchorNode: LexicalNode, + anchorCellNode: TableCellNode, tableNode: TableNode, direction: 'backward' | 'forward', -) { - const anchorCellNode = $findMatchingParent(anchorNode, $isTableCellNode); - if (!$isTableCellNode(anchorCellNode)) { - return false; - } +): boolean { const [tableMap, cellValue] = $computeTableMap( tableNode, anchorCellNode, @@ -1948,7 +2150,7 @@ function $getTableEdgeCursorPosition( } // TODO: Add support for nested tables - const domSelection = window.getSelection(); + const domSelection = getDOMSelection(getEditorWindow(editor)); if (!domSelection) { return undefined; } @@ -2009,3 +2211,19 @@ function $getTableEdgeCursorPosition( return undefined; } } + +export function $getObserverCellFromCellNodeOrThrow( + tableObserver: TableObserver, + tableCellNode: TableCellNode, +): TableDOMCell { + const {tableNode} = tableObserver.$lookup(); + const currentCords = tableNode.getCordsFromCellNode( + tableCellNode, + tableObserver.table, + ); + return tableNode.getDOMCellFromCordsOrThrow( + currentCords.x, + currentCords.y, + tableObserver.table, + ); +} diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index c2cce0126a1..e1c0c0884cd 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -788,12 +788,12 @@ export function $unmergeCell(): void { } export function $computeTableMap( - grid: TableNode, + tableNode: TableNode, cellA: TableCellNode, cellB: TableCellNode, ): [TableMapType, TableMapValueType, TableMapValueType] { const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck( - grid, + tableNode, cellA, cellB, ); @@ -803,10 +803,14 @@ export function $computeTableMap( } export function $computeTableMapSkipCellCheck( - grid: TableNode, + tableNode: TableNode, cellA: null | TableCellNode, cellB: null | TableCellNode, -): [TableMapType, TableMapValueType | null, TableMapValueType | null] { +): [ + tableMap: TableMapType, + cellAValue: TableMapValueType | null, + cellBValue: TableMapValueType | null, +] { const tableMap: TableMapType = []; let cellAValue: null | TableMapValueType = null; let cellBValue: null | TableMapValueType = null; @@ -817,7 +821,7 @@ export function $computeTableMapSkipCellCheck( } return row; } - const gridChildren = grid.getChildren(); + const gridChildren = tableNode.getChildren(); for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) { const row = gridChildren[rowIdx]; invariant( @@ -905,6 +909,118 @@ export function $getNodeTriplet( return [cell, row, grid]; } +export interface TableCellRectBoundary { + minColumn: number; + minRow: number; + maxColumn: number; + maxRow: number; +} + +export interface TableCellRectSpans { + topSpan: number; + leftSpan: number; + rightSpan: number; + bottomSpan: number; +} + +export function $computeTableCellRectSpans( + map: TableMapType, + boundary: TableCellRectBoundary, +): TableCellRectSpans { + const {minColumn, maxColumn, minRow, maxRow} = boundary; + let topSpan = 1; + let leftSpan = 1; + let rightSpan = 1; + let bottomSpan = 1; + const topRow = map[minRow]; + const bottomRow = map[maxRow]; + for (let col = minColumn; col <= maxColumn; col++) { + topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan); + bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan); + } + for (let row = minRow; row <= maxRow; row++) { + leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan); + rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan); + } + return {bottomSpan, leftSpan, rightSpan, topSpan}; +} + +export function $computeTableCellRectBoundary( + map: TableMapType, + cellAMap: TableMapValueType, + cellBMap: TableMapValueType, +): TableCellRectBoundary { + let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn); + let minRow = Math.min(cellAMap.startRow, cellBMap.startRow); + let maxColumn = Math.max( + cellAMap.startColumn + cellAMap.cell.__colSpan - 1, + cellBMap.startColumn + cellBMap.cell.__colSpan - 1, + ); + let maxRow = Math.max( + cellAMap.startRow + cellAMap.cell.__rowSpan - 1, + cellBMap.startRow + cellBMap.cell.__rowSpan - 1, + ); + let exploredMinColumn = minColumn; + let exploredMinRow = minRow; + let exploredMaxColumn = minColumn; + let exploredMaxRow = minRow; + function expandBoundary(mapValue: TableMapValueType): void { + const { + cell, + startColumn: cellStartColumn, + startRow: cellStartRow, + } = mapValue; + minColumn = Math.min(minColumn, cellStartColumn); + minRow = Math.min(minRow, cellStartRow); + maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1); + maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1); + } + while ( + minColumn < exploredMinColumn || + minRow < exploredMinRow || + maxColumn > exploredMaxColumn || + maxRow > exploredMaxRow + ) { + if (minColumn < exploredMinColumn) { + // Expand on the left + const rowDiff = exploredMaxRow - exploredMinRow; + const previousColumn = exploredMinColumn - 1; + for (let i = 0; i <= rowDiff; i++) { + expandBoundary(map[exploredMinRow + i][previousColumn]); + } + exploredMinColumn = previousColumn; + } + if (minRow < exploredMinRow) { + // Expand on top + const columnDiff = exploredMaxColumn - exploredMinColumn; + const previousRow = exploredMinRow - 1; + for (let i = 0; i <= columnDiff; i++) { + expandBoundary(map[previousRow][exploredMinColumn + i]); + } + exploredMinRow = previousRow; + } + if (maxColumn > exploredMaxColumn) { + // Expand on the right + const rowDiff = exploredMaxRow - exploredMinRow; + const nextColumn = exploredMaxColumn + 1; + for (let i = 0; i <= rowDiff; i++) { + expandBoundary(map[exploredMinRow + i][nextColumn]); + } + exploredMaxColumn = nextColumn; + } + if (maxRow > exploredMaxRow) { + // Expand on the bottom + const columnDiff = exploredMaxColumn - exploredMinColumn; + const nextRow = exploredMaxRow + 1; + for (let i = 0; i <= columnDiff; i++) { + expandBoundary(map[nextRow][exploredMinColumn + i]); + } + exploredMaxRow = nextRow; + } + } + return {maxColumn, maxRow, minColumn, minRow}; +} + export function $getTableCellNodeRect(tableCellNode: TableCellNode): { rowIndex: number; columnIndex: number; diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index b6c46fbdeeb..663fbb236a4 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -26,7 +26,6 @@ import { $getRoot, $getSelection, $isElementNode, - $isNodeSelection, $isRangeSelection, $isRootNode, $isTextNode, @@ -437,7 +436,7 @@ function onClick(event: PointerEvent, editor: LexicalEditor): void { domSelection.removeAllRanges(); selection.dirty = true; } else if (event.detail === 3 && !selection.isCollapsed()) { - // Tripple click causing selection to overflow into the nearest element. In that + // Triple click causing selection to overflow into the nearest element. In that // case visually it looks like a single element content is selected, focus node // is actually at the beginning of the next element (if present) and any manipulations // with selection (formatting) are affecting second element as well @@ -1089,7 +1088,8 @@ function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void { dispatchCommand(editor, REDO_COMMAND, undefined); } else { const prevSelection = editor._editorState._selection; - if ($isNodeSelection(prevSelection)) { + if (prevSelection !== null && !$isRangeSelection(prevSelection)) { + // Only RangeSelection can use the native cut/copy/select all if (isCopy(key, shiftKey, metaKey, ctrlKey)) { event.preventDefault(); dispatchCommand(editor, COPY_COMMAND, event); diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index dc3baa6fd4f..28b4f57e943 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1615,6 +1615,13 @@ export function updateDOMBlockCursorElement( } } +/** + * Returns the selection for the given window, or the global window if null. + * Will return null if {@link CAN_USE_DOM} is false. + * + * @param targetWindow The window to get the selection from + * @returns a Selection or null + */ export function getDOMSelection(targetWindow: null | Window): null | Selection { return !CAN_USE_DOM ? null : (targetWindow || window).getSelection(); } diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 2898a73a9b7..5f1eae58210 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -179,6 +179,7 @@ export { $setCompositionKey, $setSelection, $splitNode, + getDOMSelection, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, isBlockDomNode, diff --git a/packages/shared/viteModuleResolution.ts b/packages/shared/viteModuleResolution.ts index 572e1e52dfd..1764b008640 100644 --- a/packages/shared/viteModuleResolution.ts +++ b/packages/shared/viteModuleResolution.ts @@ -11,6 +11,7 @@ import type { NpmModuleExportEntry, PackageMetadata, } from '../../scripts/shared/PackageMetadata'; +import type {Alias} from 'vite'; import * as fs from 'node:fs'; import {createRequire} from 'node:module'; @@ -81,7 +82,7 @@ const distModuleResolution = (environment: 'development' | 'production') => { export default function moduleResolution( environment: 'source' | 'development' | 'production', -) { +): Alias[] { return environment === 'source' ? sourceModuleResolution() : distModuleResolution(environment); From 89af9942b8f80f0c1df35392ab7768b50b9173ba Mon Sep 17 00:00:00 2001 From: Hamza <40746210+hamza221@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:07:31 +0800 Subject: [PATCH 104/133] [lexical-table] Bug Fix: get table-cell background selection color from a class (#6658) Signed-off-by: hamza221 Signed-off-by: Hamza Mahjoubi Co-authored-by: Ivaylo Pavlov Co-authored-by: Bob Ippolito --- examples/react-table/src/ExampleTheme.ts | 4 - examples/react-table/src/styles.css | 33 +++---- .../html/TablesHTMLCopyAndPaste.spec.mjs | 48 ++++------ .../__tests__/e2e/Indentation.spec.mjs | 15 ++-- .../__tests__/e2e/Tables.spec.mjs | 88 +++++++------------ .../src/themes/PlaygroundEditorTheme.css | 33 +++---- .../src/themes/PlaygroundEditorTheme.ts | 4 - .../lexical-table/src/LexicalTableCellNode.ts | 38 ++++---- .../src/LexicalTableSelectionHelpers.ts | 28 +++--- packages/lexical/src/LexicalEditor.ts | 4 - 10 files changed, 100 insertions(+), 195 deletions(-) diff --git a/examples/react-table/src/ExampleTheme.ts b/examples/react-table/src/ExampleTheme.ts index ca6919c8757..3033445c2df 100644 --- a/examples/react-table/src/ExampleTheme.ts +++ b/examples/react-table/src/ExampleTheme.ts @@ -34,13 +34,9 @@ export default { tableCellActionButton: 'ExampleEditorTheme__tableCellActionButton', tableCellActionButtonContainer: 'ExampleEditorTheme__tableCellActionButtonContainer', - tableCellEditing: 'ExampleEditorTheme__tableCellEditing', tableCellHeader: 'ExampleEditorTheme__tableCellHeader', - tableCellPrimarySelected: 'ExampleEditorTheme__tableCellPrimarySelected', tableCellResizer: 'ExampleEditorTheme__tableCellResizer', tableCellSelected: 'ExampleEditorTheme__tableCellSelected', - tableCellSortedIndicator: 'ExampleEditorTheme__tableCellSortedIndicator', - tableResizeRuler: 'ExampleEditorTheme__tableCellResizeRuler', tableSelected: 'ExampleEditorTheme__tableSelected', tableSelection: 'ExampleEditorTheme__tableSelection', text: { diff --git a/examples/react-table/src/styles.css b/examples/react-table/src/styles.css index c6832a104f4..ceade988694 100644 --- a/examples/react-table/src/styles.css +++ b/examples/react-table/src/styles.css @@ -474,16 +474,6 @@ i.justify-align { position: relative; outline: none; } -.ExampleEditorTheme__tableCellSortedIndicator { - display: block; - opacity: 0.5; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 4px; - background-color: #999; -} .ExampleEditorTheme__tableCellResizer { position: absolute; right: -4px; @@ -498,21 +488,18 @@ i.justify-align { text-align: start; } .ExampleEditorTheme__tableCellSelected { - background-color: #c9dbf0; + caret-color: transparent; } -.ExampleEditorTheme__tableCellPrimarySelected { - border: 2px solid rgb(60, 132, 244); - display: block; - height: calc(100% - 2px); +.ExampleEditorTheme__tableCellSelected::after { position: absolute; - width: calc(100% - 2px); - left: -1px; - top: -1px; - z-index: 2; -} -.ExampleEditorTheme__tableCellEditing { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); - border-radius: 3px; + left: 0; + right: 0; + bottom: 0; + top: 0; + background-color: rgb(172, 206, 247); + opacity: 0.6; + content: ''; + pointer-events: none; } .ExampleEditorTheme__tableAddColumns { position: absolute; diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs index 8ac28284f78..a2c9e0bf610 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs @@ -302,8 +302,7 @@ test.describe('HTML Tables CopyAndPaste', () => {
+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

@@ -311,8 +310,7 @@ test.describe('HTML Tables CopyAndPaste', () => {

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

@@ -320,20 +318,17 @@ test.describe('HTML Tables CopyAndPaste', () => {

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

@@ -341,8 +336,7 @@ test.describe('HTML Tables CopyAndPaste', () => {

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">

{

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

@@ -198,8 +197,7 @@ test.describe('Identation', () => {

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

{

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

{

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

{

+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected">

{

+

a

+

bb

@@ -772,12 +770,10 @@ test.describe.parallel('Tables', () => {
+

d

+

e

@@ -873,12 +869,10 @@ test.describe.parallel('Tables', () => {
+

a

+

bb

@@ -886,12 +880,10 @@ test.describe.parallel('Tables', () => {
+

d

+

e

@@ -997,12 +989,10 @@ test.describe.parallel('Tables', () => {
+

a

+

bb

@@ -1010,12 +1000,10 @@ test.describe.parallel('Tables', () => {
+

d

+

e

@@ -1448,12 +1436,10 @@ test.describe.parallel('Tables', () => {
+


+


+


+


+


+


+


+


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected" + rowspan="2">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellHeader PlaygroundEditorTheme__tableCellSelected" + colspan="2">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCell PlaygroundEditorTheme__tableCellSelected">


+ class="PlaygroundEditorTheme__tableCellSelected" + style="text-align: center">

a

+ class="PlaygroundEditorTheme__tableCellSelected" + style="text-align: center">

bb

@@ -3707,13 +3685,15 @@ test.describe.parallel('Tables', () => {
+ class="PlaygroundEditorTheme__tableCellSelected" + style="text-align: center">

d

+ class="PlaygroundEditorTheme__tableCellSelected" + style="text-align: center">

e

diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index cbed93864d1..657814a0041 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -151,16 +151,6 @@ position: relative; outline: none; } -.PlaygroundEditorTheme__tableCellSortedIndicator { - display: block; - opacity: 0.5; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 4px; - background-color: #999; -} .PlaygroundEditorTheme__tableCellResizer { position: absolute; right: -4px; @@ -175,21 +165,18 @@ text-align: start; } .PlaygroundEditorTheme__tableCellSelected { - background-color: #c9dbf0; + caret-color: transparent; } -.PlaygroundEditorTheme__tableCellPrimarySelected { - border: 2px solid rgb(60, 132, 244); - display: block; - height: calc(100% - 2px); +.PlaygroundEditorTheme__tableCellSelected::after { position: absolute; - width: calc(100% - 2px); - left: -1px; - top: -1px; - z-index: 2; -} -.PlaygroundEditorTheme__tableCellEditing { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); - border-radius: 3px; + left: 0; + right: 0; + bottom: 0; + top: 0; + background-color: rgb(172, 206, 247); + opacity: 0.6; + content: ''; + pointer-events: none; } .PlaygroundEditorTheme__tableAddColumns { position: absolute; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index e1c87638895..882bb879898 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -95,13 +95,9 @@ const theme: EditorThemeClasses = { tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton', tableCellActionButtonContainer: 'PlaygroundEditorTheme__tableCellActionButtonContainer', - tableCellEditing: 'PlaygroundEditorTheme__tableCellEditing', tableCellHeader: 'PlaygroundEditorTheme__tableCellHeader', - tableCellPrimarySelected: 'PlaygroundEditorTheme__tableCellPrimarySelected', tableCellResizer: 'PlaygroundEditorTheme__tableCellResizer', tableCellSelected: 'PlaygroundEditorTheme__tableCellSelected', - tableCellSortedIndicator: 'PlaygroundEditorTheme__tableCellSortedIndicator', - tableResizeRuler: 'PlaygroundEditorTheme__tableCellResizeRuler', tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping', tableScrollableWrapper: 'PlaygroundEditorTheme__tableScrollableWrapper', tableSelected: 'PlaygroundEditorTheme__tableSelected', diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index 525a8bce82c..c43e7fe1c9e 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -122,10 +122,8 @@ export class TableCellNode extends ElementNode { this.__backgroundColor = null; } - createDOM(config: EditorConfig): HTMLElement { - const element = document.createElement( - this.getTag(), - ) as HTMLTableCellElement; + createDOM(config: EditorConfig): HTMLTableCellElement { + const element = document.createElement(this.getTag()); if (this.__width) { element.style.width = `${this.__width}px`; @@ -150,33 +148,27 @@ export class TableCellNode extends ElementNode { } exportDOM(editor: LexicalEditor): DOMExportOutput { - const {element} = super.exportDOM(editor); + const output = super.exportDOM(editor); - if (element) { - const element_ = element as HTMLTableCellElement; - element_.style.border = '1px solid black'; + if (output.element) { + const element = output.element as HTMLTableCellElement; + element.style.border = '1px solid black'; if (this.__colSpan > 1) { - element_.colSpan = this.__colSpan; + element.colSpan = this.__colSpan; } if (this.__rowSpan > 1) { - element_.rowSpan = this.__rowSpan; + element.rowSpan = this.__rowSpan; } - element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`; + element.style.width = `${this.getWidth() || COLUMN_WIDTH}px`; - element_.style.verticalAlign = 'top'; - element_.style.textAlign = 'start'; - - const backgroundColor = this.getBackgroundColor(); - if (backgroundColor !== null) { - element_.style.backgroundColor = backgroundColor; - } else if (this.hasHeader()) { - element_.style.backgroundColor = '#f2f3f5'; + element.style.verticalAlign = 'top'; + element.style.textAlign = 'start'; + if (this.__backgroundColor === null && this.hasHeader()) { + element.style.backgroundColor = '#f2f3f5'; } } - return { - element, - }; + return output; } exportJSON(): SerializedTableCellNode { @@ -211,7 +203,7 @@ export class TableCellNode extends ElementNode { return self; } - getTag(): string { + getTag(): 'th' | 'td' { return this.hasHeader() ? 'th' : 'td'; } diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 2a1e4945037..4bb6d3944c6 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -27,7 +27,12 @@ import { $getClipboardDataFromSelection, copyToClipboard, } from '@lexical/clipboard'; -import {$findMatchingParent, objectKlassEquals} from '@lexical/utils'; +import { + $findMatchingParent, + addClassNamesToElement, + objectKlassEquals, + removeClassNamesFromElement, +} from '@lexical/utils'; import { $createParagraphNode, $createRangeSelectionFromDom, @@ -1553,24 +1558,15 @@ function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) { } } -const BROWSER_BLUE_RGB = '172,206,247'; function $addHighlightToDOM(editor: LexicalEditor, cell: TableDOMCell): void { const element = cell.elem; + const editorThemeClasses = editor._config.theme; const node = $getNearestNodeFromDOMNode(element); invariant( $isTableCellNode(node), 'Expected to find LexicalNode from Table Cell DOMNode', ); - const backgroundColor = node.getBackgroundColor(); - if (backgroundColor === null) { - element.style.setProperty('background-color', `rgb(${BROWSER_BLUE_RGB})`); - } else { - element.style.setProperty( - 'background-image', - `linear-gradient(to right, rgba(${BROWSER_BLUE_RGB},0.85), rgba(${BROWSER_BLUE_RGB},0.85))`, - ); - } - element.style.setProperty('caret-color', 'transparent'); + addClassNamesToElement(element, editorThemeClasses.tableCellSelected); } function $removeHighlightFromDOM( @@ -1583,12 +1579,8 @@ function $removeHighlightFromDOM( $isTableCellNode(node), 'Expected to find LexicalNode from Table Cell DOMNode', ); - const backgroundColor = node.getBackgroundColor(); - if (backgroundColor === null) { - element.style.removeProperty('background-color'); - } - element.style.removeProperty('background-image'); - element.style.removeProperty('caret-color'); + const editorThemeClasses = editor._config.theme; + removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected); } export function $findCellNode(node: LexicalNode): null | TableCellNode { diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 9223e571544..60722c6f32b 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -135,14 +135,10 @@ export type EditorThemeClasses = { tableAddRows?: EditorThemeClassName; tableCellActionButton?: EditorThemeClassName; tableCellActionButtonContainer?: EditorThemeClassName; - tableCellPrimarySelected?: EditorThemeClassName; tableCellSelected?: EditorThemeClassName; tableCell?: EditorThemeClassName; - tableCellEditing?: EditorThemeClassName; tableCellHeader?: EditorThemeClassName; tableCellResizer?: EditorThemeClassName; - tableCellSortedIndicator?: EditorThemeClassName; - tableResizeRuler?: EditorThemeClassName; tableRow?: EditorThemeClassName; tableScrollableWrapper?: EditorThemeClassName; tableSelected?: EditorThemeClassName; From 149949f8511bf9afac765c357bddf27239b10905 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 25 Nov 2024 16:34:11 +0100 Subject: [PATCH 105/133] [lexical-selection] Bug Fix: Wrong selection type in $setBlocksType (#6867) --- packages/lexical-selection/flow/LexicalSelection.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-selection/flow/LexicalSelection.js.flow b/packages/lexical-selection/flow/LexicalSelection.js.flow index d1d68d66376..bc70e777b53 100644 --- a/packages/lexical-selection/flow/LexicalSelection.js.flow +++ b/packages/lexical-selection/flow/LexicalSelection.js.flow @@ -51,7 +51,7 @@ declare export function $wrapNodes( wrappingElement?: ElementNode, ): void; declare export function $setBlocksType( - selection: RangeSelection, + selection: BaseSelection | null, createElement: () => ElementNode, ): void; declare export function $isAtNodeEnd(point: Point): boolean; From 00a8d8701606ab1057bfe8118aceadb20180eaa2 Mon Sep 17 00:00:00 2001 From: "C." <106287207+citruscai@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:03:17 -0600 Subject: [PATCH 106/133] Add Documentation for RootNode's semantic and use case (#6869) --- packages/lexical-website/docs/concepts/nodes.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/lexical-website/docs/concepts/nodes.md b/packages/lexical-website/docs/concepts/nodes.md index aedb7bb43cb..baa60eb92c5 100644 --- a/packages/lexical-website/docs/concepts/nodes.md +++ b/packages/lexical-website/docs/concepts/nodes.md @@ -25,6 +25,23 @@ There is only ever a single `RootNode` in an `EditorState` and it is always at t - To get the text content of the entire editor, you should use `rootNode.getTextContent()`. - To avoid selection issues, Lexical forbids insertion of text nodes directly into a `RootNode`. +#### Semantics and Use Cases + +The `RootNode` has specific characteristics and restrictions to maintain editor integrity: + +1. **Non-extensibility** + The `RootNode` cannot be subclassed or replaced with a custom implementation. It is designed as a fixed part of the editor architecture. + +2. **Exclusion from Mutation Listeners** + The `RootNode` does not participate in mutation listeners. Instead, use a root-level or update listener to observe changes at the document level. + +3. **Compatibility with Node Transforms** + While the `RootNode` is not "part of the document" in the traditional sense, it can still appear to be in some cases, such as during serialization or when applying node transforms. + +4. **Document-Level Metadata** + If you are attempting to use the `RootNode` for document-level metadata (e.g., undo/redo support), consider alternative designs. Currently, Lexical does not provide direct facilities for this use case, but solutions like creating a shadow root under the `RootNode` might work. + +By design, the `RootNode` serves as a container for the editor's content rather than an active part of the document's logical structure. This approach simplifies operations like serialization and keeps the focus on content nodes. ### [`LineBreakNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalLineBreakNode.ts) From 3c78cbb9f08a1ba5a262e4bf1e328050db2787cb Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 26 Nov 2024 07:46:31 -0800 Subject: [PATCH 107/133] [lexical-utils] Bug Fix: Add feature detection to calculateZoomLevel (#6864) Co-authored-by: Sherry --- packages/lexical-utils/src/index.ts | 33 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index f9dad4ea5fc..68bafe208d8 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -647,19 +647,38 @@ export function $insertFirst(parent: ElementNode, node: LexicalNode): void { } } +let NEEDS_MANUAL_ZOOM = IS_FIREFOX || !CAN_USE_DOM ? false : undefined; +function needsManualZoom(): boolean { + if (NEEDS_MANUAL_ZOOM === undefined) { + // If the browser implements standardized CSS zoom, then the client rect + // will be wider after zoom is applied + // https://chromestatus.com/feature/5198254868529152 + // https://github.com/facebook/lexical/issues/6863 + const div = document.createElement('div'); + div.style.cssText = + 'position: absolute; opacity: 0; width: 100px; left: -1000px;'; + document.body.appendChild(div); + const noZoom = div.getBoundingClientRect(); + div.style.setProperty('zoom', '2'); + NEEDS_MANUAL_ZOOM = div.getBoundingClientRect().width === noZoom.width; + document.body.removeChild(div); + } + return NEEDS_MANUAL_ZOOM; +} + /** * Calculates the zoom level of an element as a result of using - * css zoom property. + * css zoom property. For browsers that implement standardized CSS + * zoom (Firefox, Chrome >= 128), this will always return 1. * @param element */ export function calculateZoomLevel(element: Element | null): number { - if (IS_FIREFOX) { - return 1; - } let zoom = 1; - while (element) { - zoom *= Number(window.getComputedStyle(element).getPropertyValue('zoom')); - element = element.parentElement; + if (needsManualZoom()) { + while (element) { + zoom *= Number(window.getComputedStyle(element).getPropertyValue('zoom')); + element = element.parentElement; + } } return zoom; } From ff10b1d820097cdc248eefe531c627323c41f8db Mon Sep 17 00:00:00 2001 From: Bedru Umer <63902795+bedre7@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:48:17 +0300 Subject: [PATCH 108/133] [lexical-playground] Bug Fix: autocomplete format before and after insertion (#6845) --- .../__tests__/e2e/Autocomplete.spec.mjs | 112 ++++++++++++++++-- packages/lexical-playground/src/App.tsx | 33 +++--- .../src/context/SharedAutocompleteContext.tsx | 71 ----------- .../src/nodes/AutocompleteNode.tsx | 72 +++++------ .../src/plugins/AutocompletePlugin/index.tsx | 43 +++++-- 5 files changed, 180 insertions(+), 151 deletions(-) delete mode 100644 packages/lexical-playground/src/context/SharedAutocompleteContext.tsx diff --git a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs index 1f1cf21bcae..39d3ef16e50 100644 --- a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs @@ -6,6 +6,14 @@ * */ +import { + decreaseFontSize, + increaseFontSize, + toggleBold, + toggleItalic, + toggleStrikethrough, + toggleUnderline, +} from '../keyboardShortcuts/index.mjs'; import { assertHTML, focusEditor, @@ -30,14 +38,12 @@ test.describe('Autocomplete', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> Sort by alpha - - - betical (TAB) - + + betical (TAB) -

`, html` @@ -45,8 +51,7 @@ test.describe('Autocomplete', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> Sort by alpha - -
+

`, ); @@ -58,7 +63,94 @@ test.describe('Autocomplete', () => {

- Sort by alphabetical order: + Sort by alpha + + betical order: + +

+ `, + ); + }); + + test('Can autocomplete in the same format as the original text', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await toggleBold(page); + await toggleItalic(page); + await toggleUnderline(page); + await toggleStrikethrough(page); + await increaseFontSize(page); + + await page.keyboard.type('Test'); + await sleep(500); + + await assertHTML( + page, + html` +

+ + Test + + + imonials (TAB) + +

+ `, + html` +

+ + Test + + +

+ `, + ); + + await page.keyboard.press('Tab'); + + await toggleBold(page); + await toggleItalic(page); + await toggleUnderline(page); + await toggleStrikethrough(page); + await decreaseFontSize(page); + + await page.keyboard.type(' 2024'); + + await assertHTML( + page, + html` +

+ + Test + + + imonials + + 2024

`, ); diff --git a/packages/lexical-playground/src/App.tsx b/packages/lexical-playground/src/App.tsx index 6f20d8c3841..aa4852cf87e 100644 --- a/packages/lexical-playground/src/App.tsx +++ b/packages/lexical-playground/src/App.tsx @@ -22,7 +22,6 @@ import { import {isDevPlayground} from './appSettings'; import {FlashMessageContext} from './context/FlashMessageContext'; import {SettingsContext, useSettings} from './context/SettingsContext'; -import {SharedAutocompleteContext} from './context/SharedAutocompleteContext'; import {SharedHistoryContext} from './context/SharedHistoryContext'; import {ToolbarContext} from './context/ToolbarContext'; import Editor from './Editor'; @@ -211,24 +210,22 @@ function App(): JSX.Element { - - -
- - Lexical Logo - -
-
- -
- - {isDevPlayground ? : null} - {isDevPlayground ? : null} - {isDevPlayground ? : null} + +
+ + Lexical Logo + +
+
+ +
+ + {isDevPlayground ? : null} + {isDevPlayground ? : null} + {isDevPlayground ? : null} - {measureTypingPerf ? : null} -
-
+ {measureTypingPerf ? : null} +
diff --git a/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx b/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx deleted file mode 100644 index 4f282709eea..00000000000 --- a/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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 * as React from 'react'; -import { - createContext, - ReactNode, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; - -type Suggestion = null | string; -type CallbackFn = (newSuggestion: Suggestion) => void; -type SubscribeFn = (callbackFn: CallbackFn) => () => void; -type PublishFn = (newSuggestion: Suggestion) => void; -type ContextShape = [SubscribeFn, PublishFn]; -type HookShape = [suggestion: Suggestion, setSuggestion: PublishFn]; - -const Context: React.Context = createContext([ - (_cb) => () => { - return; - }, - (_newSuggestion: Suggestion) => { - return; - }, -]); - -export const SharedAutocompleteContext = ({ - children, -}: { - children: ReactNode; -}): JSX.Element => { - const context: ContextShape = useMemo(() => { - let suggestion: Suggestion | null = null; - const listeners: Set = new Set(); - return [ - (cb: (newSuggestion: Suggestion) => void) => { - cb(suggestion); - listeners.add(cb); - return () => { - listeners.delete(cb); - }; - }, - (newSuggestion: Suggestion) => { - suggestion = newSuggestion; - for (const listener of listeners) { - listener(newSuggestion); - } - }, - ]; - }, []); - return {children}; -}; - -export const useSharedAutocompleteContext = (): HookShape => { - const [subscribe, publish]: ContextShape = useContext(Context); - const [suggestion, setSuggestion] = useState(null); - useEffect(() => { - return subscribe((newSuggestion: Suggestion) => { - setSuggestion(newSuggestion); - }); - }, [subscribe]); - return [suggestion, publish]; -}; diff --git a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx index f3eb6bd715a..220add6396c 100644 --- a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx +++ b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx @@ -7,36 +7,26 @@ */ import type { + DOMExportOutput, EditorConfig, - EditorThemeClassName, LexicalEditor, NodeKey, - SerializedLexicalNode, + SerializedTextNode, Spread, } from 'lexical'; -import {DecoratorNode} from 'lexical'; -import * as React from 'react'; +import {TextNode} from 'lexical'; -import {useSharedAutocompleteContext} from '../context/SharedAutocompleteContext'; import {uuid as UUID} from '../plugins/AutocompletePlugin'; -declare global { - interface Navigator { - userAgentData?: { - mobile: boolean; - }; - } -} - export type SerializedAutocompleteNode = Spread< { uuid: string; }, - SerializedLexicalNode + SerializedTextNode >; -export class AutocompleteNode extends DecoratorNode { +export class AutocompleteNode extends TextNode { /** * A unique uuid is generated for each session and assigned to the instance. * This helps to: @@ -48,7 +38,7 @@ export class AutocompleteNode extends DecoratorNode { __uuid: string; static clone(node: AutocompleteNode): AutocompleteNode { - return new AutocompleteNode(node.__uuid, node.__key); + return new AutocompleteNode(node.__text, node.__uuid, node.__key); } static getType(): 'autocomplete' { @@ -58,7 +48,14 @@ export class AutocompleteNode extends DecoratorNode { static importJSON( serializedNode: SerializedAutocompleteNode, ): AutocompleteNode { - const node = $createAutocompleteNode(serializedNode.uuid); + const node = $createAutocompleteNode( + serializedNode.text, + serializedNode.uuid, + ); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); return node; } @@ -71,8 +68,8 @@ export class AutocompleteNode extends DecoratorNode { }; } - constructor(uuid: string, key?: NodeKey) { - super(key); + constructor(text: string, uuid: string, key?: NodeKey) { + super(text, key); this.__uuid = uuid; } @@ -84,36 +81,23 @@ export class AutocompleteNode extends DecoratorNode { return false; } - createDOM(config: EditorConfig): HTMLElement { - return document.createElement('span'); + exportDOM(_: LexicalEditor): DOMExportOutput { + return {element: null}; } - decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null { + createDOM(config: EditorConfig): HTMLElement { if (this.__uuid !== UUID) { - return null; + return document.createElement('span'); } - return ; + const dom = super.createDOM(config); + dom.classList.add(config.theme.autocomplete); + return dom; } } -export function $createAutocompleteNode(uuid: string): AutocompleteNode { - return new AutocompleteNode(uuid); -} - -function AutocompleteComponent({ - className, -}: { - className: EditorThemeClassName; -}): JSX.Element { - const [suggestion] = useSharedAutocompleteContext(); - const userAgentData = window.navigator.userAgentData; - const isMobile = - userAgentData !== undefined - ? userAgentData.mobile - : window.innerWidth <= 800 && window.innerHeight <= 600; - return ( - - {suggestion} {isMobile ? '(SWIPE \u2B95)' : '(TAB)'} - - ); +export function $createAutocompleteNode( + text: string, + uuid: string, +): AutocompleteNode { + return new AutocompleteNode(text, uuid); } diff --git a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx index 7e32e1f4fb1..fa7d5fe5690 100644 --- a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx @@ -6,7 +6,7 @@ * */ -import type {BaseSelection, NodeKey} from 'lexical'; +import type {BaseSelection, NodeKey, TextNode} from 'lexical'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {$isAtNodeEnd} from '@lexical/selection'; @@ -24,13 +24,21 @@ import { } from 'lexical'; import {useCallback, useEffect} from 'react'; -import {useSharedAutocompleteContext} from '../../context/SharedAutocompleteContext'; +import {useToolbarState} from '../../context/ToolbarContext'; import { $createAutocompleteNode, AutocompleteNode, } from '../../nodes/AutocompleteNode'; import {addSwipeRightListener} from '../../utils/swipe'; +declare global { + interface Navigator { + userAgentData?: { + mobile: boolean; + }; + } +} + type SearchPromise = { dismiss: () => void; promise: Promise; @@ -76,16 +84,27 @@ function useQuery(): (searchText: string) => SearchPromise { }, []); } +function formatSuggestionText(suggestion: string): string { + const userAgentData = window.navigator.userAgentData; + const isMobile = + userAgentData !== undefined + ? userAgentData.mobile + : window.innerWidth <= 800 && window.innerHeight <= 600; + + return `${suggestion} ${isMobile ? '(SWIPE \u2B95)' : '(TAB)'}`; +} + export default function AutocompletePlugin(): JSX.Element | null { const [editor] = useLexicalComposerContext(); - const [, setSuggestion] = useSharedAutocompleteContext(); const query = useQuery(); + const {toolbarState} = useToolbarState(); useEffect(() => { let autocompleteNodeKey: null | NodeKey = null; let lastMatch: null | string = null; let lastSuggestion: null | string = null; let searchPromise: null | SearchPromise = null; + let prevNodeFormat: number = 0; function $clearSuggestion() { const autocompleteNode = autocompleteNodeKey !== null @@ -101,7 +120,7 @@ export default function AutocompletePlugin(): JSX.Element | null { } lastMatch = null; lastSuggestion = null; - setSuggestion(null); + prevNodeFormat = 0; } function updateAsyncSuggestion( refSearchPromise: SearchPromise, @@ -124,12 +143,18 @@ export default function AutocompletePlugin(): JSX.Element | null { return; } const selectionCopy = selection.clone(); - const node = $createAutocompleteNode(uuid); + const prevNode = selection.getNodes()[0] as TextNode; + prevNodeFormat = prevNode.getFormat(); + const node = $createAutocompleteNode( + formatSuggestionText(newSuggestion), + uuid, + ) + .setFormat(prevNodeFormat) + .setStyle(`font-size: ${toolbarState.fontSize}`); autocompleteNodeKey = node.getKey(); selection.insertNodes([node]); $setSelection(selectionCopy); lastSuggestion = newSuggestion; - setSuggestion(newSuggestion); }, {tag: 'history-merge'}, ); @@ -175,7 +200,9 @@ export default function AutocompletePlugin(): JSX.Element | null { if (autocompleteNode === null) { return false; } - const textNode = $createTextNode(lastSuggestion); + const textNode = $createTextNode(lastSuggestion) + .setFormat(prevNodeFormat) + .setStyle(`font-size: ${toolbarState.fontSize}`); autocompleteNode.replace(textNode); textNode.selectNext(); $clearSuggestion(); @@ -224,7 +251,7 @@ export default function AutocompletePlugin(): JSX.Element | null { : []), unmountSuggestion, ); - }, [editor, query, setSuggestion]); + }, [editor, query, toolbarState.fontSize]); return null; } From ec66f75762a5e6932fece5ba41ec6102a3e9daf3 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 26 Nov 2024 11:47:06 -0800 Subject: [PATCH 109/133] [lexical-playground] Bug Fix: Preserve the selection using the link editor from a table (#6865) --- .../FloatingLinkEditorPlugin/index.tsx | 32 ++++++--- .../src/plugins/TableCellResizer/index.tsx | 57 ++++++++-------- .../plugins/TableHoverActionsPlugin/index.tsx | 66 ++++++++++--------- 3 files changed, 85 insertions(+), 70 deletions(-) diff --git a/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx index d3a3fef97ed..39c9cb2d695 100644 --- a/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingLinkEditorPlugin/index.tsx @@ -37,6 +37,12 @@ import {getSelectedNode} from '../../utils/getSelectedNode'; import {setFloatingElemPositionForLinkEditor} from '../../utils/setFloatingElemPositionForLinkEditor'; import {sanitizeUrl} from '../../utils/url'; +function preventDefault( + event: React.KeyboardEvent | React.MouseEvent, +): void { + event.preventDefault(); +} + function FloatingLinkEditor({ editor, isLink, @@ -183,19 +189,26 @@ function FloatingLinkEditor({ event: React.KeyboardEvent, ) => { if (event.key === 'Enter') { - event.preventDefault(); - handleLinkSubmission(); + handleLinkSubmission(event); } else if (event.key === 'Escape') { event.preventDefault(); setIsLinkEditMode(false); } }; - const handleLinkSubmission = () => { + const handleLinkSubmission = ( + event: + | React.KeyboardEvent + | React.MouseEvent, + ) => { + event.preventDefault(); if (lastSelection !== null) { if (linkUrl !== '') { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl)); editor.update(() => { + editor.dispatchCommand( + TOGGLE_LINK_COMMAND, + sanitizeUrl(editedLinkUrl), + ); const selection = $getSelection(); if ($isRangeSelection(selection)) { const parent = getSelectedNode(selection).getParent(); @@ -235,7 +248,7 @@ function FloatingLinkEditor({ className="link-cancel" role="button" tabIndex={0} - onMouseDown={(event) => event.preventDefault()} + onMouseDown={preventDefault} onClick={() => { setIsLinkEditMode(false); }} @@ -245,7 +258,7 @@ function FloatingLinkEditor({ className="link-confirm" role="button" tabIndex={0} - onMouseDown={(event) => event.preventDefault()} + onMouseDown={preventDefault} onClick={handleLinkSubmission} /> @@ -262,8 +275,9 @@ function FloatingLinkEditor({ className="link-edit" role="button" tabIndex={0} - onMouseDown={(event) => event.preventDefault()} - onClick={() => { + onMouseDown={preventDefault} + onClick={(event) => { + event.preventDefault(); setEditedLinkUrl(linkUrl); setIsLinkEditMode(true); }} @@ -272,7 +286,7 @@ function FloatingLinkEditor({ className="link-trash" role="button" tabIndex={0} - onMouseDown={(event) => event.preventDefault()} + onMouseDown={preventDefault} onClick={() => { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); }} diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 7f586262645..23f8d1935ab 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -88,27 +88,27 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { useEffect(() => { const onMouseMove = (event: MouseEvent) => { - setTimeout(() => { - const target = event.target; - - if (draggingDirection) { - updateMouseCurrentPos({ - x: event.clientX, - y: event.clientY, - }); - return; - } - updateIsMouseDown(isMouseDownOnEvent(event)); - if (resizerRef.current && resizerRef.current.contains(target as Node)) { - return; - } + const target = event.target; - if (targetRef.current !== target) { - targetRef.current = target as HTMLElement; - const cell = getDOMCellFromTarget(target as HTMLElement); + if (draggingDirection) { + updateMouseCurrentPos({ + x: event.clientX, + y: event.clientY, + }); + return; + } + updateIsMouseDown(isMouseDownOnEvent(event)); + if (resizerRef.current && resizerRef.current.contains(target as Node)) { + return; + } - if (cell && activeCell !== cell) { - editor.update(() => { + if (targetRef.current !== target) { + targetRef.current = target as HTMLElement; + const cell = getDOMCellFromTarget(target as HTMLElement); + + if (cell && activeCell !== cell) { + editor.getEditorState().read( + () => { const tableCellNode = $getNearestNodeFromDOMNode(cell.elem); if (!tableCellNode) { throw new Error('TableCellResizer: Table cell node not found.'); @@ -128,24 +128,21 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { targetRef.current = target as HTMLElement; tableRectRef.current = tableElement.getBoundingClientRect(); updateActiveCell(cell); - }); - } else if (cell == null) { - resetState(); - } + }, + {editor}, + ); + } else if (cell == null) { + resetState(); } - }, 0); + } }; const onMouseDown = (event: MouseEvent) => { - setTimeout(() => { - updateIsMouseDown(true); - }, 0); + updateIsMouseDown(true); }; const onMouseUp = (event: MouseEvent) => { - setTimeout(() => { - updateIsMouseDown(false); - }, 0); + updateIsMouseDown(false); }; const removeRootListener = editor.registerRootListener( diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx index 92a26ff0015..2116d21179a 100644 --- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -66,40 +66,44 @@ function TableHoverActionsContainer({ let hoveredColumnNode: TableCellNode | null = null; let tableDOMElement: HTMLElement | null = null; - editor.update(() => { - const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode); - - if ($isTableCellNode(maybeTableCell)) { - const table = $findMatchingParent(maybeTableCell, (node) => - $isTableNode(node), - ); - if (!$isTableNode(table)) { - return; - } - - tableDOMElement = getTableElement( - table, - editor.getElementByKey(table.getKey()), - ); - - if (tableDOMElement) { - const rowCount = table.getChildrenSize(); - const colCount = ( - (table as TableNode).getChildAtIndex(0) as TableRowNode - )?.getChildrenSize(); - - const rowIndex = $getTableRowIndexFromTableCellNode(maybeTableCell); - const colIndex = - $getTableColumnIndexFromTableCellNode(maybeTableCell); + editor.getEditorState().read( + () => { + const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode); + + if ($isTableCellNode(maybeTableCell)) { + const table = $findMatchingParent(maybeTableCell, (node) => + $isTableNode(node), + ); + if (!$isTableNode(table)) { + return; + } - if (rowIndex === rowCount - 1) { - hoveredRowNode = maybeTableCell; - } else if (colIndex === colCount - 1) { - hoveredColumnNode = maybeTableCell; + tableDOMElement = getTableElement( + table, + editor.getElementByKey(table.getKey()), + ); + + if (tableDOMElement) { + const rowCount = table.getChildrenSize(); + const colCount = ( + (table as TableNode).getChildAtIndex(0) as TableRowNode + )?.getChildrenSize(); + + const rowIndex = + $getTableRowIndexFromTableCellNode(maybeTableCell); + const colIndex = + $getTableColumnIndexFromTableCellNode(maybeTableCell); + + if (rowIndex === rowCount - 1) { + hoveredRowNode = maybeTableCell; + } else if (colIndex === colCount - 1) { + hoveredColumnNode = maybeTableCell; + } } } - } - }); + }, + {editor}, + ); if (tableDOMElement) { const { From 876eba373eb7188b243800fc3955cf002c563035 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 27 Nov 2024 17:43:37 +0000 Subject: [PATCH 110/133] Allow passing params to run-all (#6874) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ee24db444cf..6042bfc3f0e 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "test-e2e-collab-ci-webkit": "npm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"npm run collab\" -P \"npm run test-e2e-collab-webkit -- {@}\"", "test-e2e-prod-ci-chromium": "npm run prepare-ci-prod && cross-env E2E_PORT=4000 npm run test-e2e-prod-chromium", "test-e2e-collab-prod-ci-chromium": " npm run prepare-ci-prod && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"npm run collab\" -P \"npm run test-e2e-collab-prod-chromium -- {@}\"", - "debug-run-all": "npm-run-all debug-test-e2e-*", - "run-all": "npm-run-all test-e2e-*", + "debug-run-all": "npm-run-all 'debug-test-e2e-* -- {1}' --", + "run-all": "npm-run-all 'test-e2e-* -- {1}' --", "debug-test-e2e": "cross-env playwright test --debug", "debug-test-e2e-chromium": "cross-env E2E_BROWSER=chromium playwright test --debug --project=\"chromium\"", "debug-test-e2e-prod-chromium": "cross-env E2E_BROWSER=chromium E2E_PORT=4173 playwright test --debug --project=\"chromium\"", From 8cbc5d4f64f13d7f52f3e472a5fd6f2885a9bc97 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 27 Nov 2024 19:12:28 +0000 Subject: [PATCH 111/133] Add SelectionAlwaysOnDisplay plugin (#6872) --- .flowconfig | 1 + packages/lexical-devtools/tsconfig.json | 3 + .../e2e/SelectionAlwaysOnDisplay.spec.mjs | 62 +++++++++++++++++++ .../__tests__/utils/index.mjs | 3 + packages/lexical-playground/src/Editor.tsx | 4 +- packages/lexical-playground/src/Settings.tsx | 8 +++ .../lexical-playground/src/appSettings.ts | 1 + .../LexicalSelectionAlwaysOnDisplay.js.flow | 13 ++++ packages/lexical-react/package.json | 30 +++++++++ .../src/LexicalSelectionAlwaysOnDisplay.tsx | 20 ++++++ .../src/LexicalTabIndentationPlugin.tsx | 2 +- .../lexical-utils/flow/LexicalUtils.js.flow | 12 ++++ ...icalUtilsInsertNodeToNearestRoot.test.tsx} | 0 packages/lexical-utils/src/index.ts | 1 + .../src/selectionAlwaysOnDisplay.ts | 48 ++++++++++++++ .../lexical-website/docs/react/plugins.md | 9 +++ tsconfig.build.json | 3 + tsconfig.json | 3 + 18 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 packages/lexical-playground/__tests__/e2e/SelectionAlwaysOnDisplay.spec.mjs create mode 100644 packages/lexical-react/flow/LexicalSelectionAlwaysOnDisplay.js.flow create mode 100644 packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx rename packages/lexical-utils/src/__tests__/unit/{LexlcaiUtilsInsertNodeToNearestRoot.test.tsx => LexicalUtilsInsertNodeToNearestRoot.test.tsx} (100%) create mode 100644 packages/lexical-utils/src/selectionAlwaysOnDisplay.ts diff --git a/.flowconfig b/.flowconfig index 04806f560c0..4cc10dfc735 100644 --- a/.flowconfig +++ b/.flowconfig @@ -69,6 +69,7 @@ module.name_mapper='^@lexical/react/LexicalNodeMenuPlugin$' -> '/p module.name_mapper='^@lexical/react/LexicalOnChangePlugin$' -> '/packages/lexical-react/flow/LexicalOnChangePlugin.js.flow' module.name_mapper='^@lexical/react/LexicalPlainTextPlugin$' -> '/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow' module.name_mapper='^@lexical/react/LexicalRichTextPlugin$' -> '/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow' +module.name_mapper='^@lexical/react/LexicalSelectionAlwaysOnDisplay$' -> '/packages/lexical-react/flow/LexicalSelectionAlwaysOnDisplay.js.flow' module.name_mapper='^@lexical/react/LexicalTabIndentationPlugin$' -> '/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow' module.name_mapper='^@lexical/react/LexicalTableOfContents$' -> '/packages/lexical-react/flow/LexicalTableOfContents.js.flow' module.name_mapper='^@lexical/react/LexicalTableOfContentsPlugin$' -> '/packages/lexical-react/flow/LexicalTableOfContentsPlugin.js.flow' diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json index 74c92e8040d..6cc06c3ec2c 100644 --- a/packages/lexical-devtools/tsconfig.json +++ b/packages/lexical-devtools/tsconfig.json @@ -120,6 +120,9 @@ "@lexical/react/LexicalRichTextPlugin": [ "../lexical-react/src/LexicalRichTextPlugin.tsx" ], + "@lexical/react/LexicalSelectionAlwaysOnDisplay": [ + "../lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx" + ], "@lexical/react/LexicalTabIndentationPlugin": [ "../lexical-react/src/LexicalTabIndentationPlugin.tsx" ], diff --git a/packages/lexical-playground/__tests__/e2e/SelectionAlwaysOnDisplay.spec.mjs b/packages/lexical-playground/__tests__/e2e/SelectionAlwaysOnDisplay.spec.mjs new file mode 100644 index 00000000000..638af3e619e --- /dev/null +++ b/packages/lexical-playground/__tests__/e2e/SelectionAlwaysOnDisplay.spec.mjs @@ -0,0 +1,62 @@ +/** + * 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 {selectAll} from '../keyboardShortcuts/index.mjs'; +import { + evaluate, + expect, + focusEditor, + initialize, + locate, + test, +} from '../utils/index.mjs'; + +/* eslint-disable sort-keys-fix/sort-keys-fix */ +test.describe('SelectionAlwaysOnDisplay', () => { + test.beforeEach(({isCollab, page}) => + initialize({isCollab, page, selectionAlwaysOnDisplay: true}), + ); + test(`retain selection works`, async ({page, isPlainText, browserName}) => { + test.skip(isPlainText); // Fixed in #6873 + await focusEditor(page); + await page.keyboard.type('Lexical'); + await selectAll(page); + await locate(page, 'body').click(); + const {distance, widthDifference, heightDifference} = await evaluate( + page, + () => { + function compareNodeAlignment(node1, node2, tolerance = 0) { + const rect1 = node1.getBoundingClientRect(); + const rect2 = node2.getBoundingClientRect(); + const distance_ = Math.sqrt( + Math.pow(rect1.left - rect2.left, 2) + + Math.pow(rect1.top - rect2.top, 2), + ); + const widthDifference_ = Math.abs(rect1.width - rect2.width); + const heightDifference_ = Math.abs(rect1.height - rect2.height); + return { + distance: distance_, + widthDifference: widthDifference_, + heightDifference: heightDifference_, + }; + } + const editorSpan = document.querySelector( + '[contenteditable="true"] span', + ); + const fakeSelection = document.querySelector( + '[style*="background: highlight"]', + ); + return compareNodeAlignment(editorSpan, fakeSelection, 5); + }, + ); + await expect(distance).toBeLessThanOrEqual(5); + await expect(widthDifference).toBeLessThanOrEqual(5); + await expect(heightDifference).toBeLessThanOrEqual(5); + }); +}); +/* eslint-enable sort-keys-fix/sort-keys-fix */ diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index b8b79e9e76c..6dbd0686ae5 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -82,6 +82,7 @@ export async function initialize({ tableCellBackgroundColor, shouldUseLexicalContextMenu, tableHorizontalScroll, + selectionAlwaysOnDisplay, }) { // Tests with legacy events often fail to register keypress, so // slowing it down to reduce flakiness @@ -115,6 +116,7 @@ export async function initialize({ appSettings.tableCellBackgroundColor = tableCellBackgroundColor; } appSettings.shouldUseLexicalContextMenu = !!shouldUseLexicalContextMenu; + appSettings.selectionAlwaysOnDisplay = !!selectionAlwaysOnDisplay; const urlParams = appSettingsToURLParams(appSettings); const url = `http://localhost:${E2E_PORT}/${ @@ -171,6 +173,7 @@ export const test = base.extend({ isPlainText: IS_PLAIN_TEXT, isRichText: IS_RICH_TEXT, legacyEvents: LEGACY_EVENTS, + selectionAlwaysOnDisplay: false, shouldUseLexicalContextMenu: false, }); diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 3fd409b5774..fb0055c51bd 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -20,6 +20,7 @@ import {HorizontalRulePlugin} from '@lexical/react/LexicalHorizontalRulePlugin'; import {ListPlugin} from '@lexical/react/LexicalListPlugin'; import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin'; import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; +import {SelectionAlwaysOnDisplay} from '@lexical/react/LexicalSelectionAlwaysOnDisplay'; import {TabIndentationPlugin} from '@lexical/react/LexicalTabIndentationPlugin'; import {TablePlugin} from '@lexical/react/LexicalTablePlugin'; import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; @@ -95,6 +96,7 @@ export default function Editor(): JSX.Element { tableCellMerge, tableCellBackgroundColor, tableHorizontalScroll, + selectionAlwaysOnDisplay, }, } = useSettings(); const isEditable = useLexicalEditable(); @@ -157,11 +159,11 @@ export default function Editor(): JSX.Element { {isMaxLength && } + {selectionAlwaysOnDisplay && } - diff --git a/packages/lexical-playground/src/Settings.tsx b/packages/lexical-playground/src/Settings.tsx index 2a126f8d0db..4c10f1df767 100644 --- a/packages/lexical-playground/src/Settings.tsx +++ b/packages/lexical-playground/src/Settings.tsx @@ -33,6 +33,7 @@ export default function Settings(): JSX.Element { shouldUseLexicalContextMenu, shouldPreserveNewLinesInMarkdown, // tableHorizontalScroll, + selectionAlwaysOnDisplay, }, } = useSettings(); useEffect(() => { @@ -175,6 +176,13 @@ export default function Settings(): JSX.Element { checked={tableHorizontalScroll} text="Tables have horizontal scroll" /> */} + { + setOption('selectionAlwaysOnDisplay', !selectionAlwaysOnDisplay); + }} + checked={selectionAlwaysOnDisplay} + text="Retain selection" + /> ) : null} diff --git a/packages/lexical-playground/src/appSettings.ts b/packages/lexical-playground/src/appSettings.ts index ab489c3e668..068b1ce1a31 100644 --- a/packages/lexical-playground/src/appSettings.ts +++ b/packages/lexical-playground/src/appSettings.ts @@ -22,6 +22,7 @@ export const DEFAULT_SETTINGS = { isMaxLength: false, isRichText: true, measureTypingPerf: false, + selectionAlwaysOnDisplay: false, shouldPreserveNewLinesInMarkdown: false, shouldUseLexicalContextMenu: false, showNestedEditorTreeView: false, diff --git a/packages/lexical-react/flow/LexicalSelectionAlwaysOnDisplay.js.flow b/packages/lexical-react/flow/LexicalSelectionAlwaysOnDisplay.js.flow new file mode 100644 index 00000000000..b096fb0aa60 --- /dev/null +++ b/packages/lexical-react/flow/LexicalSelectionAlwaysOnDisplay.js.flow @@ -0,0 +1,13 @@ +/** + * 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. + * + * @flow strict + */ + +/** + * LexicalSelectionAlwaysOnDisplay + */ +declare export function SelectionAlwaysOnDisplay(): null; diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index c01c40fd23d..cc6d6eefeeb 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -971,6 +971,36 @@ "default": "./LexicalRichTextPlugin.js" } }, + "./LexicalSelectionAlwaysOnDisplay": { + "import": { + "types": "./LexicalSelectionAlwaysOnDisplay.d.ts", + "development": "./LexicalSelectionAlwaysOnDisplay.dev.mjs", + "production": "./LexicalSelectionAlwaysOnDisplay.prod.mjs", + "node": "./LexicalSelectionAlwaysOnDisplay.node.mjs", + "default": "./LexicalSelectionAlwaysOnDisplay.mjs" + }, + "require": { + "types": "./LexicalSelectionAlwaysOnDisplay.d.ts", + "development": "./LexicalSelectionAlwaysOnDisplay.dev.js", + "production": "./LexicalSelectionAlwaysOnDisplay.prod.js", + "default": "./LexicalSelectionAlwaysOnDisplay.js" + } + }, + "./LexicalSelectionAlwaysOnDisplay.js": { + "import": { + "types": "./LexicalSelectionAlwaysOnDisplay.d.ts", + "development": "./LexicalSelectionAlwaysOnDisplay.dev.mjs", + "production": "./LexicalSelectionAlwaysOnDisplay.prod.mjs", + "node": "./LexicalSelectionAlwaysOnDisplay.node.mjs", + "default": "./LexicalSelectionAlwaysOnDisplay.mjs" + }, + "require": { + "types": "./LexicalSelectionAlwaysOnDisplay.d.ts", + "development": "./LexicalSelectionAlwaysOnDisplay.dev.js", + "production": "./LexicalSelectionAlwaysOnDisplay.prod.js", + "default": "./LexicalSelectionAlwaysOnDisplay.js" + } + }, "./LexicalTabIndentationPlugin": { "import": { "types": "./LexicalTabIndentationPlugin.d.ts", diff --git a/packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx b/packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx new file mode 100644 index 00000000000..69a24adbc58 --- /dev/null +++ b/packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx @@ -0,0 +1,20 @@ +/** + * 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 {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {selectionAlwaysOnDisplay} from '@lexical/utils'; +import {useEffect} from 'react'; + +export function SelectionAlwaysOnDisplay(): null { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + return selectionAlwaysOnDisplay(editor); + }, [editor]); + + return null; +} diff --git a/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx b/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx index 7767526244a..db8c0e8a6e2 100644 --- a/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx +++ b/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx @@ -87,7 +87,7 @@ export function TabIndentationPlugin(): null { const [editor] = useLexicalComposerContext(); useEffect(() => { return registerTabIndentation(editor); - }); + }, [editor]); return null; } diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 3f73f5a1db9..11524eee950 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -65,6 +65,18 @@ declare export function $findMatchingParent( ): LexicalNode | null; type Func = () => void; declare export function mergeRegister(...func: Array): () => void; +declare export function markSelection( + editor: LexicalEditor, + onReposition?: (node: Array) => void, +): () => void; +declare export function positionNodeOnRange( + editor: LexicalEditor, + range: Range, + onReposition: (node: Array) => void, +): () => void; +declare export function selectionAlwaysOnDisplay( + editor: LexicalEditor, +): () => void; declare export function $getNearestBlockElementAncestorOrThrow( startNode: LexicalNode, ): ElementNode; diff --git a/packages/lexical-utils/src/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalUtilsInsertNodeToNearestRoot.test.tsx similarity index 100% rename from packages/lexical-utils/src/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.tsx rename to packages/lexical-utils/src/__tests__/unit/LexicalUtilsInsertNodeToNearestRoot.test.tsx diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 68bafe208d8..8994e3dad65 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -44,6 +44,7 @@ import normalizeClassNames from 'shared/normalizeClassNames'; export {default as markSelection} from './markSelection'; export {default as mergeRegister} from './mergeRegister'; export {default as positionNodeOnRange} from './positionNodeOnRange'; +export {default as selectionAlwaysOnDisplay} from './selectionAlwaysOnDisplay'; export { $splitNode, isBlockDomNode, diff --git a/packages/lexical-utils/src/selectionAlwaysOnDisplay.ts b/packages/lexical-utils/src/selectionAlwaysOnDisplay.ts new file mode 100644 index 00000000000..0677e590512 --- /dev/null +++ b/packages/lexical-utils/src/selectionAlwaysOnDisplay.ts @@ -0,0 +1,48 @@ +/** + * 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 {LexicalEditor} from 'lexical'; + +import markSelection from './markSelection'; + +export default function selectionAlwaysOnDisplay( + editor: LexicalEditor, +): () => void { + let removeSelectionMark: (() => void) | null = null; + + const onSelectionChange = () => { + const domSelection = getSelection(); + const domAnchorNode = domSelection && domSelection.anchorNode; + const editorRootElement = editor.getRootElement(); + + const isSelectionInsideEditor = + domAnchorNode !== null && + editorRootElement !== null && + editorRootElement.contains(domAnchorNode); + + if (isSelectionInsideEditor) { + if (removeSelectionMark !== null) { + removeSelectionMark(); + removeSelectionMark = null; + } + } else { + if (removeSelectionMark === null) { + removeSelectionMark = markSelection(editor); + } + } + }; + + document.addEventListener('selectionchange', onSelectionChange); + + return () => { + if (removeSelectionMark !== null) { + removeSelectionMark(); + } + document.removeEventListener('selectionchange', onSelectionChange); + }; +} diff --git a/packages/lexical-website/docs/react/plugins.md b/packages/lexical-website/docs/react/plugins.md index 1e7f60c0294..c40f8c22251 100644 --- a/packages/lexical-website/docs/react/plugins.md +++ b/packages/lexical-website/docs/react/plugins.md @@ -193,7 +193,16 @@ In order to use `TableOfContentsPlugin`, you need to pass a callback function in Allows you to get a ref to the underlying editor instance outside of LexicalComposer, which is convenient when you want to interact with the editor from a separate part of your application. + ```jsx const editorRef = useRef(null); ``` + +### `LexicalSelectionAlwaysOnDisplay` + +By default, browser text selection becomes invisible when clicking away from the editor. This plugin ensures the selection remains visible. + +```jsx + +``` \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json index 227dca5252a..e214cd1023c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -121,6 +121,9 @@ "@lexical/react/LexicalRichTextPlugin": [ "./packages/lexical-react/src/LexicalRichTextPlugin.tsx" ], + "@lexical/react/LexicalSelectionAlwaysOnDisplay": [ + "./packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx" + ], "@lexical/react/LexicalTabIndentationPlugin": [ "./packages/lexical-react/src/LexicalTabIndentationPlugin.tsx" ], diff --git a/tsconfig.json b/tsconfig.json index 50f67a71a8c..cb2feb1477f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -129,6 +129,9 @@ "@lexical/react/LexicalRichTextPlugin": [ "./packages/lexical-react/src/LexicalRichTextPlugin.tsx" ], + "@lexical/react/LexicalSelectionAlwaysOnDisplay": [ + "./packages/lexical-react/src/LexicalSelectionAlwaysOnDisplay.tsx" + ], "@lexical/react/LexicalTabIndentationPlugin": [ "./packages/lexical-react/src/LexicalTabIndentationPlugin.tsx" ], From 9f6fef15f85e93949d905e3bd7341b2f68028682 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 27 Nov 2024 19:12:59 +0000 Subject: [PATCH 112/133] positionNodeOnRange fixes (#6873) --- packages/lexical-utils/src/markSelection.ts | 84 +++++++++---------- .../lexical-utils/src/positionNodeOnRange.ts | 34 ++++++-- packages/lexical/src/LexicalUtils.ts | 4 +- packages/lexical/src/index.ts | 2 + 4 files changed, 68 insertions(+), 56 deletions(-) diff --git a/packages/lexical-utils/src/markSelection.ts b/packages/lexical-utils/src/markSelection.ts index 9ad57a2da7c..382b57b4bb7 100644 --- a/packages/lexical-utils/src/markSelection.ts +++ b/packages/lexical-utils/src/markSelection.ts @@ -9,17 +9,26 @@ import { $getSelection, $isRangeSelection, + $isTextNode, type EditorState, ElementNode, + getDOMTextNode, type LexicalEditor, TextNode, } from 'lexical'; -import invariant from 'shared/invariant'; import mergeRegister from './mergeRegister'; import positionNodeOnRange from './positionNodeOnRange'; import px from './px'; +/** + * Place one or multiple newly created Nodes at the current selection. Multiple + * nodes will only be created when the selection spans multiple lines (aka + * client rects). + * + * This function can come useful when you want to show the selection but the + * editor has been focused away. + */ export default function markSelection( editor: LexicalEditor, onReposition?: (node: Array) => void, @@ -57,7 +66,7 @@ export default function markSelection( currentAnchorOffset !== previousAnchorOffset || currentAnchorNodeKey !== previousAnchorNode.getKey() || (currentAnchorNode !== previousAnchorNode && - (!(previousAnchorNode instanceof TextNode) || + (!$isTextNode(previousAnchorNode) || currentAnchorNode.updateDOM( previousAnchorNode, currentAnchorNodeDOM, @@ -69,7 +78,7 @@ export default function markSelection( currentFocusOffset !== previousFocusOffset || currentFocusNodeKey !== previousFocusNode.getKey() || (currentFocusNode !== previousFocusNode && - (!(previousFocusNode instanceof TextNode) || + (!$isTextNode(previousFocusNode) || currentFocusNode.updateDOM( previousFocusNode, currentFocusNodeDOM, @@ -82,13 +91,7 @@ export default function markSelection( const focusHTMLElement = editor.getElementByKey( focus.getNode().getKey(), ); - // TODO handle selection beyond the common TextNode - if ( - anchorHTMLElement !== null && - focusHTMLElement !== null && - anchorHTMLElement.tagName === 'SPAN' && - focusHTMLElement.tagName === 'SPAN' - ) { + if (anchorHTMLElement !== null && focusHTMLElement !== null) { const range = document.createRange(); let firstHTMLElement; let firstOffset; @@ -105,48 +108,39 @@ export default function markSelection( lastHTMLElement = focusHTMLElement; lastOffset = focus.offset; } - const firstTextNode = firstHTMLElement.firstChild; - invariant( - firstTextNode !== null, - 'Expected text node to be first child of span', + const firstHTMLElementTextChild = getDOMTextNode(firstHTMLElement); + const lastHTMLElementtextChild = getDOMTextNode(lastHTMLElement); + range.setStart( + firstHTMLElementTextChild || firstHTMLElement, + firstOffset, ); - const lastTextNode = lastHTMLElement.firstChild; - invariant( - lastTextNode !== null, - 'Expected text node to be first child of span', - ); - range.setStart(firstTextNode, firstOffset); - range.setEnd(lastTextNode, lastOffset); + range.setEnd(lastHTMLElementtextChild || lastHTMLElement, lastOffset); removeRangeListener(); removeRangeListener = positionNodeOnRange( editor, range, (domNodes) => { - for (const domNode of domNodes) { - const domNodeStyle = domNode.style; - if (domNodeStyle.background !== 'Highlight') { - domNodeStyle.background = 'Highlight'; - } - if (domNodeStyle.color !== 'HighlightText') { - domNodeStyle.color = 'HighlightText'; - } - if (domNodeStyle.zIndex !== '-1') { - domNodeStyle.zIndex = '-1'; - } - if (domNodeStyle.pointerEvents !== 'none') { - domNodeStyle.pointerEvents = 'none'; - } - if (domNodeStyle.marginTop !== px(-1.5)) { - domNodeStyle.marginTop = px(-1.5); - } - if (domNodeStyle.paddingTop !== px(4)) { - domNodeStyle.paddingTop = px(4); - } - if (domNodeStyle.paddingBottom !== px(0)) { - domNodeStyle.paddingBottom = px(0); + if (onReposition === undefined) { + for (const domNode of domNodes) { + const domNodeStyle = domNode.style; + + if (domNodeStyle.background !== 'Highlight') { + domNodeStyle.background = 'Highlight'; + } + if (domNodeStyle.color !== 'HighlightText') { + domNodeStyle.color = 'HighlightText'; + } + if (domNodeStyle.marginTop !== px(-1.5)) { + domNodeStyle.marginTop = px(-1.5); + } + if (domNodeStyle.paddingTop !== px(4)) { + domNodeStyle.paddingTop = px(4); + } + if (domNodeStyle.paddingBottom !== px(0)) { + domNodeStyle.paddingBottom = px(0); + } } - } - if (onReposition !== undefined) { + } else { onReposition(domNodes); } }, diff --git a/packages/lexical-utils/src/positionNodeOnRange.ts b/packages/lexical-utils/src/positionNodeOnRange.ts index d371bac2d79..2ae7df5e35e 100644 --- a/packages/lexical-utils/src/positionNodeOnRange.ts +++ b/packages/lexical-utils/src/positionNodeOnRange.ts @@ -6,9 +6,8 @@ * */ -import type {LexicalEditor} from 'lexical'; - import {createRectsFromDOMRange} from '@lexical/selection'; +import {isHTMLElement, type LexicalEditor} from 'lexical'; import invariant from 'shared/invariant'; import px from './px'; @@ -20,7 +19,23 @@ const mutationObserverConfig = { subtree: true, }; -export default function positionNodeOnRange( +function prependDOMNode(parent: HTMLElement, node: HTMLElement) { + parent.insertBefore(node, parent.firstChild); +} + +/** + * Place one or multiple newly created Nodes at the passed Range's position. + * Multiple nodes will only be created when the Range spans multiple lines (aka + * client rects). + * + * This function can come particularly useful to highlight particular parts of + * the text without interfering with the EditorState, that will often replicate + * the state across collab and clipboard. + * + * This function accounts for DOM updates which can modify the passed Range. + * Hence, the function return to remove the listener. + */ +export default function mlcPositionNodeOnRange( editor: LexicalEditor, range: Range, onReposition: (node: Array) => void, @@ -30,15 +45,16 @@ export default function positionNodeOnRange( let observer: null | MutationObserver = null; let lastNodes: Array = []; const wrapperNode = document.createElement('div'); + wrapperNode.style.position = 'relative'; function position(): void { invariant(rootDOMNode !== null, 'Unexpected null rootDOMNode'); invariant(parentDOMNode !== null, 'Unexpected null parentDOMNode'); - const {left: rootLeft, top: rootTop} = rootDOMNode.getBoundingClientRect(); - const parentDOMNode_ = parentDOMNode; + const {left: parentLeft, top: parentTop} = + parentDOMNode.getBoundingClientRect(); const rects = createRectsFromDOMRange(editor, range); if (!wrapperNode.isConnected) { - parentDOMNode_.append(wrapperNode); + prependDOMNode(parentDOMNode, wrapperNode); } let hasRepositioned = false; for (let i = 0; i < rects.length; i++) { @@ -51,12 +67,12 @@ export default function positionNodeOnRange( rectNodeStyle.position = 'absolute'; hasRepositioned = true; } - const left = px(rect.left - rootLeft); + const left = px(rect.left - parentLeft); if (rectNodeStyle.left !== left) { rectNodeStyle.left = left; hasRepositioned = true; } - const top = px(rect.top - rootTop); + const top = px(rect.top - parentTop); if (rectNodeStyle.top !== top) { rectNode.style.top = top; hasRepositioned = true; @@ -105,7 +121,7 @@ export default function positionNodeOnRange( return stop(); } const currentParentDOMNode = currentRootDOMNode.parentElement; - if (!(currentParentDOMNode instanceof HTMLElement)) { + if (currentParentDOMNode === null || !isHTMLElement(currentParentDOMNode)) { return stop(); } stop(); diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 28b4f57e943..f4cc88ac86a 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -194,14 +194,14 @@ export function $isTokenOrSegmented(node: TextNode): boolean { return node.isToken() || node.isSegmented(); } -function isDOMNodeLexicalTextNode(node: Node): node is Text { +export function isDOMTextNode(node: Node): node is Text { return node.nodeType === DOM_TEXT_TYPE; } export function getDOMTextNode(element: Node | null): Text | null { let node = element; while (node != null) { - if (isDOMNodeLexicalTextNode(node)) { + if (isDOMTextNode(node)) { return node; } node = node.firstChild; diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 5f1eae58210..562c7c4e387 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -180,10 +180,12 @@ export { $setSelection, $splitNode, getDOMSelection, + getDOMTextNode, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, isBlockDomNode, isDocumentFragment, + isDOMTextNode, isDOMUnmanaged, isHTMLAnchorElement, isHTMLElement, From a016d11010bad0989273cc3c6efd92d09357ac96 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 27 Nov 2024 11:37:44 -0800 Subject: [PATCH 113/133] [lexical-table] Bug Fix: Fix left arrow key handling for table selection (#6875) --- .../6870-table-left-arrow-selection.spec.mjs | 109 ++++++++++++++++++ .../src/LexicalTableSelectionHelpers.ts | 52 ++++++--- 2 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 packages/lexical-playground/__tests__/regression/6870-table-left-arrow-selection.spec.mjs diff --git a/packages/lexical-playground/__tests__/regression/6870-table-left-arrow-selection.spec.mjs b/packages/lexical-playground/__tests__/regression/6870-table-left-arrow-selection.spec.mjs new file mode 100644 index 00000000000..b5e081668be --- /dev/null +++ b/packages/lexical-playground/__tests__/regression/6870-table-left-arrow-selection.spec.mjs @@ -0,0 +1,109 @@ +/** + * 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 {moveToEditorEnd} from '../keyboardShortcuts/index.mjs'; +import { + assertSelection, + focusEditor, + initialize, + insertSampleImage, + insertTable, + IS_TABLE_HORIZONTAL_SCROLL, + test, +} from '../utils/index.mjs'; + +const WRAPPER = IS_TABLE_HORIZONTAL_SCROLL ? [0] : []; + +test.describe('Regression test #6870', () => { + test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); + test('left arrow moves selection around decorators near tables', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isCollab); + test.skip(isPlainText); + + await focusEditor(page); + + await insertTable(page, 2, 1); + await moveToEditorEnd(page); + await insertSampleImage(page); + + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [2], + focusOffset: 1, + focusPath: [2], + }); + + // First ArrowLeft moves inside the image + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); + + await page.keyboard.press('ArrowLeft'); + + // Moves into the last cell of the table + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [1, ...WRAPPER, 2, 0, 0], + focusOffset: 0, + focusPath: [1, ...WRAPPER, 2, 0, 0], + }); + }); + test('left arrow expands selection around decorators near tables', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isCollab); + test.skip(isPlainText); + + await focusEditor(page); + + await insertTable(page, 2, 1); + await moveToEditorEnd(page); + await insertSampleImage(page); + + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [2], + focusOffset: 1, + focusPath: [2], + }); + + await page.keyboard.down('Shift'); + // Only one press is needed here, it won't move into the node + await page.keyboard.press('ArrowLeft'); + + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); + + await page.keyboard.press('ArrowLeft'); + await page.keyboard.up('Shift'); + + // Include the whole table + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [2], + focusOffset: 1, + focusPath: [], + }); + }); +}); diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 4bb6d3944c6..08c6f1f4fd4 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -16,6 +16,7 @@ import type { import type { BaseSelection, ElementFormatType, + ElementNode, LexicalCommand, LexicalEditor, LexicalNode, @@ -1593,6 +1594,27 @@ export function $findTableNode(node: LexicalNode): null | TableNode { return $isTableNode(tableNode) ? tableNode : null; } +function $getBlockParentIfFirstNode(node: LexicalNode): ElementNode | null { + for ( + let prevNode = node, currentNode: LexicalNode | null = node; + currentNode !== null; + prevNode = currentNode, currentNode = currentNode.getParent() + ) { + if ($isElementNode(currentNode)) { + if ( + currentNode !== prevNode && + currentNode.getFirstChild() !== prevNode + ) { + // Not the first child or the initial node + return null; + } else if (!currentNode.isInline()) { + return currentNode; + } + } + } + return null; +} + function $handleArrowKey( editor: LexicalEditor, event: KeyboardEvent, @@ -1611,32 +1633,30 @@ function $handleArrowKey( if (!$isSelectionInTable(selection, tableNode)) { if ($isRangeSelection(selection)) { - if (selection.isCollapsed() && direction === 'backward') { - const anchorType = selection.anchor.type; - const anchorOffset = selection.anchor.offset; - if ( - anchorType !== 'element' && - !(anchorType === 'text' && anchorOffset === 0) - ) { - return false; - } - const anchorNode = selection.anchor.getNode(); - if (!anchorNode) { + if (direction === 'backward') { + if (selection.focus.offset > 0) { return false; } - const parentNode = $findMatchingParent( - anchorNode, - (n) => $isElementNode(n) && !n.isInline(), + const parentNode = $getBlockParentIfFirstNode( + selection.focus.getNode(), ); if (!parentNode) { return false; } const siblingNode = parentNode.getPreviousSibling(); - if (!siblingNode || !$isTableNode(siblingNode)) { + if (!$isTableNode(siblingNode)) { return false; } stopEvent(event); - siblingNode.selectEnd(); + if (event.shiftKey) { + selection.focus.set( + siblingNode.getParentOrThrow().getKey(), + siblingNode.getIndexWithinParent(), + 'element', + ); + } else { + siblingNode.selectEnd(); + } return true; } else if ( event.shiftKey && From 805215b71ceccdf3d370c1a8180d1ea1dc23db6f Mon Sep 17 00:00:00 2001 From: Oluwasanya Olaoluwa Date: Thu, 28 Nov 2024 08:15:24 +0100 Subject: [PATCH 114/133] [lexical-playground] Fix: Hidden comment button on narrow screens or vertical monitors (#6871) --- .../lexical-playground/src/plugins/CommentPlugin/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.css b/packages/lexical-playground/src/plugins/CommentPlugin/index.css index 7f7a035fcc7..b0cc7257f03 100644 --- a/packages/lexical-playground/src/plugins/CommentPlugin/index.css +++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.css @@ -43,7 +43,7 @@ i.add-comment { background-image: url(../../images/icons/chat-left-text.svg); } -@media (max-width: 1024px) { +@media (max-width: 600px) { .CommentPlugin_AddCommentBox { display: none; } @@ -164,7 +164,7 @@ i.comments { transition: opacity 0.2s linear; } -@media (max-width: 1024px) { +@media (max-width: 600px) { .CommentPlugin_ShowCommentsButton { display: none; } From 0d1bb6670f71a70b2ad18243fee7ff4a0309a20f Mon Sep 17 00:00:00 2001 From: "C." <106287207+citruscai@users.noreply.github.com> Date: Thu, 28 Nov 2024 01:43:16 -0600 Subject: [PATCH 115/133] [lexical-playground] Feature: Highlight special strings with format (#6860) --- .../__tests__/e2e/SpecialTexts.spec.mjs | 96 ++++++++++++++++++ .../__tests__/utils/index.mjs | 6 ++ packages/lexical-playground/src/Editor.tsx | 3 + packages/lexical-playground/src/Settings.tsx | 12 +++ .../lexical-playground/src/appSettings.ts | 1 + .../src/nodes/PlaygroundNodes.ts | 2 + .../src/nodes/SpecialTextNode.tsx | 97 +++++++++++++++++++ .../src/plugins/SpecialTextPlugin/index.ts | 72 ++++++++++++++ .../src/themes/PlaygroundEditorTheme.css | 5 + .../src/themes/PlaygroundEditorTheme.ts | 1 + packages/lexical/src/LexicalEditor.ts | 1 + 11 files changed, 296 insertions(+) create mode 100644 packages/lexical-playground/__tests__/e2e/SpecialTexts.spec.mjs create mode 100644 packages/lexical-playground/src/nodes/SpecialTextNode.tsx create mode 100644 packages/lexical-playground/src/plugins/SpecialTextPlugin/index.ts diff --git a/packages/lexical-playground/__tests__/e2e/SpecialTexts.spec.mjs b/packages/lexical-playground/__tests__/e2e/SpecialTexts.spec.mjs new file mode 100644 index 00000000000..82304e24dac --- /dev/null +++ b/packages/lexical-playground/__tests__/e2e/SpecialTexts.spec.mjs @@ -0,0 +1,96 @@ +/** + * 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 { + assertHTML, + focusEditor, + html, + initialize, + test, + waitForSelector, +} from '../utils/index.mjs'; + +test.describe('Special Text', () => { + test.use({shouldAllowHighlightingWithBrackets: true}); + test.beforeEach(({isCollab, page, shouldAllowHighlightingWithBrackets}) => + initialize({ + isCollab, + page, + shouldAllowHighlightingWithBrackets, + }), + ); + test('should handle a single special text', async ({page, isCollab}) => { + await focusEditor(page); + await page.keyboard.type('[MLH Fellowship]'); + await waitForSelector(page, '.PlaygroundEditorTheme__specialText'); + + await assertHTML( + page, + html` +

+ + MLH Fellowship + +

+ `, + ); + }); + test('should handle multiple special texts', async ({page, isCollab}) => { + await focusEditor(page); + await page.keyboard.type('[MLH Fellowship] [MLH Fellowship]'); + await waitForSelector(page, '.PlaygroundEditorTheme__specialText'); + await assertHTML( + page, + html` +

+ + MLH Fellowship + + + + MLH Fellowship + +

+ `, + ); + }); + + test('should not work when the option to use brackets for highlighting is disabled', async ({ + page, + isCollab, + shouldAllowHighlightingWithBrackets, + }) => { + await initialize({ + isCollab, + page, + shouldAllowHighlightingWithBrackets: false, + }); + await focusEditor(page); + await page.keyboard.type('[MLH Fellowship]'); + await assertHTML( + page, + html` +

+ [MLH Fellowship] +

+ `, + ); + }); +}); diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 6dbd0686ae5..ed81a581866 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -82,6 +82,7 @@ export async function initialize({ tableCellBackgroundColor, shouldUseLexicalContextMenu, tableHorizontalScroll, + shouldAllowHighlightingWithBrackets, selectionAlwaysOnDisplay, }) { // Tests with legacy events often fail to register keypress, so @@ -116,6 +117,10 @@ export async function initialize({ appSettings.tableCellBackgroundColor = tableCellBackgroundColor; } appSettings.shouldUseLexicalContextMenu = !!shouldUseLexicalContextMenu; + + appSettings.shouldAllowHighlightingWithBrackets = + !!shouldAllowHighlightingWithBrackets; + appSettings.selectionAlwaysOnDisplay = !!selectionAlwaysOnDisplay; const urlParams = appSettingsToURLParams(appSettings); @@ -174,6 +179,7 @@ export const test = base.extend({ isRichText: IS_RICH_TEXT, legacyEvents: LEGACY_EVENTS, selectionAlwaysOnDisplay: false, + shouldAllowHighlightingWithBrackets: false, shouldUseLexicalContextMenu: false, }); diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index fb0055c51bd..1b752ced18c 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -62,6 +62,7 @@ import MentionsPlugin from './plugins/MentionsPlugin'; import PageBreakPlugin from './plugins/PageBreakPlugin'; import PollPlugin from './plugins/PollPlugin'; import ShortcutsPlugin from './plugins/ShortcutsPlugin'; +import SpecialTextPlugin from './plugins/SpecialTextPlugin'; import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'; import TabFocusPlugin from './plugins/TabFocusPlugin'; import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; @@ -96,6 +97,7 @@ export default function Editor(): JSX.Element { tableCellMerge, tableCellBackgroundColor, tableHorizontalScroll, + shouldAllowHighlightingWithBrackets, selectionAlwaysOnDisplay, }, } = useSettings(); @@ -260,6 +262,7 @@ export default function Editor(): JSX.Element { {isAutocomplete && }
{showTableOfContents && }
{shouldUseLexicalContextMenu && } + {shouldAllowHighlightingWithBrackets && } */} + { + setOption( + 'shouldAllowHighlightingWithBrackets', + !shouldAllowHighlightingWithBrackets, + ); + }} + checked={shouldAllowHighlightingWithBrackets} + text="Use Brackets for Highlighting" + /> + { setOption('selectionAlwaysOnDisplay', !selectionAlwaysOnDisplay); diff --git a/packages/lexical-playground/src/appSettings.ts b/packages/lexical-playground/src/appSettings.ts index 068b1ce1a31..f0763e80e31 100644 --- a/packages/lexical-playground/src/appSettings.ts +++ b/packages/lexical-playground/src/appSettings.ts @@ -23,6 +23,7 @@ export const DEFAULT_SETTINGS = { isRichText: true, measureTypingPerf: false, selectionAlwaysOnDisplay: false, + shouldAllowHighlightingWithBrackets: false, shouldPreserveNewLinesInMarkdown: false, shouldUseLexicalContextMenu: false, showNestedEditorTreeView: false, diff --git a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts index e44931905ba..1048fb82ce2 100644 --- a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts +++ b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts @@ -34,6 +34,7 @@ import {LayoutItemNode} from './LayoutItemNode'; import {MentionNode} from './MentionNode'; import {PageBreakNode} from './PageBreakNode'; import {PollNode} from './PollNode'; +import {SpecialTextNode} from './SpecialTextNode'; import {StickyNode} from './StickyNode'; import {TweetNode} from './TweetNode'; import {YouTubeNode} from './YouTubeNode'; @@ -73,6 +74,7 @@ const PlaygroundNodes: Array> = [ PageBreakNode, LayoutContainerNode, LayoutItemNode, + SpecialTextNode, ]; export default PlaygroundNodes; diff --git a/packages/lexical-playground/src/nodes/SpecialTextNode.tsx b/packages/lexical-playground/src/nodes/SpecialTextNode.tsx new file mode 100644 index 00000000000..474241d15d4 --- /dev/null +++ b/packages/lexical-playground/src/nodes/SpecialTextNode.tsx @@ -0,0 +1,97 @@ +/** + * 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 { + EditorConfig, + LexicalNode, + NodeKey, + SerializedTextNode, +} from 'lexical'; + +import {addClassNamesToElement} from '@lexical/utils'; +import {$applyNodeReplacement, TextNode} from 'lexical'; + +/** @noInheritDoc */ +export class SpecialTextNode extends TextNode { + static getType(): string { + return 'specialText'; + } + + static clone(node: SpecialTextNode): SpecialTextNode { + return new SpecialTextNode(node.__text, node.__key); + } + + constructor(text: string, key?: NodeKey) { + super(text, key); + } + + createDOM(config: EditorConfig): HTMLElement { + const dom = document.createElement('span'); + addClassNamesToElement(dom, config.theme.specialText); + dom.textContent = this.getTextContent(); + return dom; + } + + updateDOM( + prevNode: TextNode, + dom: HTMLElement, + config: EditorConfig, + ): boolean { + if (prevNode.__text.startsWith('[') && prevNode.__text.endsWith(']')) { + const strippedText = this.__text.substring(1, this.__text.length - 1); // Strip brackets again + dom.textContent = strippedText; // Update the text content + } + + addClassNamesToElement(dom, config.theme.specialText); + + return false; + } + + static importJSON(serializedNode: SerializedTextNode): SpecialTextNode { + const node = $createSpecialTextNode(serializedNode.text); + node.setFormat(serializedNode.format); + node.setStyle(serializedNode.style); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + return node; + } + + exportJSON(): SerializedTextNode { + return { + ...super.exportJSON(), + type: 'specialText', + }; + } + + isTextEntity(): true { + return true; + } + canInsertTextAfter(): boolean { + return false; // Prevents appending text to this node + } +} + +/** + * Creates a SpecialTextNode with the given text. + * @param text - Text content for the SpecialTextNode. + * @returns A new SpecialTextNode instance. + */ +export function $createSpecialTextNode(text = ''): SpecialTextNode { + return $applyNodeReplacement(new SpecialTextNode(text)); +} + +/** + * Checks if a node is a SpecialTextNode. + * @param node - Node to check. + * @returns True if the node is a SpecialTextNode. + */ +export function $isSpecialTextNode( + node: LexicalNode | null | undefined, +): node is SpecialTextNode { + return node instanceof SpecialTextNode; +} diff --git a/packages/lexical-playground/src/plugins/SpecialTextPlugin/index.ts b/packages/lexical-playground/src/plugins/SpecialTextPlugin/index.ts new file mode 100644 index 00000000000..ecbec844b66 --- /dev/null +++ b/packages/lexical-playground/src/plugins/SpecialTextPlugin/index.ts @@ -0,0 +1,72 @@ +/** + * 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 {LexicalEditor} from 'lexical'; + +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {TextNode} from 'lexical'; +import {useEffect} from 'react'; + +import { + $createSpecialTextNode, + SpecialTextNode, +} from '../../nodes/SpecialTextNode'; + +const BRACKETED_TEXT_REGEX = /\[([^\[\]]+)\]/; // eslint-disable-line + +function $findAndTransformText(node: TextNode): null | TextNode { + const text = node.getTextContent(); + + const match = BRACKETED_TEXT_REGEX.exec(text); + if (match) { + const matchedText = match[1]; + const startIndex = match.index; + + let targetNode; + if (startIndex === 0) { + [targetNode] = node.splitText(startIndex + match[0].length); + } else { + [, targetNode] = node.splitText(startIndex, startIndex + match[0].length); + } + + const specialTextNode = $createSpecialTextNode(matchedText); + targetNode.replace(specialTextNode); + return specialTextNode; + } + + return null; +} + +function $textNodeTransform(node: TextNode): void { + let targetNode: TextNode | null = node; + + while (targetNode !== null) { + if (!targetNode.isSimpleText()) { + return; + } + + targetNode = $findAndTransformText(targetNode); + } +} + +function useTextTransformation(editor: LexicalEditor): void { + useEffect(() => { + if (!editor.hasNodes([SpecialTextNode])) { + throw new Error( + 'SpecialTextPlugin: SpecialTextNode not registered on editor', + ); + } + + return editor.registerNodeTransform(TextNode, $textNodeTransform); + }, [editor]); +} + +export default function SpecialTextPlugin(): JSX.Element | null { + const [editor] = useLexicalComposerContext(); + useTextTransformation(editor); + return null; +} diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 657814a0041..931beb900cb 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -448,3 +448,8 @@ outline: 2px solid rgb(60, 132, 244); user-select: none; } + +.PlaygroundEditorTheme__specialText { + background-color: yellow; + font-weight: bold; +} diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index 882bb879898..0b45916782b 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -90,6 +90,7 @@ const theme: EditorThemeClasses = { paragraph: 'PlaygroundEditorTheme__paragraph', quote: 'PlaygroundEditorTheme__quote', rtl: 'PlaygroundEditorTheme__rtl', + specialText: 'PlaygroundEditorTheme__specialText', table: 'PlaygroundEditorTheme__table', tableCell: 'PlaygroundEditorTheme__tableCell', tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton', diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 60722c6f32b..157d8d04f95 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -98,6 +98,7 @@ export type EditorThemeClasses = { code?: EditorThemeClassName; codeHighlight?: Record; hashtag?: EditorThemeClassName; + specialText?: EditorThemeClassName; heading?: { h1?: EditorThemeClassName; h2?: EditorThemeClassName; From 16e49878af5525e2a32b0d39aec5838e04636944 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 28 Nov 2024 14:39:51 -0800 Subject: [PATCH 116/133] [lexical-table] Bug Fix: Fix table tab navigation (#6880) --- .../__tests__/e2e/Tables.spec.mjs | 298 ++++++++++++++++++ .../lexical-table/src/LexicalTableNode.ts | 22 +- .../lexical-table/src/LexicalTableObserver.ts | 19 +- .../src/LexicalTableSelectionHelpers.ts | 70 +++- 4 files changed, 375 insertions(+), 34 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index d85bb4c4ccd..4fd7ca25b30 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -2437,6 +2437,304 @@ test.describe.parallel('Tables', () => { ); }); + test('Merged cell tab navigation forward', async ({ + page, + isPlainText, + isCollab, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 3, 3); + + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + await click(page, '.PlaygroundEditorTheme__tableCell'); + for (const i of Array.from({length: 9 - 2}, (_v, idx) => idx)) { + await page.keyboard.type(String(i)); + await page.keyboard.press('Tab'); + } + await page.keyboard.type('Done!'); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+

+ 0 +

+
+

+ 1 +

+
+

+ 2 +

+
+

+ 3 +

+
+

+ 4 +

+
+

+ 5 +

+
+

+ 6 +

+
+

+ Done! +

+ `, + ); + }); + + test('Merged cell tab navigation reverse', async ({ + page, + isPlainText, + isCollab, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 3, 3); + + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


+ + + + + + + + + + + + + + + + + + + +
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + await click(page, ':nth-match(.PlaygroundEditorTheme__tableCell, 7)'); + for (const i of Array.from({length: 9 - 2}, (_v, idx) => idx)) { + await page.keyboard.type(String(i)); + await page.keyboard.down('Shift'); + await page.keyboard.press('Tab'); + await page.keyboard.up('Shift'); + } + await page.keyboard.type('Done!'); + await assertHTML( + page, + html` +

+ Done! +

+ + + + + + + + + + + + + + + + + + + +
+

+ 6 +

+
+

+ 5 +

+
+

+ 4 +

+
+

+ 3 +

+
+

+ 2 +

+
+

+ 1 +

+
+

+ 0 +

+
+


+ `, + ); + }); + test('Merge with content', async ({page, isPlainText, isCollab}) => { await initialize({isCollab, page}); test.skip(isPlainText); diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 838758e2e83..1de74231576 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -34,7 +34,10 @@ import {PIXEL_VALUE_REG_EXP} from './constants'; import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; import {TableRowNode} from './LexicalTableRowNode'; -import {getTable} from './LexicalTableSelectionHelpers'; +import { + $getNearestTableCellInTableFromDOMNode, + getTable, +} from './LexicalTableSelectionHelpers'; export type SerializedTableNode = Spread< { @@ -270,17 +273,16 @@ export class TableNode extends ElementNode { continue; } - const x = row.findIndex((cell) => { - if (!cell) { - return; + for (let x = 0; x < row.length; x++) { + const cell = row[x]; + if (cell == null) { + continue; } const {elem} = cell; - const cellNode = $getNearestNodeFromDOMNode(elem); - return cellNode === tableCellNode; - }); - - if (x !== -1) { - return {x, y}; + const cellNode = $getNearestTableCellInTableFromDOMNode(this, elem); + if (cellNode !== null && tableCellNode.is(cellNode)) { + return {x, y}; + } } } diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 0d148257121..059c471b96d 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -17,7 +17,6 @@ import { $createRangeSelection, $createTextNode, $getEditor, - $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, $getSelection, @@ -38,7 +37,7 @@ import { type TableSelection, } from './LexicalTableSelection'; import { - $findTableNode, + $getNearestTableCellInTableFromDOMNode, $updateDOMForSelection, getTable, getTableElement, @@ -351,13 +350,15 @@ export class TableObserver { this.focusY = cellY; if (this.isHighlightingCells) { - const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + const focusTableCellNode = $getNearestTableCellInTableFromDOMNode( + tableNode, + cell.elem, + ); if ( this.tableSelection != null && this.anchorCellNodeKey != null && - $isTableCellNode(focusTableCellNode) && - tableNode.is($findTableNode(focusTableCellNode)) + focusTableCellNode !== null ) { this.focusCellNodeKey = focusTableCellNode.getKey(); this.tableSelection = $createTableSelectionFrom( @@ -407,9 +408,13 @@ export class TableObserver { this.anchorX = cell.x; this.anchorY = cell.y; - const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem); + const {tableNode} = this.$lookup(); + const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode( + tableNode, + cell.elem, + ); - if ($isTableCellNode(anchorTableCellNode)) { + if (anchorTableCellNode !== null) { const anchorNodeKey = anchorTableCellNode.getKey(); this.tableSelection = this.tableSelection != null diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 08c6f1f4fd4..7959f1cfd75 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -15,6 +15,7 @@ import type { } from './LexicalTableSelection'; import type { BaseSelection, + EditorState, ElementFormatType, ElementNode, LexicalCommand, @@ -661,23 +662,17 @@ export function applyTableHandlers( } const tableCellNode = $findCellNode(selection.anchor.getNode()); - if (tableCellNode === null) { + if ( + tableCellNode === null || + !tableNode.is($findTableNode(tableCellNode)) + ) { return false; } stopEvent(event); - - const currentCords = tableNode.getCordsFromCellNode( + $selectAdjacentCell( tableCellNode, - tableObserver.table, - ); - - selectTableNodeInDirection( - tableObserver, - tableNode, - currentCords.x, - currentCords.y, - !event.shiftKey ? 'forward' : 'backward', + event.shiftKey ? 'previous' : 'next', ); return true; @@ -975,13 +970,13 @@ export function applyTableHandlers( domSelection.focusNode, ); const isFocusOutside = - focusNode && !tableNode.is($findTableNode(focusNode)); + focusNode && !tableNode.isParentOf(focusNode); const anchorNode = $getNearestNodeFromDOMNode( domSelection.anchorNode, ); const isAnchorInside = - anchorNode && tableNode.is($findTableNode(anchorNode)); + anchorNode && tableNode.isParentOf(anchorNode); if ( isFocusOutside && @@ -1302,6 +1297,36 @@ export function $removeHighlightStyleToTable( }); } +function $selectAdjacentCell( + tableCellNode: TableCellNode, + direction: 'next' | 'previous', +) { + const siblingMethod = + direction === 'next' ? 'getNextSibling' : 'getPreviousSibling'; + const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild'; + const sibling = tableCellNode[siblingMethod](); + if ($isElementNode(sibling)) { + return sibling.selectEnd(); + } + const parentRow = $findMatchingParent(tableCellNode, $isTableRowNode); + invariant(parentRow !== null, 'selectAdjacentCell: Cell not in table row'); + for ( + let nextRow = parentRow[siblingMethod](); + $isTableRowNode(nextRow); + nextRow = nextRow[siblingMethod]() + ) { + const child = nextRow[childMethod](); + if ($isElementNode(child)) { + return child.selectEnd(); + } + } + const parentTable = $findMatchingParent(parentRow, $isTableNode); + invariant(parentTable !== null, 'selectAdjacentCell: Row not in table'); + return direction === 'next' + ? parentTable.selectNext() + : parentTable.selectPrevious(); +} + type Direction = 'backward' | 'forward' | 'up' | 'down'; const selectTableNodeInDirection = ( @@ -1722,11 +1747,11 @@ function $handleArrowKey( ? selection.getNodes()[selection.getNodes().length - 1] : selection.getNodes()[0]; if (selectedNode) { - const tableCellNode = $findMatchingParent( + const tableCellNode = $findParentTableCellNodeInTable( + tableNode, selectedNode, - $isTableCellNode, ); - if (tableCellNode && tableNode.isParentOf(tableCellNode)) { + if (tableCellNode !== null) { const firstDescendant = tableNode.getFirstDescendant(); const lastDescendant = tableNode.getLastDescendant(); if (!firstDescendant || !lastDescendant) { @@ -2239,3 +2264,14 @@ export function $getObserverCellFromCellNodeOrThrow( tableObserver.table, ); } + +export function $getNearestTableCellInTableFromDOMNode( + tableNode: TableNode, + startingDOM: Node, + editorState?: EditorState, +) { + return $findParentTableCellNodeInTable( + tableNode, + $getNearestNodeFromDOMNode(startingDOM, editorState), + ); +} From 9a51939f4d0f4a856161b7ffe3e125b07f9f5fba Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 29 Nov 2024 05:35:53 -0800 Subject: [PATCH 117/133] [lexical-table] Bug Fix: Fix scrollable table exportDOM (#6884) --- packages/lexical-table/src/LexicalTableNode.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 1de74231576..ea14f9e5a9b 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -227,9 +227,12 @@ export class TableNode extends ElementNode { } exportDOM(editor: LexicalEditor): DOMExportOutput { + const {element, after} = super.exportDOM(editor); return { - ...super.exportDOM(editor), after: (tableElement) => { + if (after) { + tableElement = after(tableElement); + } if ( tableElement && isHTMLElement(tableElement) && @@ -249,6 +252,10 @@ export class TableNode extends ElementNode { } return tableElement; }, + element: + element && isHTMLElement(element) && element.nodeName !== 'TABLE' + ? element.querySelector('table') + : element, }; } From 67d0ec56aab99c56c00f8d4276a00815c4cefe57 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 29 Nov 2024 22:49:08 +0800 Subject: [PATCH 118/133] v0.20.1 (#6887) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 39 ++ examples/react-plain-text/package.json | 6 +- examples/react-rich-collab/package.json | 8 +- examples/react-rich/package.json | 6 +- examples/react-table/package.json | 6 +- examples/vanilla-js-plugin/package.json | 12 +- examples/vanilla-js/package.json | 12 +- package-lock.json | 446 +++++++++--------- package.json | 2 +- packages/lexical-clipboard/package.json | 12 +- packages/lexical-code/package.json | 6 +- packages/lexical-devtools-core/package.json | 14 +- packages/lexical-devtools/package.json | 6 +- packages/lexical-dragon/package.json | 4 +- packages/lexical-eslint-plugin/package.json | 2 +- packages/lexical-file/package.json | 4 +- packages/lexical-hashtag/package.json | 6 +- packages/lexical-headless/package.json | 4 +- packages/lexical-history/package.json | 6 +- packages/lexical-html/package.json | 8 +- packages/lexical-link/package.json | 6 +- packages/lexical-list/package.json | 6 +- packages/lexical-mark/package.json | 6 +- packages/lexical-markdown/package.json | 16 +- packages/lexical-offset/package.json | 4 +- packages/lexical-overflow/package.json | 4 +- packages/lexical-plain-text/package.json | 10 +- packages/lexical-playground/package.json | 32 +- packages/lexical-react/package.json | 40 +- packages/lexical-rich-text/package.json | 10 +- packages/lexical-selection/package.json | 4 +- packages/lexical-table/package.json | 8 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 8 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 8 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- scripts/error-codes/codes.json | 40 +- 42 files changed, 465 insertions(+), 388 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8614e6be2a..8e9603aece7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +## v0.20.1 (2024-11-29) + +- lexical-table Bug Fix Fix table tab navigation (#6880) Bob Ippolito +- lexical-playground Feature Highlight special strings with format (#6860) C. +- lexical-playground Fix Hidden comment button on narrow screens or vertical monitors (#6871) Oluwasanya Olaoluwa +- lexical-table Bug Fix Fix left arrow key handling for table selection (#6875) Bob Ippolito +- positionNodeOnRange fixes (#6873) Gerard Rovira +- Add SelectionAlwaysOnDisplay plugin (#6872) Gerard Rovira +- Allow passing params to run-all (#6874) Gerard Rovira +- lexical-playground Bug Fix Preserve the selection using the link editor from a table (#6865) Bob Ippolito +- lexical-playground Bug Fix autocomplete format before and after insertion (#6845) Bedru Umer +- lexical-utils Bug Fix Add feature detection to calculateZoomLevel (#6864) Bob Ippolito +- Add Documentation for RootNodes semantic and use case (#6869) C. +- lexical-selection Bug Fix Wrong selection type in setBlocksType (#6867) Simon +- lexical-table Bug Fix get table-cell background selection color from a class (#6658) Hamza +- lexical-table Bug Fix Resolve table selection issue when the mouse crosses over a portal (#6834) Bob Ippolito +- Lexical Chore Update default skipInitialization to false for registerMutationListener (#6857) Fadekemi Adebayo +- tests npm upgrade cross-spawn (#6856) Sherry +- Feature Deprecate nodesOfType function (#6855) Sachin Mahato +- Lexical Bug Fix backspace bug when deleting nodes with canInsertTextAfter set to false (#6268) Dani Lauzurica +- lexical-link Test Appending inline element nodes to ListNode (#6826) Fadekemi Adebayo +- Chore npm upgrade cross-spawn (#6848) Sherry +- Table Action Menu - fix UI issue with Merge Cells item (#6830) Syed Umar Anis +- lexical-utils Bug Fix Add missing Flow type declarations (#6841) Hadi Hamid +- lexical-react Fix(lexical-react) ContentEditable props type rename (#6837) (Ivan) +- lexical-link Test Removing link from node(children) (#6817) Oluwasanya Olaoluwa +- lexical-table Bug Fix Fix down arrow key handling in TableObserver (#6839) Bob Ippolito +- Prevent initial value fn to be called on rerender (#6835) Maksim Horbachevsky +- Link flow types (#6833) Gerard Rovira +- lexical-examples Chore Add DOMExportOutputMap type to the exportMap (#6827) Ajaezo Kingsley +- lexicallexical-table Feature Scrollable tables with experimental getDOMSlot API (#6759) Bob Ippolito +- lexical-rich-textlexical-plain-text workaround for Korean IME issue on iOS (#6819) wnhlee +- Fix Aria attributes for ContentEditable are ignored (#6814) Oluwasanya Olaoluwa +- Mention nodes shouldnt be spellcheckd ) (#6788) Sevki +- lexical-list Bug Fix Handle appending inline element nodes in ListNode.append (#6791) Aman Harwara +- lexical-mark Bug Fix Stop MarkNode ids array deep copy in clone (#6810) Ebad +- v0.20.0 (#6809) Bob Ippolito +- v0.20.0 Lexical GitHub Actions Bot + ## v0.20.0 (2024-11-07) - Add optional selection argument to getHtmlContent flow type (#6803) Rajiv Anisetti diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index fde9b24aacf..0b72b918372 100644 --- a/examples/react-plain-text/package.json +++ b/examples/react-plain-text/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-plain-text-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.0", - "lexical": "0.20.0", + "@lexical/react": "0.20.1", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index da5cf1bef88..d6c35f512ca 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-collab-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -12,9 +12,9 @@ "server:webrtc": "cross-env HOST=localhost PORT=1235 npx y-webrtc" }, "dependencies": { - "@lexical/react": "0.20.0", - "@lexical/yjs": "0.20.0", - "lexical": "0.20.0", + "@lexical/react": "0.20.1", + "@lexical/yjs": "0.20.1", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0", "y-webrtc": "^10.3.0", diff --git a/examples/react-rich/package.json b/examples/react-rich/package.json index 35918ba999d..11a6deefa16 100644 --- a/examples/react-rich/package.json +++ b/examples/react-rich/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.0", - "lexical": "0.20.0", + "@lexical/react": "0.20.1", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-table/package.json b/examples/react-table/package.json index 037fbd8e494..023bdc63022 100644 --- a/examples/react-table/package.json +++ b/examples/react-table/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-table-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.0", - "lexical": "0.20.0", + "@lexical/react": "0.20.1", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vanilla-js-plugin/package.json b/examples/vanilla-js-plugin/package.json index 403d0199954..522f062453c 100644 --- a/examples/vanilla-js-plugin/package.json +++ b/examples/vanilla-js-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-plugin-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/dragon": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/utils": "0.20.1", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.20.0" + "lexical": "0.20.1" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 89982850634..4dbfc4ef427 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-example", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/dragon": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index 99142fd872d..4f34e28d3b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.20.0", + "version": "0.20.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "workspaces": [ "packages/*" @@ -38926,28 +38926,28 @@ } }, "packages/lexical": { - "version": "0.20.0", + "version": "0.20.1", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1", "prismjs": "^1.27.0" }, "devDependencies": { @@ -38956,7 +38956,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.20.0", + "version": "0.20.1", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -38973,12 +38973,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.0", + "@lexical/devtools-core": "0.20.1", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.0", + "lexical": "0.20.1", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -38986,15 +38986,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "peerDependencies": { "react": ">=17.x", @@ -39003,15 +39003,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -39022,136 +39022,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/code": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/code": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-playground": { - "version": "0.20.0", + "version": "0.20.1", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/file": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/react": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/file": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/react": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", "katex": "^0.16.10", - "lexical": "0.20.0", + "lexical": "0.20.1", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -39175,28 +39175,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.20.0", - "license": "MIT", - "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/devtools-core": "0.20.0", - "@lexical/dragon": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/markdown": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "@lexical/yjs": "0.20.0", - "lexical": "0.20.0", + "version": "0.20.1", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/devtools-core": "0.20.1", + "@lexical/dragon": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/markdown": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "@lexical/yjs": "0.20.1", + "lexical": "0.20.1", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -39206,55 +39206,55 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "lexical": "0.20.0" + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "lexical": "0.20.1" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.20.0", + "version": "0.20.1", "dependencies": { "@docusaurus/core": "3.6.0", "@docusaurus/faster": "3.6.0", @@ -39284,12 +39284,12 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@lexical/offset": "0.20.0", - "@lexical/selection": "0.20.0", - "lexical": "0.20.0" + "@lexical/offset": "0.20.1", + "@lexical/selection": "0.20.1", + "lexical": "0.20.1" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -39322,10 +39322,10 @@ } }, "packages/shared": { - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } }, @@ -43955,19 +43955,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.20.0", + "@lexical/utils": "0.20.1", "@types/prismjs": "^1.26.0", - "lexical": "0.20.0", + "lexical": "0.20.1", "prismjs": "^1.27.0" } }, @@ -43979,7 +43979,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.20.0", + "@lexical/devtools-core": "0.20.1", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -43988,7 +43988,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.20.0", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -44000,18 +44000,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/eslint-plugin": { @@ -44023,152 +44023,152 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/code": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/devtools-core": "0.20.0", - "@lexical/dragon": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/markdown": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "@lexical/yjs": "0.20.0", - "lexical": "0.20.0", + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/devtools-core": "0.20.1", + "@lexical/dragon": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/markdown": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "@lexical/yjs": "0.20.1", + "lexical": "0.20.1", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/clipboard": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "lexical": "0.20.0" + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "lexical": "0.20.1" } }, "@lexical/website": { @@ -44201,9 +44201,9 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.20.0", - "@lexical/selection": "0.20.0", - "lexical": "0.20.0" + "@lexical/offset": "0.20.1", + "@lexical/selection": "0.20.1", + "lexical": "0.20.1" } }, "@mdx-js/mdx": { @@ -56244,26 +56244,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/file": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/react": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/file": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/react": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@types/lodash-es": "^4.14.182", "@vitejs/plugin-react": "^4.2.1", "katex": "^0.16.10", - "lexical": "0.20.0", + "lexical": "0.20.1", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -62551,7 +62551,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.20.0" + "lexical": "0.20.1" } }, "shebang-command": { diff --git a/package.json b/package.json index 6042bfc3f0e..bb783c7aba0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index f515b440818..52809c12883 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index 7265a938997..8eced7c0c3c 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index 9361a8c699c..898e727567d 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/html": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index ed014eed61f..e37b259d426 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -2,7 +2,7 @@ "name": "@lexical/devtools", "description": "Lexical DevTools browser extension", "private": true, - "version": "0.20.0", + "version": "0.20.1", "type": "module", "scripts": { "dev": "wxt", @@ -41,12 +41,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.0", + "@lexical/devtools-core": "0.20.1", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.0", + "lexical": "0.20.1", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index 9d4c1e67857..f43dbad2050 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index 01c8727bc8e..dce543388d4 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index a19c36867f7..0c16305ecb1 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index e591e17fac4..760d08ca9ef 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index 100dcbbc90c..a360a7a43aa 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 75cfee062ab..98754d358d9 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index 77fbe5e4b88..20a7e55148b 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 8c2363d556e..442bd99359e 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index b190078c9fb..112a4658532 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 51dbfa99949..d332743a4d7 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index a14fd94e555..640afed4c3c 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/code": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index 46954e7f762..8d45def7d85 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index faf7c4073ae..6b18fe80f57 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index e8045945b1e..1e40220afd6 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,7 +7,7 @@ "plain-text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 7c298206228..59913f8092c 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.20.0", + "version": "0.20.1", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/file": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/react": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/file": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/react": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/utils": "0.20.1", "katex": "^0.16.10", - "lexical": "0.20.0", + "lexical": "0.20.1", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index cc6d6eefeeb..a9c10c823e6 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/code": "0.20.0", - "@lexical/devtools-core": "0.20.0", - "@lexical/dragon": "0.20.0", - "@lexical/hashtag": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/link": "0.20.0", - "@lexical/list": "0.20.0", - "@lexical/mark": "0.20.0", - "@lexical/markdown": "0.20.0", - "@lexical/overflow": "0.20.0", - "@lexical/plain-text": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "@lexical/text": "0.20.0", - "@lexical/utils": "0.20.0", - "@lexical/yjs": "0.20.0", - "lexical": "0.20.0", + "@lexical/clipboard": "0.20.1", + "@lexical/code": "0.20.1", + "@lexical/devtools-core": "0.20.1", + "@lexical/dragon": "0.20.1", + "@lexical/hashtag": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/link": "0.20.1", + "@lexical/list": "0.20.1", + "@lexical/mark": "0.20.1", + "@lexical/markdown": "0.20.1", + "@lexical/overflow": "0.20.1", + "@lexical/plain-text": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "@lexical/text": "0.20.1", + "@lexical/utils": "0.20.1", + "@lexical/yjs": "0.20.1", + "lexical": "0.20.1", "react-error-boundary": "^3.1.4" }, "peerDependencies": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index 972c69af447..bfd957ef118 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,7 +7,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 88fe2149704..20a2fa3b9ed 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index 8aba5552fe6..6d55ddca96e 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/clipboard": "0.20.0", - "@lexical/utils": "0.20.0", - "lexical": "0.20.0" + "@lexical/clipboard": "0.20.1", + "@lexical/utils": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index bf8042fea89..381274b1d73 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 103f06f7ed3..353b1ef70ea 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.20.0", - "@lexical/selection": "0.20.0", - "@lexical/table": "0.20.0", - "lexical": "0.20.0" + "@lexical/list": "0.20.1", + "@lexical/selection": "0.20.1", + "@lexical/table": "0.20.1", + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index 020be5e1ba7..73f06c57e9a 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.20.0", + "version": "0.20.1", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index 497b72af328..ccb16e52dfc 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,13 +11,13 @@ "crdt" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.20.0", - "@lexical/selection": "0.20.0", - "lexical": "0.20.0" + "@lexical/offset": "0.20.1", + "@lexical/selection": "0.20.1", + "lexical": "0.20.1" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index 385584c84a0..bb878028f68 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index c59c3f6537c..af4e8c17553 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.20.0", + "version": "0.20.1", "dependencies": { - "lexical": "0.20.0" + "lexical": "0.20.1" }, "repository": { "type": "git", diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index 4cb6414e571..85b7e84d0bd 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,7 +1,7 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.20.0", + "version": "0.20.1", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/react": "0.20.1", + "@lexical/utils": "0.20.1", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.20.0", + "lexical": "0.20.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index f5d0384fc65..09554ca218f 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "lexical-esm-nextjs", - "version": "0.20.0", + "version": "0.20.1", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.20.0", - "@lexical/react": "0.20.0", - "lexical": "0.20.0", + "@lexical/plain-text": "0.20.1", + "@lexical/react": "0.20.1", + "lexical": "0.20.1", "next": "^14.2.1", "react": "^18", "react-dom": "^18" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json index df0007067f7..2127aff3e12 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json @@ -1,6 +1,6 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.20.0", + "version": "0.20.1", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.20.0", - "@lexical/history": "0.20.0", - "@lexical/rich-text": "0.20.0", - "@lexical/utils": "0.20.0", + "@lexical/dragon": "0.20.1", + "@lexical/history": "0.20.1", + "@lexical/rich-text": "0.20.1", + "@lexical/utils": "0.20.1", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.20.0", + "lexical": "0.20.1", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index fe602bf04fc..384e63523bd 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -212,5 +212,43 @@ "210": "Expected TableRowNode to have a parent TableNode", "211": "Expected node %s of type %s to have a block ElementNode ancestor", "212": "Expected node %s of type %s to have a block ancestor", - "213": "Expected ancestor to be a block ElementNode" + "213": "Expected ancestor to be a block ElementNode", + "214": "$internalResolveSelectionPoint: node in DOM but not keyToDOMMap", + "215": "$internalResolveSelectionPoint: resolvedElement is not an ElementNode", + "216": "invariant", + "217": "$validatePoint: %s key %s not found in current editorState", + "218": "$validatePoint: %s key %s is not a TextNode", + "219": "$validatePoint: %s point.offset > node.getTextContentSize() (%s > %s)", + "220": "$validatePoint: %s key %s is not an ElementNode", + "221": "$validatePoint: %s point.offset > node.getChildrenSize() (%s > %s)", + "222": "ElementDOMSlot.insertChild: before is not in element", + "223": "ElementDOMSlot.removeChild: dom is not in element", + "224": "ElementDOMSlot.replaceChild: prevDom is not in element", + "225": "indexPath: root is not a parent of child", + "226": "ElementNode.splice: start + deleteCount > oldSize (%s + %s > %s)", + "227": "$reconcileChildren: prevChildren.length !== prevChildrenSize", + "228": "$reconcileChildren: nextChildren.length !== nextChildrenSize", + "229": "TableNode.getDOMSlot: createDOM() did not return a table", + "230": "$getElementForTableNode: Table Element Not Found", + "231": "TableObserver: Expected tableNodeKey %s to be a TableNode", + "232": "TableObserver: Expected to find TableElement in DOM for key %s", + "233": "TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('%s' !== '%s')", + "234": "TableObserver anchorTableCell is null", + "235": "TableObserver focusTableCell is null", + "236": "Expected Table selection", + "237": "No table cells present", + "238": "Expected TableSelection %s to be (or a child of) TableCellNode, got key %s of type %s", + "239": "Expected TableSelection %s cell parent to be a TableRowNode", + "240": "Expected TableSelection %s row parent to be a TableNode", + "241": "Expected TableSelection anchor and focus to be in the same table", + "242": "$createTableSelectionFrom: tableNode %s is not attached", + "243": "$createTableSelectionFrom: anchorCell %s is not in table %s", + "244": "$createTableSelectionFrom: focusCell %s is not in table %s", + "245": "getTableElement: Expecting table in as DOM node for TableNode, not %s", + "246": "applyTableHandlers: editor has no root element set", + "247": "selectAdjacentCell: Cell not in table row", + "248": "selectAdjacentCell: Row not in table", + "249": "getCornerOrThrow: cell %s is not at a corner of rect", + "250": "cellAtCornerOrThrow: %s = %s missing in tableMap", + "251": "$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode" } From 6cbfc0157c39b5936b8cef82c44a2e5688c3653f Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Fri, 29 Nov 2024 15:53:43 +0000 Subject: [PATCH 119/133] [lexical-playground] Fix CSS property (#6886) --- packages/lexical-playground/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 87aeb604a82..87fb8fdbbff 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -767,7 +767,7 @@ i.page-break, .dropdown .item .shortcut { color: #939393; - self-align: flex-end; + align-self: flex-end; } .dropdown .item .active { From 8e0e3007e99e89cc3977996bc5613b6196148526 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 29 Nov 2024 17:37:25 -0800 Subject: [PATCH 120/133] [lexical][lexical-playground] Bug Fix: Allow setEditorState to work correctly inside of an update (#6876) --- .../__tests__/e2e/Autocomplete.spec.mjs | 128 ++++++++++++++++++ .../src/plugins/AutocompletePlugin/index.tsx | 62 ++++----- packages/lexical/src/LexicalEditor.ts | 22 ++- .../src/__tests__/unit/LexicalEditor.test.tsx | 3 +- 4 files changed, 179 insertions(+), 36 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs index 39d3ef16e50..2a7c3156111 100644 --- a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs @@ -13,6 +13,7 @@ import { toggleItalic, toggleStrikethrough, toggleUnderline, + undo, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -155,4 +156,131 @@ test.describe('Autocomplete', () => { `, ); }); + test('Undo does not cause an exception', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + // Autocomplete has known issues in collab https://github.com/facebook/lexical/issues/6844 + test.skip(isCollab); + await focusEditor(page); + await toggleBold(page); + await toggleItalic(page); + await toggleUnderline(page); + await toggleStrikethrough(page); + await increaseFontSize(page); + + await page.keyboard.type('Test'); + await sleep(500); + + await assertHTML( + page, + html` +

+ + Test + + + imonials (TAB) + +

+ `, + html` +

+ + Test + + +

+ `, + ); + + await page.keyboard.press('Tab'); + + await assertHTML( + page, + html` +

+ + Test + + + imonials + +

+ `, + html` +

+ + Test + + +

+ `, + ); + + await undo(page); + + await assertHTML( + page, + html` +

+ + Test + + + imonials (TAB) + +

+ `, + html` +

+ + Test + + +

+ `, + ); + }); }); diff --git a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx index fa7d5fe5690..6f427cdfe7d 100644 --- a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx @@ -12,6 +12,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {$isAtNodeEnd} from '@lexical/selection'; import {mergeRegister} from '@lexical/utils'; import { + $addUpdateTag, $createTextNode, $getNodeByKey, $getSelection, @@ -31,6 +32,8 @@ import { } from '../../nodes/AutocompleteNode'; import {addSwipeRightListener} from '../../utils/swipe'; +const HISTORY_MERGE = {tag: 'history-merge'}; + declare global { interface Navigator { userAgentData?: { @@ -130,34 +133,27 @@ export default function AutocompletePlugin(): JSX.Element | null { // Outdated or no suggestion return; } - editor.update( - () => { - const selection = $getSelection(); - const [hasMatch, match] = $search(selection); - if ( - !hasMatch || - match !== lastMatch || - !$isRangeSelection(selection) - ) { - // Outdated - return; - } - const selectionCopy = selection.clone(); - const prevNode = selection.getNodes()[0] as TextNode; - prevNodeFormat = prevNode.getFormat(); - const node = $createAutocompleteNode( - formatSuggestionText(newSuggestion), - uuid, - ) - .setFormat(prevNodeFormat) - .setStyle(`font-size: ${toolbarState.fontSize}`); - autocompleteNodeKey = node.getKey(); - selection.insertNodes([node]); - $setSelection(selectionCopy); - lastSuggestion = newSuggestion; - }, - {tag: 'history-merge'}, - ); + editor.update(() => { + const selection = $getSelection(); + const [hasMatch, match] = $search(selection); + if (!hasMatch || match !== lastMatch || !$isRangeSelection(selection)) { + // Outdated + return; + } + const selectionCopy = selection.clone(); + const prevNode = selection.getNodes()[0] as TextNode; + prevNodeFormat = prevNode.getFormat(); + const node = $createAutocompleteNode( + formatSuggestionText(newSuggestion), + uuid, + ) + .setFormat(prevNodeFormat) + .setStyle(`font-size: ${toolbarState.fontSize}`); + autocompleteNodeKey = node.getKey(); + selection.insertNodes([node]); + $setSelection(selectionCopy); + lastSuggestion = newSuggestion; + }, HISTORY_MERGE); } function $handleAutocompleteNodeTransform(node: AutocompleteNode) { @@ -187,10 +183,12 @@ export default function AutocompletePlugin(): JSX.Element | null { } }) .catch((e) => { - console.error(e); + if (e !== 'Dismissed') { + console.error(e); + } }); lastMatch = match; - }); + }, HISTORY_MERGE); } function $handleAutocompleteIntent(): boolean { if (lastSuggestion === null || autocompleteNodeKey === null) { @@ -219,13 +217,15 @@ export default function AutocompletePlugin(): JSX.Element | null { editor.update(() => { if ($handleAutocompleteIntent()) { e.preventDefault(); + } else { + $addUpdateTag(HISTORY_MERGE.tag); } }); } function unmountSuggestion() { editor.update(() => { $clearSuggestion(); - }); + }, HISTORY_MERGE); } const rootElem = editor.getRootElement(); diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 157d8d04f95..02be308ba84 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -19,7 +19,7 @@ import invariant from 'shared/invariant'; import {$getRoot, $getSelection, TextNode} from '.'; import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants'; -import {createEmptyEditorState} from './LexicalEditorState'; +import {cloneEditorState, createEmptyEditorState} from './LexicalEditorState'; import {addRootElementEvents, removeRootElementEvents} from './LexicalEvents'; import {$flushRootMutations, initMutationObserver} from './LexicalMutations'; import {LexicalNode} from './LexicalNode'; @@ -1112,6 +1112,16 @@ export class LexicalEditor { ); } + // Ensure that we have a writable EditorState so that transforms can run + // during a historic operation + let writableEditorState = editorState; + if (writableEditorState._readOnly) { + writableEditorState = cloneEditorState(editorState); + writableEditorState._selection = editorState._selection + ? editorState._selection.clone() + : null; + } + $flushRootMutations(this); const pendingEditorState = this._pendingEditorState; const tags = this._updateTags; @@ -1121,11 +1131,10 @@ export class LexicalEditor { if (tag != null) { tags.add(tag); } - $commitPendingUpdates(this); } - this._pendingEditorState = editorState; + this._pendingEditorState = writableEditorState; this._dirtyType = FULL_RECONCILE; this._dirtyElements.set('root', false); this._compositionKey = null; @@ -1134,7 +1143,12 @@ export class LexicalEditor { tags.add(tag); } - $commitPendingUpdates(this); + // Only commit pending updates if not already in an editor.update + // (e.g. dispatchCommand) otherwise this will cause a second commit + // with an already read-only state and selection + if (!this._updating) { + $commitPendingUpdates(this); + } } /** diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 4b89e56c232..cf33a568d3f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -2549,7 +2549,8 @@ describe('LexicalEditor tests', () => { `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, ); editor.setEditorState(state); - expect(editor._editorState).toBe(state); + // A writable version of the EditorState may have been created, we settle for equal serializations + expect(editor._editorState.toJSON()).toEqual(state.toJSON()); expect(editor._pendingEditorState).toBe(null); }); From b4260a666b70e504e44c7ab100c5f2390f992942 Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo <82163647+Shopiley@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:22:15 +0100 Subject: [PATCH 121/133] [lexical-website] Feature: Document the withKlass option for node replacement (#6890) Co-authored-by: Bob Ippolito --- packages/lexical-website/docs/concepts/node-replacement.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-website/docs/concepts/node-replacement.md b/packages/lexical-website/docs/concepts/node-replacement.md index 8090b41ac05..1aafbc50595 100644 --- a/packages/lexical-website/docs/concepts/node-replacement.md +++ b/packages/lexical-website/docs/concepts/node-replacement.md @@ -1,6 +1,6 @@ -# Node Overrides +# Node Overrides / Node Replacements Some of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in the ListPlugin instead of using the core ListNode? That's where Node Overrides can help. @@ -22,6 +22,10 @@ const editorConfig = { ] } ``` +In the snippet above, +- `replace`: Specifies the core node type to be replaced. +- `with`: Defines a transformation function to replace instances of the original node to the custom node. +- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior. Once this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below. From ca24a88694f61c98966678cf0a4879f4b9ac1371 Mon Sep 17 00:00:00 2001 From: Sherry Date: Sun, 1 Dec 2024 11:47:25 +0800 Subject: [PATCH 122/133] v0.20.2 (#6891) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 43 ++ examples/react-plain-text/package.json | 6 +- examples/react-rich-collab/package.json | 8 +- examples/react-rich/package.json | 6 +- examples/react-table/package.json | 6 +- examples/vanilla-js-plugin/package.json | 12 +- examples/vanilla-js/package.json | 12 +- package-lock.json | 442 +++++++++--------- package.json | 2 +- packages/lexical-clipboard/package.json | 12 +- packages/lexical-code/package.json | 6 +- packages/lexical-devtools-core/package.json | 14 +- packages/lexical-devtools/package.json | 6 +- packages/lexical-dragon/package.json | 4 +- packages/lexical-eslint-plugin/package.json | 2 +- packages/lexical-file/package.json | 4 +- packages/lexical-hashtag/package.json | 6 +- packages/lexical-headless/package.json | 4 +- packages/lexical-history/package.json | 6 +- packages/lexical-html/package.json | 8 +- packages/lexical-link/package.json | 6 +- packages/lexical-list/package.json | 6 +- packages/lexical-mark/package.json | 6 +- packages/lexical-markdown/package.json | 16 +- packages/lexical-offset/package.json | 4 +- packages/lexical-overflow/package.json | 4 +- packages/lexical-plain-text/package.json | 10 +- packages/lexical-playground/package.json | 32 +- packages/lexical-react/package.json | 40 +- packages/lexical-rich-text/package.json | 10 +- packages/lexical-selection/package.json | 4 +- packages/lexical-table/package.json | 8 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 8 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 8 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- 41 files changed, 428 insertions(+), 385 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9603aece7..15a945980bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +## v0.20.2 (2024-11-30) + +- lexicallexical-playground Bug Fix Allow setEditorState to work correctly inside of an update (#6876) Bob Ippolito +- lexical-playground Fix CSS property (#6886) Ivaylo Pavlov +- v0.20.1 (#6887) Sherry +- lexical-table Bug Fix Fix scrollable table exportDOM (#6884) Bob Ippolito +- lexical-table Bug Fix Fix table tab navigation (#6880) Bob Ippolito +- lexical-playground Feature Highlight special strings with format (#6860) C. +- lexical-playground Fix Hidden comment button on narrow screens or vertical monitors (#6871) Oluwasanya Olaoluwa +- lexical-table Bug Fix Fix left arrow key handling for table selection (#6875) Bob Ippolito +- positionNodeOnRange fixes (#6873) Gerard Rovira +- Add SelectionAlwaysOnDisplay plugin (#6872) Gerard Rovira +- Allow passing params to run-all (#6874) Gerard Rovira +- lexical-playground Bug Fix Preserve the selection using the link editor from a table (#6865) Bob Ippolito +- lexical-playground Bug Fix autocomplete format before and after insertion (#6845) Bedru Umer +- lexical-utils Bug Fix Add feature detection to calculateZoomLevel (#6864) Bob Ippolito +- Add Documentation for RootNodes semantic and use case (#6869) C. +- lexical-selection Bug Fix Wrong selection type in setBlocksType (#6867) Simon +- lexical-table Bug Fix get table-cell background selection color from a class (#6658) Hamza +- lexical-table Bug Fix Resolve table selection issue when the mouse crosses over a portal (#6834) Bob Ippolito +- Lexical Chore Update default skipInitialization to false for registerMutationListener (#6857) Fadekemi Adebayo +- tests npm upgrade cross-spawn (#6856) Sherry +- Feature Deprecate nodesOfType function (#6855) Sachin Mahato +- Lexical Bug Fix backspace bug when deleting nodes with canInsertTextAfter set to false (#6268) Dani Lauzurica +- lexical-link Test Appending inline element nodes to ListNode (#6826) Fadekemi Adebayo +- Chore npm upgrade cross-spawn (#6848) Sherry +- Table Action Menu - fix UI issue with Merge Cells item (#6830) Syed Umar Anis +- lexical-utils Bug Fix Add missing Flow type declarations (#6841) Hadi Hamid +- lexical-react Fix(lexical-react) ContentEditable props type rename (#6837) (Ivan) +- lexical-link Test Removing link from node(children) (#6817) Oluwasanya Olaoluwa +- lexical-table Bug Fix Fix down arrow key handling in TableObserver (#6839) Bob Ippolito +- Prevent initial value fn to be called on rerender (#6835) Maksim Horbachevsky +- Link flow types (#6833) Gerard Rovira +- lexical-examples Chore Add DOMExportOutputMap type to the exportMap (#6827) Ajaezo Kingsley +- lexicallexical-table Feature Scrollable tables with experimental getDOMSlot API (#6759) Bob Ippolito +- lexical-rich-textlexical-plain-text workaround for Korean IME issue on iOS (#6819) wnhlee +- Fix Aria attributes for ContentEditable are ignored (#6814) Oluwasanya Olaoluwa +- Mention nodes shouldnt be spellcheckd ) (#6788) Sevki +- lexical-list Bug Fix Handle appending inline element nodes in ListNode.append (#6791) Aman Harwara +- lexical-mark Bug Fix Stop MarkNode ids array deep copy in clone (#6810) Ebad +- v0.20.0 (#6809) Bob Ippolito +- v0.20.0 Lexical GitHub Actions Bot + ## v0.20.1 (2024-11-29) - lexical-table Bug Fix Fix table tab navigation (#6880) Bob Ippolito diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index 0b72b918372..26355282515 100644 --- a/examples/react-plain-text/package.json +++ b/examples/react-plain-text/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-plain-text-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.1", - "lexical": "0.20.1", + "@lexical/react": "0.20.2", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index d6c35f512ca..c6b9d221594 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-collab-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -12,9 +12,9 @@ "server:webrtc": "cross-env HOST=localhost PORT=1235 npx y-webrtc" }, "dependencies": { - "@lexical/react": "0.20.1", - "@lexical/yjs": "0.20.1", - "lexical": "0.20.1", + "@lexical/react": "0.20.2", + "@lexical/yjs": "0.20.2", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0", "y-webrtc": "^10.3.0", diff --git a/examples/react-rich/package.json b/examples/react-rich/package.json index 11a6deefa16..6986a9cb25f 100644 --- a/examples/react-rich/package.json +++ b/examples/react-rich/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.1", - "lexical": "0.20.1", + "@lexical/react": "0.20.2", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-table/package.json b/examples/react-table/package.json index 023bdc63022..3de430adb67 100644 --- a/examples/react-table/package.json +++ b/examples/react-table/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-table-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.1", - "lexical": "0.20.1", + "@lexical/react": "0.20.2", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vanilla-js-plugin/package.json b/examples/vanilla-js-plugin/package.json index 522f062453c..7e5651a5b96 100644 --- a/examples/vanilla-js-plugin/package.json +++ b/examples/vanilla-js-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-plugin-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/dragon": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/utils": "0.20.2", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.20.1" + "lexical": "0.20.2" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 4dbfc4ef427..a9b693efb47 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-example", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/dragon": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index 4f34e28d3b0..2bba3934e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.20.1", + "version": "0.20.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "workspaces": [ "packages/*" @@ -38926,28 +38926,28 @@ } }, "packages/lexical": { - "version": "0.20.1", + "version": "0.20.2", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2", "prismjs": "^1.27.0" }, "devDependencies": { @@ -38956,7 +38956,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.20.1", + "version": "0.20.2", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -38973,12 +38973,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.1", + "@lexical/devtools-core": "0.20.2", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.1", + "lexical": "0.20.2", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -38986,15 +38986,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "peerDependencies": { "react": ">=17.x", @@ -39003,15 +39003,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -39022,136 +39022,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/code": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/code": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-playground": { - "version": "0.20.1", + "version": "0.20.2", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/file": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/react": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/file": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/react": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", "katex": "^0.16.10", - "lexical": "0.20.1", + "lexical": "0.20.2", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -39175,28 +39175,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/devtools-core": "0.20.1", - "@lexical/dragon": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/markdown": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "@lexical/yjs": "0.20.1", - "lexical": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/devtools-core": "0.20.2", + "@lexical/dragon": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/markdown": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "@lexical/yjs": "0.20.2", + "lexical": "0.20.2", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -39206,55 +39206,55 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "lexical": "0.20.1" + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "lexical": "0.20.2" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.20.1", + "version": "0.20.2", "dependencies": { "@docusaurus/core": "3.6.0", "@docusaurus/faster": "3.6.0", @@ -39284,12 +39284,12 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "@lexical/offset": "0.20.1", - "@lexical/selection": "0.20.1", - "lexical": "0.20.1" + "@lexical/offset": "0.20.2", + "@lexical/selection": "0.20.2", + "lexical": "0.20.2" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -39322,10 +39322,10 @@ } }, "packages/shared": { - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } }, @@ -43955,19 +43955,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.20.1", + "@lexical/utils": "0.20.2", "@types/prismjs": "^1.26.0", - "lexical": "0.20.1", + "lexical": "0.20.2", "prismjs": "^1.27.0" } }, @@ -43979,7 +43979,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.20.1", + "@lexical/devtools-core": "0.20.2", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -43988,7 +43988,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.20.1", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -44000,18 +44000,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/eslint-plugin": { @@ -44023,152 +44023,152 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/code": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/devtools-core": "0.20.1", - "@lexical/dragon": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/markdown": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "@lexical/yjs": "0.20.1", - "lexical": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/devtools-core": "0.20.2", + "@lexical/dragon": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/markdown": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "@lexical/yjs": "0.20.2", + "lexical": "0.20.2", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/clipboard": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "lexical": "0.20.1" + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "lexical": "0.20.2" } }, "@lexical/website": { @@ -44201,9 +44201,9 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.20.1", - "@lexical/selection": "0.20.1", - "lexical": "0.20.1" + "@lexical/offset": "0.20.2", + "@lexical/selection": "0.20.2", + "lexical": "0.20.2" } }, "@mdx-js/mdx": { @@ -56244,26 +56244,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/file": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/react": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/file": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/react": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@types/lodash-es": "^4.14.182", "@vitejs/plugin-react": "^4.2.1", "katex": "^0.16.10", - "lexical": "0.20.1", + "lexical": "0.20.2", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -62551,7 +62551,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.20.1" + "lexical": "0.20.2" } }, "shebang-command": { diff --git a/package.json b/package.json index bb783c7aba0..c7405f91d61 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index 52809c12883..a48073ab1cd 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index 8eced7c0c3c..a34c9c4b2a4 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index 898e727567d..96399206cb0 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/html": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index e37b259d426..ca173691239 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -2,7 +2,7 @@ "name": "@lexical/devtools", "description": "Lexical DevTools browser extension", "private": true, - "version": "0.20.1", + "version": "0.20.2", "type": "module", "scripts": { "dev": "wxt", @@ -41,12 +41,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.1", + "@lexical/devtools-core": "0.20.2", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.1", + "lexical": "0.20.2", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index f43dbad2050..4ef705819d3 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index dce543388d4..0358c0a7456 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.20.1", + "version": "0.20.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 0c16305ecb1..ed299c5722c 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 760d08ca9ef..a2ce262bf58 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index a360a7a43aa..6a2b08f43f3 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 98754d358d9..308bd7b957e 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index 20a7e55148b..67d18e3c3d0 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 442bd99359e..0734cc586b7 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index 112a4658532..82ed0595e08 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index d332743a4d7..7baa740f9d7 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index 640afed4c3c..d377f60cbea 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/code": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index 8d45def7d85..ed9e7391244 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index 6b18fe80f57..5b00f98fd05 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index 1e40220afd6..14e3877491e 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,7 +7,7 @@ "plain-text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 59913f8092c..ff91e2629fb 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.20.1", + "version": "0.20.2", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/file": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/react": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/file": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/react": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/utils": "0.20.2", "katex": "^0.16.10", - "lexical": "0.20.1", + "lexical": "0.20.2", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index a9c10c823e6..5ab8da645a7 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/code": "0.20.1", - "@lexical/devtools-core": "0.20.1", - "@lexical/dragon": "0.20.1", - "@lexical/hashtag": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/link": "0.20.1", - "@lexical/list": "0.20.1", - "@lexical/mark": "0.20.1", - "@lexical/markdown": "0.20.1", - "@lexical/overflow": "0.20.1", - "@lexical/plain-text": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "@lexical/text": "0.20.1", - "@lexical/utils": "0.20.1", - "@lexical/yjs": "0.20.1", - "lexical": "0.20.1", + "@lexical/clipboard": "0.20.2", + "@lexical/code": "0.20.2", + "@lexical/devtools-core": "0.20.2", + "@lexical/dragon": "0.20.2", + "@lexical/hashtag": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/link": "0.20.2", + "@lexical/list": "0.20.2", + "@lexical/mark": "0.20.2", + "@lexical/markdown": "0.20.2", + "@lexical/overflow": "0.20.2", + "@lexical/plain-text": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "@lexical/text": "0.20.2", + "@lexical/utils": "0.20.2", + "@lexical/yjs": "0.20.2", + "lexical": "0.20.2", "react-error-boundary": "^3.1.4" }, "peerDependencies": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index bfd957ef118..50da7d934d5 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,7 +7,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 20a2fa3b9ed..ef2f7e0cd85 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index 6d55ddca96e..537ca81e74b 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/clipboard": "0.20.1", - "@lexical/utils": "0.20.1", - "lexical": "0.20.1" + "@lexical/clipboard": "0.20.2", + "@lexical/utils": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 381274b1d73..2d1b2a00aa6 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 353b1ef70ea..4180a845c97 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.20.1", - "@lexical/selection": "0.20.1", - "@lexical/table": "0.20.1", - "lexical": "0.20.1" + "@lexical/list": "0.20.2", + "@lexical/selection": "0.20.2", + "@lexical/table": "0.20.2", + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index 73f06c57e9a..0009252a3ae 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.20.1", + "version": "0.20.2", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index ccb16e52dfc..709f9205686 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,13 +11,13 @@ "crdt" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.20.1", - "@lexical/selection": "0.20.1", - "lexical": "0.20.1" + "@lexical/offset": "0.20.2", + "@lexical/selection": "0.20.2", + "lexical": "0.20.2" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index bb878028f68..8dfa8f054c8 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index af4e8c17553..db57f14b276 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.20.1", + "version": "0.20.2", "dependencies": { - "lexical": "0.20.1" + "lexical": "0.20.2" }, "repository": { "type": "git", diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index 85b7e84d0bd..d8c77bb8ae1 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,7 +1,7 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.20.1", + "version": "0.20.2", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/react": "0.20.2", + "@lexical/utils": "0.20.2", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.20.1", + "lexical": "0.20.2", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index 09554ca218f..9fc71139745 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "lexical-esm-nextjs", - "version": "0.20.1", + "version": "0.20.2", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.20.1", - "@lexical/react": "0.20.1", - "lexical": "0.20.1", + "@lexical/plain-text": "0.20.2", + "@lexical/react": "0.20.2", + "lexical": "0.20.2", "next": "^14.2.1", "react": "^18", "react-dom": "^18" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json index 2127aff3e12..3a5e771437b 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json @@ -1,6 +1,6 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.20.1", + "version": "0.20.2", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.20.1", - "@lexical/history": "0.20.1", - "@lexical/rich-text": "0.20.1", - "@lexical/utils": "0.20.1", + "@lexical/dragon": "0.20.2", + "@lexical/history": "0.20.2", + "@lexical/rich-text": "0.20.2", + "@lexical/utils": "0.20.2", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.20.1", + "lexical": "0.20.2", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", From 230dcf251aa9b06d23de4cb2ba0873ef30228655 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Sun, 1 Dec 2024 16:46:08 +0000 Subject: [PATCH 123/133] Fix selected table colors (#6892) --- examples/react-table/src/styles.css | 4 ++-- .../lexical-playground/src/themes/PlaygroundEditorTheme.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/react-table/src/styles.css b/examples/react-table/src/styles.css index ceade988694..32b80315539 100644 --- a/examples/react-table/src/styles.css +++ b/examples/react-table/src/styles.css @@ -496,8 +496,8 @@ i.justify-align { right: 0; bottom: 0; top: 0; - background-color: rgb(172, 206, 247); - opacity: 0.6; + background-color: highlight; + mix-blend-mode: multiply; content: ''; pointer-events: none; } diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 931beb900cb..527085b7539 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -173,8 +173,8 @@ right: 0; bottom: 0; top: 0; - background-color: rgb(172, 206, 247); - opacity: 0.6; + background-color: highlight; + mix-blend-mode: multiply; content: ''; pointer-events: none; } From 6a1cf185e3e9e17cc767fe3d28b14b705a196a92 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 1 Dec 2024 09:01:55 -0800 Subject: [PATCH 124/133] [lexical-table] Bug Fix: TableNode exportDOM fixes for partial table selection (#6889) --- .../lexical-table/src/LexicalTableCellNode.ts | 7 +- .../lexical-table/src/LexicalTableNode.ts | 75 +++++++++- .../lexical-table/src/LexicalTableRowNode.ts | 10 +- .../__tests__/unit/LexicalTableNode.test.tsx | 130 ++++++++++++++++++ 4 files changed, 214 insertions(+), 8 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index c43e7fe1c9e..2b00b8dfb6e 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -26,6 +26,7 @@ import { $isLineBreakNode, $isTextNode, ElementNode, + isHTMLElement, } from 'lexical'; import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants'; @@ -150,8 +151,12 @@ export class TableCellNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const output = super.exportDOM(editor); - if (output.element) { + if (output.element && isHTMLElement(output.element)) { const element = output.element as HTMLTableCellElement; + element.setAttribute( + 'data-temporary-table-cell-lexical-key', + this.getKey(), + ); element.style.border = '1px solid black'; if (this.__colSpan > 1) { element.colSpan = this.__colSpan; diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index ea14f9e5a9b..4a4a2c970fa 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -6,6 +6,8 @@ * */ +import type {TableRowNode} from './LexicalTableRowNode'; + import { addClassNamesToElement, isHTMLElement, @@ -15,6 +17,7 @@ import { $applyNodeReplacement, $getEditor, $getNearestNodeFromDOMNode, + BaseSelection, DOMConversionMap, DOMConversionOutput, DOMExportOutput, @@ -31,13 +34,13 @@ import { import invariant from 'shared/invariant'; import {PIXEL_VALUE_REG_EXP} from './constants'; -import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode'; +import {$isTableCellNode, type TableCellNode} from './LexicalTableCellNode'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; -import {TableRowNode} from './LexicalTableRowNode'; import { $getNearestTableCellInTableFromDOMNode, getTable, } from './LexicalTableSelectionHelpers'; +import {$computeTableMapSkipCellCheck} from './LexicalTableUtils'; export type SerializedTableNode = Spread< { @@ -170,6 +173,14 @@ export class TableNode extends ElementNode { }; } + extractWithChild( + child: LexicalNode, + selection: BaseSelection | null, + destination: 'clone' | 'html', + ): boolean { + return destination === 'html'; + } + getDOMSlot(element: HTMLElement): ElementDOMSlot { const tableElement = (element.nodeName !== 'TABLE' && element.querySelector('table')) || @@ -227,11 +238,12 @@ export class TableNode extends ElementNode { } exportDOM(editor: LexicalEditor): DOMExportOutput { - const {element, after} = super.exportDOM(editor); + const superExport = super.exportDOM(editor); + const {element} = superExport; return { after: (tableElement) => { - if (after) { - tableElement = after(tableElement); + if (superExport.after) { + tableElement = superExport.after(tableElement); } if ( tableElement && @@ -243,11 +255,62 @@ export class TableNode extends ElementNode { if (!tableElement || !isHTMLElement(tableElement)) { return null; } + + // Scan the table map to build a map of table cell key to the columns it needs + const [tableMap] = $computeTableMapSkipCellCheck(this, null, null); + const cellValues = new Map< + NodeKey, + {startColumn: number; colSpan: number} + >(); + for (const mapRow of tableMap) { + for (const mapValue of mapRow) { + const key = mapValue.cell.getKey(); + if (!cellValues.has(key)) { + cellValues.set(key, { + colSpan: mapValue.cell.getColSpan(), + startColumn: mapValue.startColumn, + }); + } + } + } + + // scan the DOM to find the table cell keys that were used and mark those columns + const knownColumns = new Set(); + for (const cellDOM of tableElement.querySelectorAll( + ':scope > tr > [data-temporary-table-cell-lexical-key]', + )) { + const key = cellDOM.getAttribute( + 'data-temporary-table-cell-lexical-key', + ); + if (key) { + const cellSpan = cellValues.get(key); + cellDOM.removeAttribute('data-temporary-table-cell-lexical-key'); + if (cellSpan) { + cellValues.delete(key); + for (let i = 0; i < cellSpan.colSpan; i++) { + knownColumns.add(i + cellSpan.startColumn); + } + } + } + } + + // Compute the colgroup and columns in the export + const colGroup = tableElement.querySelector(':scope > colgroup'); + if (colGroup) { + // Only include the
+ + + + + + + + + + + + + + +
+

0

+
+

1

+
+

2

+
+

3

+
+ `, + ); + }); + }); + + test('TableNode.exportDOM() with partial table selection', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const tableNode = $createTableNodeWithDimensions( + 2, + 2, + ).setColWidths([100, 200]); + tableNode + .getAllTextNodes() + .forEach((node, i) => node.setTextContent(String(i))); + $getRoot().append(tableNode); + const tableSelection = $createTableSelection(); + tableSelection.tableKey = tableNode.getKey(); + const cells = $dfs(tableNode).flatMap(({node}) => + $isTableCellNode(node) ? [node] : [], + ); + // second column + tableSelection.anchor.set(cells[1].getKey(), 0, 'element'); + tableSelection.focus.set(cells[3].getKey(), 0, 'element'); + expectHtmlToBeEqual( + $generateHtmlFromNodes(editor, tableSelection), + html` + + + + + + + + + + +
+

1

+
+

3

+
+ `, + ); + }); + }); + test('Copy table from an external source', async () => { const {editor} = testEnv; From b9261c506af84c52e600c0f2cb0aa3a842b0f8a5 Mon Sep 17 00:00:00 2001 From: Sherry Date: Mon, 2 Dec 2024 17:56:40 +0800 Subject: [PATCH 125/133] [Documentation Update] Add release protocol to maintainers-guide.md (#6895) --- packages/lexical-website/docs/maintainers-guide.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/lexical-website/docs/maintainers-guide.md b/packages/lexical-website/docs/maintainers-guide.md index 4313e7f1cc7..a7aacf1cd63 100644 --- a/packages/lexical-website/docs/maintainers-guide.md +++ b/packages/lexical-website/docs/maintainers-guide.md @@ -287,3 +287,9 @@ from main in step 4). 4. After PR is merged to main, publish to NPM with the Github Actions "Publish to NPM" workflow (`pre-release.yml`) 5. Create a GitHub release from the tag created in step 1, manually editing the release notes 6. Announce the release in #announcements on Discord + +## Release Protocol + +1. All PRs with breaking changes must have `[Breaking Change]` in the PR's title with documentation of what followup actions consumers of the lexical library need to be aware of. +2. Monthly releases happen on the last week of the month, with a minor increment (eg. v0.20+1.0). +3. Anything in between will be a patch increment (eg. 0.20.0+1), unless there is a breaking change. From c1d8a36c839e9fc6af8c0c399b23b713c641c667 Mon Sep 17 00:00:00 2001 From: Sherry Date: Mon, 2 Dec 2024 23:08:00 +0800 Subject: [PATCH 126/133] v0.21.0 (#6896) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 6 + examples/react-plain-text/package.json | 6 +- examples/react-rich-collab/package.json | 8 +- examples/react-rich/package.json | 6 +- examples/react-table/package.json | 6 +- examples/vanilla-js-plugin/package.json | 12 +- examples/vanilla-js/package.json | 12 +- package-lock.json | 442 +++++++++--------- package.json | 2 +- packages/lexical-clipboard/package.json | 12 +- packages/lexical-code/package.json | 6 +- packages/lexical-devtools-core/package.json | 14 +- packages/lexical-devtools/package.json | 6 +- packages/lexical-dragon/package.json | 4 +- packages/lexical-eslint-plugin/package.json | 2 +- packages/lexical-file/package.json | 4 +- packages/lexical-hashtag/package.json | 6 +- packages/lexical-headless/package.json | 4 +- packages/lexical-history/package.json | 6 +- packages/lexical-html/package.json | 8 +- packages/lexical-link/package.json | 6 +- packages/lexical-list/package.json | 6 +- packages/lexical-mark/package.json | 6 +- packages/lexical-markdown/package.json | 16 +- packages/lexical-offset/package.json | 4 +- packages/lexical-overflow/package.json | 4 +- packages/lexical-plain-text/package.json | 10 +- packages/lexical-playground/package.json | 32 +- packages/lexical-react/package.json | 40 +- packages/lexical-rich-text/package.json | 10 +- packages/lexical-selection/package.json | 4 +- packages/lexical-table/package.json | 8 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 8 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 8 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- 41 files changed, 391 insertions(+), 385 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a945980bb..3eee8677fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.21.0 (2024-12-01) + +- Fix selected table colors (#6892) Gerard Rovira +- v0.20.2 (#6891) Sherry +- v0.20.2 Lexical GitHub Actions Bot + ## v0.20.2 (2024-11-30) - lexicallexical-playground Bug Fix Allow setEditorState to work correctly inside of an update (#6876) Bob Ippolito diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index 26355282515..a1c8041a371 100644 --- a/examples/react-plain-text/package.json +++ b/examples/react-plain-text/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-plain-text-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.2", - "lexical": "0.20.2", + "@lexical/react": "0.21.0", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index c6b9d221594..d8726826edf 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-collab-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -12,9 +12,9 @@ "server:webrtc": "cross-env HOST=localhost PORT=1235 npx y-webrtc" }, "dependencies": { - "@lexical/react": "0.20.2", - "@lexical/yjs": "0.20.2", - "lexical": "0.20.2", + "@lexical/react": "0.21.0", + "@lexical/yjs": "0.21.0", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0", "y-webrtc": "^10.3.0", diff --git a/examples/react-rich/package.json b/examples/react-rich/package.json index 6986a9cb25f..0529b88d472 100644 --- a/examples/react-rich/package.json +++ b/examples/react-rich/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.2", - "lexical": "0.20.2", + "@lexical/react": "0.21.0", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-table/package.json b/examples/react-table/package.json index 3de430adb67..94aa0b6a434 100644 --- a/examples/react-table/package.json +++ b/examples/react-table/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-table-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.20.2", - "lexical": "0.20.2", + "@lexical/react": "0.21.0", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vanilla-js-plugin/package.json b/examples/vanilla-js-plugin/package.json index 7e5651a5b96..0cd7c243361 100644 --- a/examples/vanilla-js-plugin/package.json +++ b/examples/vanilla-js-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-plugin-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/dragon": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/utils": "0.21.0", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.20.2" + "lexical": "0.21.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index a9b693efb47..37bc8356971 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-example", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/dragon": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index 2bba3934e6e..e5387c0ffdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.20.2", + "version": "0.21.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "workspaces": [ "packages/*" @@ -38926,28 +38926,28 @@ } }, "packages/lexical": { - "version": "0.20.2", + "version": "0.21.0", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0", "prismjs": "^1.27.0" }, "devDependencies": { @@ -38956,7 +38956,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.20.2", + "version": "0.21.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -38973,12 +38973,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.2", + "@lexical/devtools-core": "0.21.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.2", + "lexical": "0.21.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -38986,15 +38986,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "react": ">=17.x", @@ -39003,15 +39003,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -39022,136 +39022,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/code": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/code": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-playground": { - "version": "0.20.2", + "version": "0.21.0", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/file": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/react": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/file": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/react": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", "katex": "^0.16.10", - "lexical": "0.20.2", + "lexical": "0.21.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -39175,28 +39175,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/devtools-core": "0.20.2", - "@lexical/dragon": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/markdown": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "@lexical/yjs": "0.20.2", - "lexical": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/devtools-core": "0.21.0", + "@lexical/dragon": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/markdown": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "@lexical/yjs": "0.21.0", + "lexical": "0.21.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -39206,55 +39206,55 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "lexical": "0.20.2" + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "lexical": "0.21.0" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.20.2", + "version": "0.21.0", "dependencies": { "@docusaurus/core": "3.6.0", "@docusaurus/faster": "3.6.0", @@ -39284,12 +39284,12 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "@lexical/offset": "0.20.2", - "@lexical/selection": "0.20.2", - "lexical": "0.20.2" + "@lexical/offset": "0.21.0", + "@lexical/selection": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -39322,10 +39322,10 @@ } }, "packages/shared": { - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } }, @@ -43955,19 +43955,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.20.2", + "@lexical/utils": "0.21.0", "@types/prismjs": "^1.26.0", - "lexical": "0.20.2", + "lexical": "0.21.0", "prismjs": "^1.27.0" } }, @@ -43979,7 +43979,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.20.2", + "@lexical/devtools-core": "0.21.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -43988,7 +43988,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.20.2", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -44000,18 +44000,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/eslint-plugin": { @@ -44023,152 +44023,152 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/code": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/devtools-core": "0.20.2", - "@lexical/dragon": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/markdown": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "@lexical/yjs": "0.20.2", - "lexical": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/devtools-core": "0.21.0", + "@lexical/dragon": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/markdown": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "@lexical/yjs": "0.21.0", + "lexical": "0.21.0", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/clipboard": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "lexical": "0.20.2" + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "lexical": "0.21.0" } }, "@lexical/website": { @@ -44201,9 +44201,9 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.20.2", - "@lexical/selection": "0.20.2", - "lexical": "0.20.2" + "@lexical/offset": "0.21.0", + "@lexical/selection": "0.21.0", + "lexical": "0.21.0" } }, "@mdx-js/mdx": { @@ -56244,26 +56244,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/file": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/react": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/file": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/react": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@types/lodash-es": "^4.14.182", "@vitejs/plugin-react": "^4.2.1", "katex": "^0.16.10", - "lexical": "0.20.2", + "lexical": "0.21.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -62551,7 +62551,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.20.2" + "lexical": "0.21.0" } }, "shebang-command": { diff --git a/package.json b/package.json index c7405f91d61..ba3ea744db2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index a48073ab1cd..e6346978107 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index a34c9c4b2a4..c6d27e70d3c 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index 96399206cb0..a195002ad88 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/html": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index ca173691239..61eb55296d6 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -2,7 +2,7 @@ "name": "@lexical/devtools", "description": "Lexical DevTools browser extension", "private": true, - "version": "0.20.2", + "version": "0.21.0", "type": "module", "scripts": { "dev": "wxt", @@ -41,12 +41,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.20.2", + "@lexical/devtools-core": "0.21.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.20.2", + "lexical": "0.21.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index 4ef705819d3..439d3678b97 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index 0358c0a7456..d3191b8a9c1 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.20.2", + "version": "0.21.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index ed299c5722c..614ee42ed8f 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index a2ce262bf58..4b7fbbf523b 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index 6a2b08f43f3..e50aac70eff 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 308bd7b957e..644a935da80 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index 67d18e3c3d0..8f02327cbda 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 0734cc586b7..95b1797099a 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index 82ed0595e08..0d179c2e933 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 7baa740f9d7..25c4a02d3d7 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index d377f60cbea..3876ae9137d 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/code": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index ed9e7391244..9bdee1780be 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index 5b00f98fd05..e61f16ccb92 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index 14e3877491e..4d63568b927 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,7 +7,7 @@ "plain-text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index ff91e2629fb..cd7ced6303f 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.20.2", + "version": "0.21.0", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/file": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/react": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/file": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/react": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", "katex": "^0.16.10", - "lexical": "0.20.2", + "lexical": "0.21.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index 5ab8da645a7..79482eb0db9 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/code": "0.20.2", - "@lexical/devtools-core": "0.20.2", - "@lexical/dragon": "0.20.2", - "@lexical/hashtag": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/link": "0.20.2", - "@lexical/list": "0.20.2", - "@lexical/mark": "0.20.2", - "@lexical/markdown": "0.20.2", - "@lexical/overflow": "0.20.2", - "@lexical/plain-text": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "@lexical/text": "0.20.2", - "@lexical/utils": "0.20.2", - "@lexical/yjs": "0.20.2", - "lexical": "0.20.2", + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/devtools-core": "0.21.0", + "@lexical/dragon": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/markdown": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "@lexical/yjs": "0.21.0", + "lexical": "0.21.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index 50da7d934d5..aead90d4159 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,7 +7,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index ef2f7e0cd85..368cce030b4 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index 537ca81e74b..c112513f792 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/clipboard": "0.20.2", - "@lexical/utils": "0.20.2", - "lexical": "0.20.2" + "@lexical/clipboard": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 2d1b2a00aa6..28f28996d11 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 4180a845c97..1119cc3c614 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.20.2", - "@lexical/selection": "0.20.2", - "@lexical/table": "0.20.2", - "lexical": "0.20.2" + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index 0009252a3ae..72a81071855 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.20.2", + "version": "0.21.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index 709f9205686..c0e59c45cbc 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,13 +11,13 @@ "crdt" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.20.2", - "@lexical/selection": "0.20.2", - "lexical": "0.20.2" + "@lexical/offset": "0.21.0", + "@lexical/selection": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index 8dfa8f054c8..4d1dfb2f096 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index db57f14b276..573101e318f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.20.2", + "version": "0.21.0", "dependencies": { - "lexical": "0.20.2" + "lexical": "0.21.0" }, "repository": { "type": "git", diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index d8c77bb8ae1..6f2e97d445a 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,7 +1,7 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.20.2", + "version": "0.21.0", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/react": "0.21.0", + "@lexical/utils": "0.21.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.20.2", + "lexical": "0.21.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index 9fc71139745..2a7b23e34da 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "lexical-esm-nextjs", - "version": "0.20.2", + "version": "0.21.0", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.20.2", - "@lexical/react": "0.20.2", - "lexical": "0.20.2", + "@lexical/plain-text": "0.21.0", + "@lexical/react": "0.21.0", + "lexical": "0.21.0", "next": "^14.2.1", "react": "^18", "react-dom": "^18" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json index 3a5e771437b..79ae305e4b2 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json @@ -1,6 +1,6 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.20.2", + "version": "0.21.0", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.20.2", - "@lexical/history": "0.20.2", - "@lexical/rich-text": "0.20.2", - "@lexical/utils": "0.20.2", + "@lexical/dragon": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/utils": "0.21.0", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.20.2", + "lexical": "0.21.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", From 71454c3aaab32c21aed71d57b529bb2c94341ea8 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 2 Dec 2024 14:46:11 -0800 Subject: [PATCH 127/133] [lexical-link] Bug Fix: Preserve the start/end of the selection for non-text points when creating a LinkNode (#6883) --- .../__tests__/unit/LexicalLinkNode.test.ts | 143 +++++++++--- packages/lexical-link/src/index.ts | 207 +++++++++++------- 2 files changed, 241 insertions(+), 109 deletions(-) diff --git a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts index 94582207ffc..a4c49cb6dfd 100644 --- a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts +++ b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts @@ -15,14 +15,19 @@ import { } from '@lexical/link'; import {$createMarkNode, $isMarkNode} from '@lexical/mark'; import { + $createLineBreakNode, $createParagraphNode, $createTextNode, $getRoot, + $getSelection, + $isLineBreakNode, + $isRangeSelection, + $isTextNode, $selectAll, ParagraphNode, + RangeSelection, SerializedParagraphNode, - TextNode, -} from 'lexical/src'; +} from 'lexical'; import {initializeUnitTest} from 'lexical/src/__tests__/utils'; const editorConfig = Object.freeze({ @@ -47,20 +52,20 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('/'); + const linkNode = $createLinkNode('/'); expect(linkNode.__type).toBe('link'); expect(linkNode.__url).toBe('/'); }); - expect(() => new LinkNode('')).toThrow(); + expect(() => $createLinkNode('')).toThrow(); }); test('LineBreakNode.clone()', async () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('/'); + const linkNode = $createLinkNode('/'); const linkNodeClone = LinkNode.clone(linkNode); @@ -73,7 +78,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); expect(linkNode.getURL()).toBe('https://example.com/foo'); }); @@ -83,7 +88,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); expect(linkNode.getURL()).toBe('https://example.com/foo'); @@ -97,7 +102,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { target: '_blank', }); @@ -109,7 +114,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { target: '_blank', }); @@ -125,7 +130,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener noreferrer', target: '_blank', }); @@ -138,7 +143,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener', target: '_blank', }); @@ -155,7 +160,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { title: 'Hello world', }); @@ -167,7 +172,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { title: 'Hello world', }); @@ -183,7 +188,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); expect(linkNode.createDOM(editorConfig).outerHTML).toBe( '', @@ -201,7 +206,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener noreferrer', target: '_blank', title: 'Hello world', @@ -226,7 +231,7 @@ describe('LexicalLinkNode tests', () => { await editor.update(() => { // eslint-disable-next-line no-script-url - const linkNode = new LinkNode('javascript:alert(0)'); + const linkNode = $createLinkNode('javascript:alert(0)'); expect(linkNode.createDOM(editorConfig).outerHTML).toBe( '', ); @@ -237,7 +242,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); const domElement = linkNode.createDOM(editorConfig); @@ -245,7 +250,7 @@ describe('LexicalLinkNode tests', () => { '', ); - const newLinkNode = new LinkNode('https://example.com/bar'); + const newLinkNode = $createLinkNode('https://example.com/bar'); const result = newLinkNode.updateDOM( linkNode, domElement, @@ -263,7 +268,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener noreferrer', target: '_blank', title: 'Hello world', @@ -275,7 +280,7 @@ describe('LexicalLinkNode tests', () => { '', ); - const newLinkNode = new LinkNode('https://example.com/bar', { + const newLinkNode = $createLinkNode('https://example.com/bar', { rel: 'noopener', target: '_self', title: 'World hello', @@ -297,7 +302,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener noreferrer', target: '_blank', title: 'Hello world', @@ -309,7 +314,7 @@ describe('LexicalLinkNode tests', () => { '', ); - const newLinkNode = new LinkNode('https://example.com/bar'); + const newLinkNode = $createLinkNode('https://example.com/bar'); const result = newLinkNode.updateDOM( linkNode, domElement, @@ -327,7 +332,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); expect(linkNode.canInsertTextBefore()).toBe(false); }); @@ -337,7 +342,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); expect(linkNode.canInsertTextAfter()).toBe(false); }); @@ -347,7 +352,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo'); + const linkNode = $createLinkNode('https://example.com/foo'); const createdLinkNode = $createLinkNode('https://example.com/foo'); @@ -362,7 +367,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode('https://example.com/foo', { + const linkNode = $createLinkNode('https://example.com/foo', { rel: 'noopener noreferrer', target: '_blank', title: 'Hello world', @@ -388,7 +393,7 @@ describe('LexicalLinkNode tests', () => { const {editor} = testEnv; await editor.update(() => { - const linkNode = new LinkNode(''); + const linkNode = $createLinkNode(''); expect($isLinkNode(linkNode)).toBe(true); }); @@ -397,14 +402,27 @@ describe('LexicalLinkNode tests', () => { test('$toggleLink applies the title attribute when creating', async () => { const {editor} = testEnv; await editor.update(() => { - const p = new ParagraphNode(); - p.append(new TextNode('Some text')); + const p = $createParagraphNode(); + const textNode = $createTextNode('Some text'); + p.append(textNode); $getRoot().append(p); - }); - - await editor.update(() => { $selectAll(); $toggleLink('https://lexical.dev/', {title: 'Lexical Website'}); + const linkNode = p.getFirstChild() as LinkNode; + expect($isLinkNode(linkNode)).toBe(true); + expect(linkNode.getTitle()).toBe('Lexical Website'); + const selection = $getSelection() as RangeSelection; + expect($isRangeSelection(selection)).toBe(true); + expect(selection.anchor).toMatchObject({ + key: textNode.getKey(), + offset: 0, + type: 'text', + }); + expect(selection.focus).toMatchObject({ + key: textNode.getKey(), + offset: textNode.getTextContentSize(), + type: 'text', + }); }); const paragraph = editor!.getEditorState().toJSON().root @@ -442,6 +460,7 @@ describe('LexicalLinkNode tests', () => { expect(textNode.getTextContent()).toBe('some '); // Check link node and its nested structure + expect($isLinkNode(linkNode)).toBe(true); if ($isLinkNode(linkNode)) { expect(linkNode.getURL()).toBe('https://example.com/foo'); expect(linkNode.getRel()).toBe('noreferrer'); @@ -470,6 +489,7 @@ describe('LexicalLinkNode tests', () => { expect(textNode.getTextContent()).toBe('some '); // Check mark node is preserved and moved up to paragraph level + expect($isMarkNode(markNode)).toBe(true); if ($isMarkNode(markNode)) { expect(markNode.getType()).toBe('mark'); expect(markNode.getIDs()).toEqual(['knetk']); @@ -477,5 +497,64 @@ describe('LexicalLinkNode tests', () => { } }); }); + + test('$toggleLink adds link with embedded LineBreakNode', async () => { + const {editor} = testEnv; + await editor.update(() => { + const paragraph = $createParagraphNode(); + const precedingText = $createTextNode('some '); // space after + const textNode = $createTextNode('text'); + paragraph.append(precedingText, textNode, $createLineBreakNode()); + $getRoot().clear().append(paragraph); + paragraph.select(1); + $toggleLink('https://example.com/foo', { + rel: 'noreferrer', + }); + }); + + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const [precedingText, linkNode] = paragraph.getChildren(); + + // Check first text node + expect(precedingText.getTextContent()).toBe('some '); + + // Check link node and its nested structure + expect($isLinkNode(linkNode)).toBe(true); + if ($isLinkNode(linkNode)) { + expect(linkNode.getURL()).toBe('https://example.com/foo'); + expect(linkNode.getRel()).toBe('noreferrer'); + expect( + linkNode.getChildren().map((node) => node.getTextContent()), + ).toEqual(['text', '\n']); + expect($getSelection()).toMatchObject({ + anchor: { + key: linkNode.getFirstChildOrThrow().getKey(), + offset: 0, + type: 'text', + }, + focus: {key: linkNode.getKey(), offset: 2, type: 'element'}, + }); + } + }); + + await editor.update(() => { + $selectAll(); + $toggleLink(null); + }); + + // Verify structure after link removal + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const children = paragraph.getChildren(); + expect(children.map((node) => node.getTextContent())).toEqual([ + 'some text', + '\n', + ]); + const [textNode, lineBreakNode] = children; + expect($isTextNode(textNode)).toBe(true); + expect($isLineBreakNode(lineBreakNode)).toBe(true); + }); + }); }); }); diff --git a/packages/lexical-link/src/index.ts b/packages/lexical-link/src/index.ts index 1ddb4dcec47..b2cdaefc89c 100644 --- a/packages/lexical-link/src/index.ts +++ b/packages/lexical-link/src/index.ts @@ -14,6 +14,7 @@ import type { LexicalCommand, LexicalNode, NodeKey, + Point, RangeSelection, SerializedElementNode, } from 'lexical'; @@ -28,10 +29,13 @@ import { $getSelection, $isElementNode, $isRangeSelection, + $normalizeSelection__EXPERIMENTAL, + $setSelection, createCommand, ElementNode, Spread, } from 'lexical'; +import invariant from 'shared/invariant'; export type LinkAttributes = { rel?: null | string; @@ -477,6 +481,66 @@ export const TOGGLE_LINK_COMMAND: LexicalCommand< string | ({url: string} & LinkAttributes) | null > = createCommand('TOGGLE_LINK_COMMAND'); +function $getPointNode(point: Point, offset: number): LexicalNode | null { + if (point.type === 'element') { + const node = point.getNode(); + invariant( + $isElementNode(node), + '$getPointNode: element point is not an ElementNode', + ); + const childNode = node.getChildren()[point.offset + offset]; + return childNode || null; + } + return null; +} + +/** + * Preserve the logical start/end of a RangeSelection in situations where + * the point is an element that may be reparented in the callback. + * + * @param $fn The function to run + * @returns The result of the callback + */ +function $withSelectedNodes($fn: () => T): T { + const initialSelection = $getSelection(); + if (!$isRangeSelection(initialSelection)) { + return $fn(); + } + const normalized = $normalizeSelection__EXPERIMENTAL(initialSelection); + const isBackwards = normalized.isBackward(); + const anchorNode = $getPointNode(normalized.anchor, isBackwards ? -1 : 0); + const focusNode = $getPointNode(normalized.focus, isBackwards ? 0 : -1); + const rval = $fn(); + if (anchorNode || focusNode) { + const updatedSelection = $getSelection(); + if ($isRangeSelection(updatedSelection)) { + const finalSelection = updatedSelection.clone(); + if (anchorNode) { + const anchorParent = anchorNode.getParent(); + if (anchorParent) { + finalSelection.anchor.set( + anchorParent.getKey(), + anchorNode.getIndexWithinParent() + (isBackwards ? 1 : 0), + 'element', + ); + } + } + if (focusNode) { + const focusParent = focusNode.getParent(); + if (focusParent) { + finalSelection.focus.set( + focusParent.getKey(), + focusNode.getIndexWithinParent() + (isBackwards ? 0 : 1), + 'element', + ); + } + } + $setSelection($normalizeSelection__EXPERIMENTAL(finalSelection)); + } + } + return rval; +} + /** * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null, * but saves any children and brings them up to the parent node. @@ -515,93 +579,82 @@ export function $toggleLink( parentLink.remove(); } }); - } else { - // Add or merge LinkNodes - if (nodes.length === 1) { - const firstNode = nodes[0]; - // if the first node is a LinkNode or if its - // parent is a LinkNode, we update the URL, target and rel. - const linkNode = $getAncestor(firstNode, $isLinkNode); - if (linkNode !== null) { - linkNode.setURL(url); - if (target !== undefined) { - linkNode.setTarget(target); - } - if (rel !== null) { - linkNode.setRel(rel); - } - if (title !== undefined) { - linkNode.setTitle(title); - } - return; - } + return; + } + const updatedNodes = new Set(); + const updateLinkNode = (linkNode: LinkNode) => { + if (updatedNodes.has(linkNode.getKey())) { + return; + } + updatedNodes.add(linkNode.getKey()); + linkNode.setURL(url); + if (target !== undefined) { + linkNode.setTarget(target); + } + if (rel !== undefined) { + linkNode.setRel(rel); } + if (title !== undefined) { + linkNode.setTitle(title); + } + }; + // Add or merge LinkNodes + if (nodes.length === 1) { + const firstNode = nodes[0]; + // if the first node is a LinkNode or if its + // parent is a LinkNode, we update the URL, target and rel. + const linkNode = $getAncestor(firstNode, $isLinkNode); + if (linkNode !== null) { + return updateLinkNode(linkNode); + } + } - let prevParent: ElementNode | LinkNode | null = null; + $withSelectedNodes(() => { let linkNode: LinkNode | null = null; - - nodes.forEach((node) => { - const parent = node.getParent(); - - if ( - parent === linkNode || - parent === null || - ($isElementNode(node) && !node.isInline()) - ) { - return; + for (const node of nodes) { + if (!node.isAttached()) { + continue; } - - if ($isLinkNode(parent)) { - linkNode = parent; - parent.setURL(url); - if (target !== undefined) { - parent.setTarget(target); - } - if (rel !== null) { - linkNode.setRel(rel); - } - if (title !== undefined) { - linkNode.setTitle(title); - } - return; + const parentLinkNode = $getAncestor(node, $isLinkNode); + if (parentLinkNode) { + updateLinkNode(parentLinkNode); + continue; } - - if (!parent.is(prevParent)) { - prevParent = parent; - linkNode = $createLinkNode(url, {rel, target, title}); - - if ($isLinkNode(parent)) { - if (node.getPreviousSibling() === null) { - parent.insertBefore(linkNode); - } else { - parent.insertAfter(linkNode); - } - } else { - node.insertBefore(linkNode); - } - } - - if ($isLinkNode(node)) { - if (node.is(linkNode)) { - return; + if ($isElementNode(node)) { + if (!node.isInline()) { + // Ignore block nodes, if there are any children we will see them + // later and wrap in a new LinkNode + continue; } - if (linkNode !== null) { - const children = node.getChildren(); - - for (let i = 0; i < children.length; i++) { - linkNode.append(children[i]); + if ($isLinkNode(node)) { + // If it's not an autolink node and we don't already have a LinkNode + // in this block then we can update it and re-use it + if ( + !$isAutoLinkNode(node) && + (linkNode === null || !linkNode.getParentOrThrow().isParentOf(node)) + ) { + updateLinkNode(node); + linkNode = node; + continue; + } + // Unwrap LinkNode, we already have one or it's an AutoLinkNode + for (const child of node.getChildren()) { + node.insertBefore(child); } + node.remove(); + continue; } - - node.remove(); - return; } - - if (linkNode !== null) { - linkNode.append(node); + const prevLinkNode = node.getPreviousSibling(); + if ($isLinkNode(prevLinkNode) && prevLinkNode.is(linkNode)) { + prevLinkNode.append(node); + continue; } - }); - } + linkNode = $createLinkNode(url, {rel, target, title}); + node.insertAfter(linkNode); + linkNode.append(node); + } + }); } /** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */ export const toggleLink = $toggleLink; From a9dfc9359c82a4105dd30c24adbb885e6b97ae25 Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Tue, 3 Dec 2024 05:30:36 +0100 Subject: [PATCH 128/133] [lexical-website][lexical-react] Documentation Update: documentation for LexicalTreeView plugin (#6898) --- .../lexical-react/src/LexicalTreeView.tsx | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/lexical-react/src/LexicalTreeView.tsx b/packages/lexical-react/src/LexicalTreeView.tsx index db536de589b..1b7057b27ef 100644 --- a/packages/lexical-react/src/LexicalTreeView.tsx +++ b/packages/lexical-react/src/LexicalTreeView.tsx @@ -18,13 +18,31 @@ import {mergeRegister} from '@lexical/utils'; import * as React from 'react'; import {useEffect, useState} from 'react'; +/** + * TreeView is a React component that provides a visual representation of + * the Lexical editor's state and enables debugging features like time travel + * and custom tree node rendering. + * + * @param {Object} props - The properties passed to the TreeView component. + * @param {LexicalEditor} props.editor - The Lexical editor instance to be visualized and debugged. + * @param {string} [props.treeTypeButtonClassName] - Custom class name for the tree type toggle button. + * @param {string} [props.timeTravelButtonClassName] - Custom class name for the time travel toggle button. + * @param {string} [props.timeTravelPanelButtonClassName] - Custom class name for buttons inside the time travel panel. + * @param {string} [props.timeTravelPanelClassName] - Custom class name for the overall time travel panel container. + * @param {string} [props.timeTravelPanelSliderClassName] - Custom class name for the time travel slider in the panel. + * @param {string} [props.viewClassName] - Custom class name for the tree view container. + * @param {CustomPrintNodeFn} [props.customPrintNode] - A function for customizing the display of nodes in the tree. + * + * @returns {JSX.Element} - A React element that visualizes the editor's state and supports debugging interactions. + */ + export function TreeView({ treeTypeButtonClassName, timeTravelButtonClassName, timeTravelPanelSliderClassName, timeTravelPanelButtonClassName, - viewClassName, timeTravelPanelClassName, + viewClassName, editor, customPrintNode, }: { @@ -38,6 +56,7 @@ export function TreeView({ customPrintNode?: CustomPrintNodeFn; }): JSX.Element { const treeElementRef = React.createRef(); + const [editorCurrentState, setEditorCurrentState] = useState( editor.getEditorState(), ); @@ -45,6 +64,7 @@ export function TreeView({ const commandsLog = useLexicalCommandsLog(editor); useEffect(() => { + // Registers listeners to update the tree view when the editor state changes return mergeRegister( editor.registerUpdateListener(({editorState}) => { setEditorCurrentState(editorState); @@ -59,16 +79,23 @@ export function TreeView({ const element = treeElementRef.current; if (element !== null) { - // @ts-ignore Internal field + // Assigns the editor instance to the tree view DOM element for internal tracking + // @ts-ignore Internal field used by Lexical element.__lexicalEditor = editor; return () => { - // @ts-ignore Internal field + // Cleans up the reference when the component is unmounted + // @ts-ignore Internal field used by Lexical element.__lexicalEditor = null; }; } }, [editor, treeElementRef]); + /** + * Handles toggling the readonly state of the editor. + * + * @param {boolean} isReadonly - Whether the editor should be set to readonly. + */ const handleEditorReadOnly = (isReadonly: boolean) => { const rootElement = editor.getRootElement(); if (rootElement == null) { @@ -90,6 +117,7 @@ export function TreeView({ editorState={editorCurrentState} setEditorState={(state) => editor.setEditorState(state)} generateContent={async function (exportDOM) { + // Generates the content for the tree view, allowing customization with exportDOM and customPrintNode return generateContent(editor, commandsLog, exportDOM, customPrintNode); }} ref={treeElementRef} From b7fa4cf673869dac0c2e0c1fe667e71e72ff6adb Mon Sep 17 00:00:00 2001 From: Maksim Horbachevsky Date: Tue, 3 Dec 2024 11:57:49 -0500 Subject: [PATCH 129/133] Warn about "display: flex" container for the editor (#6901) --- packages/lexical/src/LexicalEditor.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 02be308ba84..1a67571c1a0 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -1070,6 +1070,19 @@ export class LexicalEditor { if (classNames != null) { nextRootElement.classList.add(...classNames); } + if (__DEV__) { + const nextRootElementParent = nextRootElement.parentElement; + if ( + nextRootElementParent != null && + ['flex', 'inline-flex'].includes( + getComputedStyle(nextRootElementParent).display, + ) + ) { + console.warn( + `When using "display: flex" or "display: inline-flex" on an element containing content editable, Chrome may have unwanted focusing behavior when clicking outside of it. Consider wrapping the content editable within a non-flex element.`, + ); + } + } } else { // If content editable is unmounted we'll reset editor state back to original // (or pending) editor state since there will be no reconciliation From bccfeeece345a7bc00ac14ca2670cab3eb887f32 Mon Sep 17 00:00:00 2001 From: Oluwasanya Olaoluwa Date: Thu, 5 Dec 2024 08:22:47 +1100 Subject: [PATCH 130/133] [lexical-website] Fix: /docs/react/ "next" button links to itself (#6911) --- .../docs/react/create_plugin.md | 6 +----- packages/lexical-website/docs/react/faq.md | 15 ++++++--------- packages/lexical-website/docs/react/index.md | 8 ++------ packages/lexical-website/docs/react/plugins.md | 18 ++++++++---------- packages/lexical-website/sidebars.js | 11 ++++++----- 5 files changed, 23 insertions(+), 35 deletions(-) diff --git a/packages/lexical-website/docs/react/create_plugin.md b/packages/lexical-website/docs/react/create_plugin.md index 310d964672c..6ebf19263fe 100644 --- a/packages/lexical-website/docs/react/create_plugin.md +++ b/packages/lexical-website/docs/react/create_plugin.md @@ -1,7 +1,3 @@ ---- -sidebar_position: 2 ---- - # Creating a React Plugin In addition to using the Lexical React plugins offered by the core library, you can make your own plugins to extend or alter Lexical's functionality to suit your own use cases. @@ -18,7 +14,7 @@ If the Plugin introduces new nodes, they have to be registered in `initialConfig ```js const initialConfig = { - namespace: "MyEditor", + namespace: 'MyEditor', nodes: [MyLexicalNode], }; ``` diff --git a/packages/lexical-website/docs/react/faq.md b/packages/lexical-website/docs/react/faq.md index c98dce9be91..b63a2295e5a 100644 --- a/packages/lexical-website/docs/react/faq.md +++ b/packages/lexical-website/docs/react/faq.md @@ -1,6 +1,3 @@ ---- ---- - # React FAQ ## My app does not work in dev when using StrictMode, help!? @@ -14,18 +11,18 @@ conventions and guidelines. This is a great place to start: Some Lexical-specific concerns (which are consequences of React's concurrent and StrictMode semantics, not due to anything unusual in Lexical): -* In React 19, `useMemo` calls are cached across StrictMode re-renders, so +- In React 19, `useMemo` calls are cached across StrictMode re-renders, so only one editor will be used for both renders. If you have a `useEffect` call with side-effects (such as updating the document when a plug-in initializes), then you should first check to make sure that this effect has not already occurred (e.g. by checking the state of the document or undoing the change as a cleanup function returned by the effect) -* `LexicalComposer`'s initialConfig prop is only considered once during +- `LexicalComposer`'s initialConfig prop is only considered once during the first render (`useMemo` is used to create the `LexicalComposerContext` which includes the editor and theme) -* If you are using an `editorState` argument in the config when creating the +- If you are using an `editorState` argument in the config when creating the editor, it will only be called once when the editor is created. -* You should generally prefer to use hooks that return state such as +- You should generally prefer to use hooks that return state such as `useLexicalEditable` (`useLexicalSubscription` is a generalization of this style) rather than manually registering the listeners and expecting a particular sequence of triggers to be called, especially @@ -45,10 +42,10 @@ build of Lexical that the hook was imported from. The most common root causes of this issue are: -* You are trying to use `useLexicalComposerContext()` in a component that is +- You are trying to use `useLexicalComposerContext()` in a component that is not a child of the `LexicalComposer`. If you need to do that, you need to pass the context or editor up the tree with something like `EditorRefPlugin`. -* You have multiple builds of Lexical in your project. This could be because +- You have multiple builds of Lexical in your project. This could be because you have a dependency that has a direct dependency on some other version of Lexical (these packages should have Lexical as `peerDependencies`, but not all do), or because your project mixes import and require statements diff --git a/packages/lexical-website/docs/react/index.md b/packages/lexical-website/docs/react/index.md index b0150b10116..1b811e5b517 100644 --- a/packages/lexical-website/docs/react/index.md +++ b/packages/lexical-website/docs/react/index.md @@ -1,9 +1,5 @@ --- -id: "index" -title: "Lexical API" -sidebar_label: "Introduction" -sidebar_position: 0 -custom_edit_url: null +sidebar_label: 'Introduction' --- # Lexical + React @@ -13,7 +9,7 @@ To make it easier for React users to implement rich-text editors, Lexical expose - {`Getting Started Guide`} +{`Getting Started Guide`} ## Supported Versions diff --git a/packages/lexical-website/docs/react/plugins.md b/packages/lexical-website/docs/react/plugins.md index c40f8c22251..3f6d24cc9d5 100644 --- a/packages/lexical-website/docs/react/plugins.md +++ b/packages/lexical-website/docs/react/plugins.md @@ -1,7 +1,3 @@ ---- -sidebar_position: 1 ---- - # Lexical Plugins React-based plugins are using Lexical editor instance from `` context: @@ -29,7 +25,7 @@ const initialConfig = { ... - +; ``` > Note: Many plugins might require you to register the one or many Lexical nodes in order for the plugin to work. You can do this by passing a reference to the node to the `nodes` array in your initial editor configuration. @@ -184,7 +180,9 @@ In order to use `TableOfContentsPlugin`, you need to pass a callback function in ```jsx {(tableOfContentsArray) => { - return ; + return ( + + ); }} ``` @@ -195,8 +193,8 @@ Allows you to get a ref to the underlying editor instance outside of LexicalComp from a separate part of your application. ```jsx - const editorRef = useRef(null); - +const editorRef = useRef(null); +; ``` ### `LexicalSelectionAlwaysOnDisplay` @@ -204,5 +202,5 @@ from a separate part of your application. By default, browser text selection becomes invisible when clicking away from the editor. This plugin ensures the selection remains visible. ```jsx - -``` \ No newline at end of file + +``` diff --git a/packages/lexical-website/sidebars.js b/packages/lexical-website/sidebars.js index d1f61b1374a..523933de4c2 100644 --- a/packages/lexical-website/sidebars.js +++ b/packages/lexical-website/sidebars.js @@ -71,12 +71,13 @@ const sidebars = { type: 'category', }, { - items: [{dirName: 'react', type: 'autogenerated'}], + items: [ + 'react/index', + 'react/plugins', + 'react/create_plugin', + 'react/faq', + ], label: 'React', - link: { - id: 'react/index', - type: 'doc', - }, type: 'category', }, { From 97481c9d1035c8759f2b94897847e7d0e2b0c12a Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 6 Dec 2024 06:56:51 -0800 Subject: [PATCH 131/133] [lexical-table][lexical-utils][lexical-react]: Bug Fix: Enforce table integrity with transforms and move non-React plugin code to @lexical/table (#6914) --- .../__tests__/e2e/Tables.spec.mjs | 12 +- .../src/plugins/TablePlugin.tsx | 46 +-- .../lexical-react/src/LexicalTablePlugin.ts | 211 +----------- .../src/shared/useCharacterLimit.ts | 14 +- .../lexical-table/flow/LexicalTable.js.flow | 14 +- .../lexical-table/src/LexicalTableCellNode.ts | 2 +- .../lexical-table/src/LexicalTableNode.ts | 9 +- .../src/LexicalTablePluginHelpers.ts | 275 +++++++++++++++ .../lexical-table/src/LexicalTableRowNode.ts | 8 +- .../__tests__/unit/LexicalTableNode.test.tsx | 312 ++++++++++++++++++ packages/lexical-table/src/index.ts | 5 + .../lexical-utils/flow/LexicalUtils.js.flow | 11 + .../unit/descendantsMatching.test.tsx | 84 +++++ .../src/__tests__/unit/iterators.test.tsx | 216 ++++++++++++ .../unit/unwrapAndFilterDescendants.test.tsx | 101 ++++++ packages/lexical-utils/src/index.ts | 155 +++++++++ .../src/__tests__/unit/LexicalEditor.test.tsx | 4 +- 17 files changed, 1217 insertions(+), 262 deletions(-) create mode 100644 packages/lexical-table/src/LexicalTablePluginHelpers.ts create mode 100644 packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx create mode 100644 packages/lexical-utils/src/__tests__/unit/iterators.test.tsx create mode 100644 packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 4fd7ca25b30..1cf4e25a7b1 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -3737,7 +3737,9 @@ test.describe.parallel('Tables', () => { Hello world

-

+


+

{



+


+
+


+

; -export const INSERT_NEW_TABLE_COMMAND: LexicalCommand = - createCommand('INSERT_NEW_TABLE_COMMAND'); - export const CellContext = createContext({ cellEditorConfig: null, cellEditorPlugins: null, @@ -155,28 +143,16 @@ export function TablePlugin({ }): JSX.Element | null { const [editor] = useLexicalComposerContext(); const cellContext = useContext(CellContext); - useEffect(() => { - if (!editor.hasNodes([TableNode])) { - invariant(false, 'TablePlugin: TableNode is not registered on editor'); + if (!editor.hasNodes([TableNode, TableRowNode, TableCellNode])) { + invariant( + false, + 'TablePlugin: TableNode, TableRowNode, or TableCellNode is not registered on editor', + ); } - + }, [editor]); + useEffect(() => { cellContext.set(cellEditorConfig, children); - - return editor.registerCommand( - INSERT_NEW_TABLE_COMMAND, - ({columns, rows, includeHeaders}) => { - const tableNode = $createTableNodeWithDimensions( - Number(rows), - Number(columns), - includeHeaders, - ); - $insertNodes([tableNode]); - return true; - }, - COMMAND_PRIORITY_EDITOR, - ); - }, [cellContext, cellEditorConfig, children, editor]); - + }, [cellContext, cellEditorConfig, children]); return null; } diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts index a5c43d17c65..7d91b0b576c 100644 --- a/packages/lexical-react/src/LexicalTablePlugin.ts +++ b/packages/lexical-react/src/LexicalTablePlugin.ts @@ -6,43 +6,15 @@ * */ -import type { - HTMLTableElementWithWithTableSelectionState, - InsertTableCommandPayload, - TableObserver, -} from '@lexical/table'; -import type {NodeKey} from 'lexical'; - import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import { - $computeTableMap, - $computeTableMapSkipCellCheck, - $createTableCellNode, - $createTableNodeWithDimensions, - $getNodeTriplet, - $getTableAndElementByKey, - $isTableCellNode, - $isTableRowNode, - applyTableHandlers, - getTableElement, - INSERT_TABLE_COMMAND, + registerTableCellUnmergeTransform, + registerTablePlugin, + registerTableSelectionObserver, setScrollableTablesActive, TableCellNode, - TableNode, - TableRowNode, } from '@lexical/table'; -import { - $insertFirst, - $insertNodeToNearestRoot, - mergeRegister, -} from '@lexical/utils'; -import { - $createParagraphNode, - $isTextNode, - COMMAND_PRIORITY_EDITOR, -} from 'lexical'; import {useEffect} from 'react'; -import invariant from 'shared/invariant'; export interface TablePluginProps { /** @@ -82,181 +54,18 @@ export function TablePlugin({ setScrollableTablesActive(editor, hasHorizontalScroll); }, [editor, hasHorizontalScroll]); - useEffect(() => { - if (!editor.hasNodes([TableNode, TableCellNode, TableRowNode])) { - invariant( - false, - 'TablePlugin: TableNode, TableCellNode or TableRowNode not registered on editor', - ); - } - - return mergeRegister( - editor.registerCommand( - INSERT_TABLE_COMMAND, - ({columns, rows, includeHeaders}) => { - const tableNode = $createTableNodeWithDimensions( - Number(rows), - Number(columns), - includeHeaders, - ); - $insertNodeToNearestRoot(tableNode); - - const firstDescendant = tableNode.getFirstDescendant(); - if ($isTextNode(firstDescendant)) { - firstDescendant.select(); - } - - return true; - }, - COMMAND_PRIORITY_EDITOR, - ), - editor.registerNodeTransform(TableNode, (node) => { - const [gridMap] = $computeTableMapSkipCellCheck(node, null, null); - const maxRowLength = gridMap.reduce((curLength, row) => { - return Math.max(curLength, row.length); - }, 0); - const rowNodes = node.getChildren(); - for (let i = 0; i < gridMap.length; ++i) { - const rowNode = rowNodes[i]; - if (!rowNode) { - continue; - } - const rowLength = gridMap[i].reduce( - (acc, cell) => (cell ? 1 + acc : acc), - 0, - ); - if (rowLength === maxRowLength) { - continue; - } - for (let j = rowLength; j < maxRowLength; ++j) { - // TODO: inherit header state from another header or body - const newCell = $createTableCellNode(0); - newCell.append($createParagraphNode()); - (rowNode as TableRowNode).append(newCell); - } - } - }), - ); - }, [editor]); + useEffect(() => registerTablePlugin(editor), [editor]); - useEffect(() => { - const tableSelections = new Map< - NodeKey, - [TableObserver, HTMLTableElementWithWithTableSelectionState] - >(); - - const initializeTableNode = ( - tableNode: TableNode, - nodeKey: NodeKey, - dom: HTMLElement, - ) => { - const tableElement = getTableElement(tableNode, dom); - const tableSelection = applyTableHandlers( - tableNode, - tableElement, - editor, - hasTabHandler, - ); - tableSelections.set(nodeKey, [tableSelection, tableElement]); - }; - - const unregisterMutationListener = editor.registerMutationListener( - TableNode, - (nodeMutations) => { - editor.getEditorState().read( - () => { - for (const [nodeKey, mutation] of nodeMutations) { - const tableSelection = tableSelections.get(nodeKey); - if (mutation === 'created' || mutation === 'updated') { - const {tableNode, tableElement} = - $getTableAndElementByKey(nodeKey); - if (tableSelection === undefined) { - initializeTableNode(tableNode, nodeKey, tableElement); - } else if (tableElement !== tableSelection[1]) { - // The update created a new DOM node, destroy the existing TableObserver - tableSelection[0].removeListeners(); - tableSelections.delete(nodeKey); - initializeTableNode(tableNode, nodeKey, tableElement); - } - } else if (mutation === 'destroyed') { - if (tableSelection !== undefined) { - tableSelection[0].removeListeners(); - tableSelections.delete(nodeKey); - } - } - } - }, - {editor}, - ); - }, - {skipInitialization: false}, - ); - - return () => { - unregisterMutationListener(); - // Hook might be called multiple times so cleaning up tables listeners as well, - // as it'll be reinitialized during recurring call - for (const [, [tableSelection]] of tableSelections) { - tableSelection.removeListeners(); - } - }; - }, [editor, hasTabHandler]); + useEffect( + () => registerTableSelectionObserver(editor, hasTabHandler), + [editor, hasTabHandler], + ); // Unmerge cells when the feature isn't enabled useEffect(() => { - if (hasCellMerge) { - return; + if (!hasCellMerge) { + return registerTableCellUnmergeTransform(editor); } - return editor.registerNodeTransform(TableCellNode, (node) => { - if (node.getColSpan() > 1 || node.getRowSpan() > 1) { - // When we have rowSpan we have to map the entire Table to understand where the new Cells - // fit best; let's analyze all Cells at once to save us from further transform iterations - const [, , gridNode] = $getNodeTriplet(node); - const [gridMap] = $computeTableMap(gridNode, node, node); - // TODO this function expects Tables to be normalized. Look into this once it exists - const rowsCount = gridMap.length; - const columnsCount = gridMap[0].length; - let row = gridNode.getFirstChild(); - invariant( - $isTableRowNode(row), - 'Expected TableNode first child to be a RowNode', - ); - const unmerged = []; - for (let i = 0; i < rowsCount; i++) { - if (i !== 0) { - row = row.getNextSibling(); - invariant( - $isTableRowNode(row), - 'Expected TableNode first child to be a RowNode', - ); - } - let lastRowCell: null | TableCellNode = null; - for (let j = 0; j < columnsCount; j++) { - const cellMap = gridMap[i][j]; - const cell = cellMap.cell; - if (cellMap.startRow === i && cellMap.startColumn === j) { - lastRowCell = cell; - unmerged.push(cell); - } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) { - invariant( - $isTableCellNode(cell), - 'Expected TableNode cell to be a TableCellNode', - ); - const newCell = $createTableCellNode(cell.__headerState); - if (lastRowCell !== null) { - lastRowCell.insertAfter(newCell); - } else { - $insertFirst(row, newCell); - } - } - } - } - for (const cell of unmerged) { - cell.setColSpan(1); - cell.setRowSpan(1); - } - } - }); }, [editor, hasCellMerge]); // Remove cell background color when feature is disabled diff --git a/packages/lexical-react/src/shared/useCharacterLimit.ts b/packages/lexical-react/src/shared/useCharacterLimit.ts index 8e1e4f813c0..75d8e58040b 100644 --- a/packages/lexical-react/src/shared/useCharacterLimit.ts +++ b/packages/lexical-react/src/shared/useCharacterLimit.ts @@ -14,7 +14,7 @@ import { OverflowNode, } from '@lexical/overflow'; import {$rootTextContent} from '@lexical/text'; -import {$dfs, mergeRegister} from '@lexical/utils'; +import {$dfs, $unwrapNode, mergeRegister} from '@lexical/utils'; import { $getSelection, $isElementNode, @@ -254,18 +254,6 @@ function $wrapNode(node: LexicalNode): OverflowNode { return overflowNode; } -function $unwrapNode(node: OverflowNode): LexicalNode | null { - 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; -} - export function $mergePrevious(overflowNode: OverflowNode): void { const previousNode = overflowNode.getPreviousSibling(); diff --git a/packages/lexical-table/flow/LexicalTable.js.flow b/packages/lexical-table/flow/LexicalTable.js.flow index 2674a125f50..0d3af559ed3 100644 --- a/packages/lexical-table/flow/LexicalTable.js.flow +++ b/packages/lexical-table/flow/LexicalTable.js.flow @@ -75,7 +75,7 @@ declare export class TableCellNode extends ElementNode { canBeEmpty(): false; } declare export function $createTableCellNode( - headerState: TableCellHeaderState, + headerState?: TableCellHeaderState, colSpan?: number, width?: ?number, ): TableCellNode; @@ -350,4 +350,14 @@ export type InsertTableCommandPayload = $ReadOnly<{ includeHeaders?: InsertTableCommandPayloadHeaders; }>; -declare export var INSERT_TABLE_COMMAND: LexicalCommand; \ No newline at end of file +declare export var INSERT_TABLE_COMMAND: LexicalCommand; + +/** + * LexicalTablePluginHelpers + */ + +declare export function registerTableCellUnmergeTransform(editor: LexicalEditor): () => void; + +declare export function registerTablePlugin(editor: LexicalEditor): () => void; + +declare export function registerTableSelectionObserver(editor: LexicalEditor, hasTabHandler?: boolean): () => void; diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index 2b00b8dfb6e..795779c4990 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -366,7 +366,7 @@ export function $convertTableCellNodeElement( } export function $createTableCellNode( - headerState: TableCellHeaderState, + headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width?: number, ): TableCellNode { diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 4a4a2c970fa..636613346b3 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -6,9 +6,8 @@ * */ -import type {TableRowNode} from './LexicalTableRowNode'; - import { + $descendantsMatching, addClassNamesToElement, isHTMLElement, removeClassNamesFromElement, @@ -36,6 +35,7 @@ import invariant from 'shared/invariant'; import {PIXEL_VALUE_REG_EXP} from './constants'; import {$isTableCellNode, type TableCellNode} from './LexicalTableCellNode'; import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; +import {$isTableRowNode, type TableRowNode} from './LexicalTableRowNode'; import { $getNearestTableCellInTableFromDOMNode, getTable, @@ -498,7 +498,10 @@ export function $convertTableElement( tableNode.setColWidths(columns); } } - return {node: tableNode}; + return { + after: (children) => $descendantsMatching(children, $isTableRowNode), + node: tableNode, + }; } export function $createTableNode(): TableNode { diff --git a/packages/lexical-table/src/LexicalTablePluginHelpers.ts b/packages/lexical-table/src/LexicalTablePluginHelpers.ts new file mode 100644 index 00000000000..ae7ee4547e2 --- /dev/null +++ b/packages/lexical-table/src/LexicalTablePluginHelpers.ts @@ -0,0 +1,275 @@ +/** + * 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 { + $insertFirst, + $insertNodeToNearestRoot, + $unwrapAndFilterDescendants, + mergeRegister, +} from '@lexical/utils'; +import { + $createParagraphNode, + $isTextNode, + COMMAND_PRIORITY_EDITOR, + LexicalEditor, + NodeKey, +} from 'lexical'; +import invariant from 'shared/invariant'; + +import { + $createTableCellNode, + $isTableCellNode, + TableCellNode, +} from './LexicalTableCellNode'; +import { + INSERT_TABLE_COMMAND, + InsertTableCommandPayload, +} from './LexicalTableCommands'; +import {$isTableNode, TableNode} from './LexicalTableNode'; +import {$getTableAndElementByKey, TableObserver} from './LexicalTableObserver'; +import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode'; +import { + applyTableHandlers, + getTableElement, + HTMLTableElementWithWithTableSelectionState, +} from './LexicalTableSelectionHelpers'; +import { + $computeTableMap, + $computeTableMapSkipCellCheck, + $createTableNodeWithDimensions, + $getNodeTriplet, +} from './LexicalTableUtils'; + +function $insertTableCommandListener({ + rows, + columns, + includeHeaders, +}: InsertTableCommandPayload): boolean { + const tableNode = $createTableNodeWithDimensions( + Number(rows), + Number(columns), + includeHeaders, + ); + $insertNodeToNearestRoot(tableNode); + + const firstDescendant = tableNode.getFirstDescendant(); + if ($isTextNode(firstDescendant)) { + firstDescendant.select(); + } + + return true; +} + +function $tableCellTransform(node: TableCellNode) { + if (!$isTableRowNode(node.getParent())) { + // TableCellNode must be a child of TableRowNode. + node.remove(); + } else if (node.isEmpty()) { + // TableCellNode should never be empty + node.append($createParagraphNode()); + } +} + +function $tableRowTransform(node: TableRowNode) { + if (!$isTableNode(node.getParent())) { + // TableRowNode must be a child of TableNode. + // TODO: Future support of tbody/thead/tfoot may change this + node.remove(); + } else { + $unwrapAndFilterDescendants(node, $isTableCellNode); + } +} + +function $tableTransform(node: TableNode) { + // TableRowNode is the only valid child for TableNode + // TODO: Future support of tbody/thead/tfoot/caption may change this + $unwrapAndFilterDescendants(node, $isTableRowNode); + + const [gridMap] = $computeTableMapSkipCellCheck(node, null, null); + const maxRowLength = gridMap.reduce((curLength, row) => { + return Math.max(curLength, row.length); + }, 0); + const rowNodes = node.getChildren(); + for (let i = 0; i < gridMap.length; ++i) { + const rowNode = rowNodes[i]; + if (!rowNode) { + continue; + } + invariant( + $isTableRowNode(rowNode), + 'TablePlugin: Expecting all children of TableNode to be TableRowNode, found %s (type %s)', + rowNode.constructor.name, + rowNode.getType(), + ); + const rowLength = gridMap[i].reduce( + (acc, cell) => (cell ? 1 + acc : acc), + 0, + ); + if (rowLength === maxRowLength) { + continue; + } + for (let j = rowLength; j < maxRowLength; ++j) { + // TODO: inherit header state from another header or body + const newCell = $createTableCellNode(); + newCell.append($createParagraphNode()); + rowNode.append(newCell); + } + } +} + +/** + * Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1. + * This should only be registered when you do not want to support merged cells. + * + * @param editor The editor + * @returns An unregister callback + */ +export function registerTableCellUnmergeTransform( + editor: LexicalEditor, +): () => void { + return editor.registerNodeTransform(TableCellNode, (node) => { + if (node.getColSpan() > 1 || node.getRowSpan() > 1) { + // When we have rowSpan we have to map the entire Table to understand where the new Cells + // fit best; let's analyze all Cells at once to save us from further transform iterations + const [, , gridNode] = $getNodeTriplet(node); + const [gridMap] = $computeTableMap(gridNode, node, node); + // TODO this function expects Tables to be normalized. Look into this once it exists + const rowsCount = gridMap.length; + const columnsCount = gridMap[0].length; + let row = gridNode.getFirstChild(); + invariant( + $isTableRowNode(row), + 'Expected TableNode first child to be a RowNode', + ); + const unmerged = []; + for (let i = 0; i < rowsCount; i++) { + if (i !== 0) { + row = row.getNextSibling(); + invariant( + $isTableRowNode(row), + 'Expected TableNode first child to be a RowNode', + ); + } + let lastRowCell: null | TableCellNode = null; + for (let j = 0; j < columnsCount; j++) { + const cellMap = gridMap[i][j]; + const cell = cellMap.cell; + if (cellMap.startRow === i && cellMap.startColumn === j) { + lastRowCell = cell; + unmerged.push(cell); + } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) { + invariant( + $isTableCellNode(cell), + 'Expected TableNode cell to be a TableCellNode', + ); + const newCell = $createTableCellNode(cell.__headerState); + if (lastRowCell !== null) { + lastRowCell.insertAfter(newCell); + } else { + $insertFirst(row, newCell); + } + } + } + } + for (const cell of unmerged) { + cell.setColSpan(1); + cell.setRowSpan(1); + } + } + }); +} + +export function registerTableSelectionObserver( + editor: LexicalEditor, + hasTabHandler: boolean = true, +): () => void { + const tableSelections = new Map< + NodeKey, + [TableObserver, HTMLTableElementWithWithTableSelectionState] + >(); + + const initializeTableNode = ( + tableNode: TableNode, + nodeKey: NodeKey, + dom: HTMLElement, + ) => { + const tableElement = getTableElement(tableNode, dom); + const tableSelection = applyTableHandlers( + tableNode, + tableElement, + editor, + hasTabHandler, + ); + tableSelections.set(nodeKey, [tableSelection, tableElement]); + }; + + const unregisterMutationListener = editor.registerMutationListener( + TableNode, + (nodeMutations) => { + editor.getEditorState().read( + () => { + for (const [nodeKey, mutation] of nodeMutations) { + const tableSelection = tableSelections.get(nodeKey); + if (mutation === 'created' || mutation === 'updated') { + const {tableNode, tableElement} = + $getTableAndElementByKey(nodeKey); + if (tableSelection === undefined) { + initializeTableNode(tableNode, nodeKey, tableElement); + } else if (tableElement !== tableSelection[1]) { + // The update created a new DOM node, destroy the existing TableObserver + tableSelection[0].removeListeners(); + tableSelections.delete(nodeKey); + initializeTableNode(tableNode, nodeKey, tableElement); + } + } else if (mutation === 'destroyed') { + if (tableSelection !== undefined) { + tableSelection[0].removeListeners(); + tableSelections.delete(nodeKey); + } + } + } + }, + {editor}, + ); + }, + {skipInitialization: false}, + ); + + return () => { + unregisterMutationListener(); + // Hook might be called multiple times so cleaning up tables listeners as well, + // as it'll be reinitialized during recurring call + for (const [, [tableSelection]] of tableSelections) { + tableSelection.removeListeners(); + } + }; +} + +/** + * Register the INSERT_TABLE_COMMAND listener and the table integrity transforms. The + * table selection observer should be registered separately after this with + * {@link registerTableSelectionObserver}. + * + * @param editor The editor + * @returns An unregister callback + */ +export function registerTablePlugin(editor: LexicalEditor): () => void { + if (!editor.hasNodes([TableNode])) { + invariant(false, 'TablePlugin: TableNode is not registered on editor'); + } + return mergeRegister( + editor.registerCommand( + INSERT_TABLE_COMMAND, + $insertTableCommandListener, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerNodeTransform(TableNode, $tableTransform), + editor.registerNodeTransform(TableRowNode, $tableRowTransform), + editor.registerNodeTransform(TableCellNode, $tableCellTransform), + ); +} diff --git a/packages/lexical-table/src/LexicalTableRowNode.ts b/packages/lexical-table/src/LexicalTableRowNode.ts index fd8bcb8fa0a..9a7d5c99c88 100644 --- a/packages/lexical-table/src/LexicalTableRowNode.ts +++ b/packages/lexical-table/src/LexicalTableRowNode.ts @@ -8,7 +8,7 @@ import type {BaseSelection, Spread} from 'lexical'; -import {addClassNamesToElement} from '@lexical/utils'; +import {$descendantsMatching, addClassNamesToElement} from '@lexical/utils'; import { $applyNodeReplacement, DOMConversionMap, @@ -21,6 +21,7 @@ import { } from 'lexical'; import {PIXEL_VALUE_REG_EXP} from './constants'; +import {$isTableCellNode} from './LexicalTableCellNode'; export type SerializedTableRowNode = Spread< { @@ -124,7 +125,10 @@ export function $convertTableRowElement(domNode: Node): DOMConversionOutput { height = parseFloat(domNode_.style.height); } - return {node: $createTableRowNode(height)}; + return { + after: (children) => $descendantsMatching(children, $isTableCellNode), + node: $createTableRowNode(height), + }; } export function $createTableRowNode(height?: number): TableRowNode { diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 96ca3c7e426..38755a841e2 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -317,6 +317,318 @@ describe('LexicalTableNode tests', () => { ); }); + test('Copy table with caption/tbody/thead/tfoot from an external source', async () => { + const {editor} = testEnv; + + const dataTransfer = new DataTransferMock(); + dataTransfer.setData( + 'text/html', + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead + html` + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Council budget (in £) 2018 +
+ Items + + Expenditure +
+ Donuts + + 3,000 +
+ Stationery + + 18,000 +
+ Totals + + 21,000 +
+ `, + ); + await editor.update(() => { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection), + 'isRangeSelection(selection)', + ); + $insertDataTransferForRichText(dataTransfer, selection, editor); + }); + // Here we are testing the createDOM, not the exportDOM, so the tbody is not there + expectTableHtmlToBeEqual( + testEnv.innerHTML, + html` + + + + + + + + + + + + + + + + + + + + + +
+

+ Items +

+
+

+ Expenditure +

+
+

+ Donuts +

+
+

+ 3,000 +

+
+

+ Stationery +

+
+

+ 18,000 +

+
+

+ Totals +

+
+

+ 21,000 +

+
+ `, + ); + }); + + test('Copy table with caption from an external source', async () => { + const {editor} = testEnv; + + const dataTransfer = new DataTransferMock(); + dataTransfer.setData( + 'text/html', + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption + html` + + + + + + + + + + + + + + + + + + + + + + + + + +
+ He-Man and Skeletor facts +
+ He-Man + + Skeletor +
+ Role + + Hero + + Villain +
+ Weapon + + Power Sword + + Havoc Staff +
+ Dark secret + + Expert florist + + Cries at romcoms +
+ `, + ); + await editor.update(() => { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection), + 'isRangeSelection(selection)', + ); + $insertDataTransferForRichText(dataTransfer, selection, editor); + }); + // Here we are testing the createDOM, not the exportDOM, so the tbody is not there + expectTableHtmlToBeEqual( + testEnv.innerHTML, + html` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+


+
+

+ He-Man +

+
+

+ Skeletor +

+
+

+ Role +

+
+

+ Hero +

+
+

+ Villain +

+
+

+ Weapon +

+
+

+ Power Sword +

+
+

+ Havoc Staff +

+
+

+ Dark secret +

+
+

+ Expert florist +

+
+

+ Cries at romcoms +

+
+ `, + ); + }); + test('Copy table from an external source like gdoc with formatting', async () => { const {editor} = testEnv; diff --git a/packages/lexical-table/src/index.ts b/packages/lexical-table/src/index.ts index be452681b98..c4fe6ace096 100644 --- a/packages/lexical-table/src/index.ts +++ b/packages/lexical-table/src/index.ts @@ -29,6 +29,11 @@ export { } from './LexicalTableNode'; export type {TableDOMCell} from './LexicalTableObserver'; export {$getTableAndElementByKey, TableObserver} from './LexicalTableObserver'; +export { + registerTableCellUnmergeTransform, + registerTablePlugin, + registerTableSelectionObserver, +} from './LexicalTablePluginHelpers'; export type {SerializedTableRowNode} from './LexicalTableRowNode'; export { $createTableRowNode, diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 11524eee950..958dd8acfa7 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -125,3 +125,14 @@ declare export function $splitNode( declare export function calculateZoomLevel(element: Element | null): number; declare export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean; + +declare export function $unwrapAndFilterDescendants( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, +): boolean; + +declare export function $firstToLastIterator(node: ElementNode): Iterable; + +declare export function $lastToFirstIterator(node: ElementNode): Iterable; + +declare export function $unwrapNode(node: ElementNode): void; diff --git a/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx b/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx new file mode 100644 index 00000000000..3a4e596626f --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx @@ -0,0 +1,84 @@ +/** + * 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} from 'lexical'; + +import {$descendantsMatching} from '@lexical/utils'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $isTextNode, + ParagraphNode, +} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +function $createTextAndParagraphWithDepth(depth: number): LexicalNode[] { + if (depth <= 0) { + return [$createTextNode(`<${depth} />`)]; + } + return [ + $createTextNode(`<${depth}>`), + $createParagraphNode().append( + ...$createTextAndParagraphWithDepth(depth - 1), + ), + $createTextNode(``), + ]; +} + +function textContentForDepth(i: number): string { + return i > 0 ? `<${i}>${textContentForDepth(i - 1)}` : `<${i} />`; +} + +describe('$descendantsMatching', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + [0, 1, 2].forEach((depth) => + it(`Can un-nest children at depth ${depth}`, () => { + editor.update( + () => { + const firstNode = $createParagraphNode(); + $getRoot() + .clear() + .append( + firstNode.append(...$createTextAndParagraphWithDepth(depth)), + ); + }, + {discrete: true}, + ); + editor.update( + () => { + const firstNode = assertClass( + $getRoot().getFirstChildOrThrow(), + ParagraphNode, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(depth === 0); + firstNode.splice( + 0, + firstNode.getChildrenSize(), + $descendantsMatching(firstNode.getChildren(), $isTextNode), + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(true); + expect(firstNode.getTextContent()).toBe(textContentForDepth(depth)); + }, + {discrete: true}, + ); + }), + ); +}); diff --git a/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx b/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx new file mode 100644 index 00000000000..f7042e14e0e --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx @@ -0,0 +1,216 @@ +/** + * 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} from 'lexical'; + +import {$firstToLastIterator, $lastToFirstIterator} from '@lexical/utils'; +import {$createParagraphNode, $createTextNode, TextNode} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +describe('$firstToLastIterator', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it(`Iterates from first to last`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + return assertClass(node, TextNode).getTextContent(); + }), + ).toEqual(['0', '1', '2', '3', '4']); + // Parent was not affected + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + }, + {discrete: true}, + ); + }); + it(`Can handle node removal`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + node.remove(); + return rval; + }), + ).toEqual(['0', '1', '2', '3', '4']); + expect(parent.getChildren()).toEqual([]); + }, + {discrete: true}, + ); + }); + it(`Detects cycles when nodes move incorrectly`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect(() => + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + parent.append(node); + return rval; + }), + ).toThrow(/\$childIterator: Cycle detected/); + }, + {discrete: true}, + ); + }); + it(`Can handle nodes moving in the other direction`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + if (node.getPreviousSibling() !== null) { + parent.splice(0, 0, [node]); + } + return rval; + }), + ).toEqual(['0', '1', '2', '3', '4']); + // This mutation reversed the nodes while traversing + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['4', '3', '2', '1', '0']); + }, + {discrete: true}, + ); + }); +}); + +describe('$lastToFirstIterator', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it(`Iterates from last to first`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + return assertClass(node, TextNode).getTextContent(); + }), + ).toEqual(['4', '3', '2', '1', '0']); + // Parent was not affected + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + }, + {discrete: true}, + ); + }); + it(`Can handle node removal`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + node.remove(); + return rval; + }), + ).toEqual(['4', '3', '2', '1', '0']); + expect(parent.getChildren()).toEqual([]); + }, + {discrete: true}, + ); + }); + it(`Can handle nodes moving in the other direction`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + parent.append(node); + return rval; + }), + ).toEqual(['4', '3', '2', '1', '0']); + // This mutation reversed the nodes while traversing + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['4', '3', '2', '1', '0']); + }, + {discrete: true}, + ); + }); +}); diff --git a/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx b/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx new file mode 100644 index 00000000000..cb42d0fe643 --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx @@ -0,0 +1,101 @@ +/** + * 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} from 'lexical'; + +import {$unwrapAndFilterDescendants} from '@lexical/utils'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $isParagraphNode, + $isTextNode, + ParagraphNode, +} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +function $createTextAndParagraphWithDepth(depth: number): LexicalNode[] { + if (depth <= 0) { + return [$createTextNode(`<${depth} />`)]; + } + return [ + $createTextNode(`<${depth}>`), + $createParagraphNode().append( + ...$createTextAndParagraphWithDepth(depth - 1), + ), + $createTextNode(``), + ]; +} + +function textContentForDepth(i: number): string { + return i > 0 ? `<${i}>${textContentForDepth(i - 1)}` : `<${i} />`; +} + +describe('$unwrapAndFilterDescendants', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it('Is a no-op with valid children', () => { + editor.update( + () => { + $getRoot().clear().append($createParagraphNode()); + }, + {discrete: true}, + ); + editor.update( + () => { + expect($unwrapAndFilterDescendants($getRoot(), $isParagraphNode)).toBe( + false, + ); + expect($getRoot().getChildrenSize()).toBe(1); + expect($isParagraphNode($getRoot().getFirstChild())).toBe(true); + }, + {discrete: true}, + ); + }); + [0, 1, 2].forEach((depth) => + it(`Can un-nest children at depth ${depth}`, () => { + editor.update( + () => { + const firstNode = $createParagraphNode(); + $getRoot() + .clear() + .append( + firstNode.append(...$createTextAndParagraphWithDepth(depth)), + ); + }, + {discrete: true}, + ); + editor.update( + () => { + const firstNode = assertClass( + $getRoot().getFirstChildOrThrow(), + ParagraphNode, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(depth === 0); + expect($unwrapAndFilterDescendants(firstNode, $isTextNode)).toBe( + depth > 0, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(true); + expect(firstNode.getTextContent()).toBe(textContentForDepth(depth)); + }, + {discrete: true}, + ); + }), + ); +}); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 8994e3dad65..0a758f40f2f 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -23,6 +23,7 @@ import { Klass, LexicalEditor, LexicalNode, + NodeKey, } from 'lexical'; // This underscore postfixing is used as a hotfix so we do not // export shared types from this module #5918 @@ -690,3 +691,157 @@ export function calculateZoomLevel(element: Element | null): number { export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean { return editor._parentEditor !== null; } + +/** + * A depth first last-to-first traversal of root that stops at each node that matches + * $predicate and ensures that its parent is root. This is typically used to discard + * invalid or unsupported wrapping nodes. For example, a TableNode must only have + * TableRowNode as children, but an importer might add invalid nodes based on + * caption, tbody, thead, etc. and this will unwrap and discard those. + * + * @param root The root to start the traversal + * @param $predicate Should return true for nodes that are permitted to be children of root + * @returns true if this unwrapped or removed any nodes + */ +export function $unwrapAndFilterDescendants( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, +): boolean { + return $unwrapAndFilterDescendantsImpl(root, $predicate, null); +} + +function $unwrapAndFilterDescendantsImpl( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, + $onSuccess: null | ((node: LexicalNode) => void), +): boolean { + let didMutate = false; + for (const node of $lastToFirstIterator(root)) { + if ($predicate(node)) { + if ($onSuccess !== null) { + $onSuccess(node); + } + continue; + } + didMutate = true; + if ($isElementNode(node)) { + $unwrapAndFilterDescendantsImpl( + node, + $predicate, + $onSuccess ? $onSuccess : (child) => node.insertAfter(child), + ); + } + node.remove(); + } + return didMutate; +} + +/** + * A depth first traversal of the children array that stops at and collects + * each node that `$predicate` matches. This is typically used to discard + * invalid or unsupported wrapping nodes on a children array in the `after` + * of an {@link lexical!DOMConversionOutput}. For example, a TableNode must only have + * TableRowNode as children, but an importer might add invalid nodes based on + * caption, tbody, thead, etc. and this will unwrap and discard those. + * + * This function is read-only and performs no mutation operations, which makes + * it suitable for import and export purposes but likely not for any in-place + * mutation. You should use {@link $unwrapAndFilterDescendants} for in-place + * mutations such as node transforms. + * + * @param children The children to traverse + * @param $predicate Should return true for nodes that are permitted to be children of root + * @returns The children or their descendants that match $predicate + */ +export function $descendantsMatching( + children: LexicalNode[], + $predicate: (node: LexicalNode) => node is T, +): T[]; +export function $descendantsMatching( + children: LexicalNode[], + $predicate: (node: LexicalNode) => boolean, +): LexicalNode[] { + const result: LexicalNode[] = []; + const stack = [...children].reverse(); + for (let child = stack.pop(); child !== undefined; child = stack.pop()) { + if ($predicate(child)) { + result.push(child); + } else if ($isElementNode(child)) { + for (const grandchild of $lastToFirstIterator(child)) { + stack.push(grandchild); + } + } + } + return result; +} + +/** + * Return an iterator that yields each child of node from first to last, taking + * care to preserve the next sibling before yielding the value in case the caller + * removes the yielded node. + * + * @param node The node whose children to iterate + * @returns An iterator of the node's children + */ +export function $firstToLastIterator(node: ElementNode): Iterable { + return { + [Symbol.iterator]: () => + $childIterator(node.getFirstChild(), (child) => child.getNextSibling()), + }; +} + +/** + * Return an iterator that yields each child of node from last to first, taking + * care to preserve the previous sibling before yielding the value in case the caller + * removes the yielded node. + * + * @param node The node whose children to iterate + * @returns An iterator of the node's children + */ +export function $lastToFirstIterator(node: ElementNode): Iterable { + return { + [Symbol.iterator]: () => + $childIterator(node.getLastChild(), (child) => + child.getPreviousSibling(), + ), + }; +} + +function $childIterator( + initialNode: LexicalNode | null, + nextNode: (node: LexicalNode) => LexicalNode | null, +): Iterator { + let state = initialNode; + const seen = __DEV__ ? new Set() : null; + return { + next() { + if (state === null) { + return iteratorDone; + } + const rval = iteratorNotDone(state); + if (__DEV__ && seen !== null) { + const key = state.getKey(); + invariant( + !seen.has(key), + '$childIterator: Cycle detected, node with key %s has already been traversed', + String(key), + ); + seen.add(key); + } + state = nextNode(state); + return rval; + }, + }; +} + +/** + * Insert all children before this node, and then remove it. + * + * @param node The ElementNode to unwrap and remove + */ +export function $unwrapNode(node: ElementNode): void { + for (const child of $firstToLastIterator(node)) { + node.insertBefore(child); + } + node.remove(); +} diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index cf33a568d3f..3986f27806f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -2210,7 +2210,7 @@ describe('LexicalEditor tests', () => { await editor.update(() => { const root = $getRoot(); - const tableCell = $createTableCellNode(0); + const tableCell = $createTableCellNode(); const tableRow = $createTableRowNode(); const table = $createTableNode(); @@ -2225,7 +2225,7 @@ describe('LexicalEditor tests', () => { await editor.update(() => { const tableRow = $getNodeByKey(tableRowKey) as TableRowNode; - const tableCell = $createTableCellNode(0); + const tableCell = $createTableCellNode(); tableRow.append(tableCell); }); From 55ef7cad49c303a343575745d311fb1a3d05c15b Mon Sep 17 00:00:00 2001 From: Hadi Elghoul <113323394+elgh0ul@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:25:05 -0500 Subject: [PATCH 132/133] [lexical][@lexical/selection] Feature: Unify $selectAll Implementations (#6902) Co-authored-by: Hadi Elghoul --- packages/lexical-selection/src/index.ts | 4 +-- .../lexical-selection/src/range-selection.ts | 35 ------------------- packages/lexical/src/LexicalUtils.ts | 21 +++++++++-- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/packages/lexical-selection/src/index.ts b/packages/lexical-selection/src/index.ts index d901ab4d4d9..8d9d47ce635 100644 --- a/packages/lexical-selection/src/index.ts +++ b/packages/lexical-selection/src/index.ts @@ -18,7 +18,6 @@ import { $isParentElementRTL, $moveCaretSelection, $moveCharacter, - $selectAll, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, @@ -32,7 +31,9 @@ import { export { /** @deprecated moved to the lexical package */ $cloneWithProperties, + /** @deprecated moved to the lexical package */ $selectAll, } from 'lexical'; + export { $addNodeStyle, $isAtNodeEnd, @@ -48,7 +49,6 @@ export { $isParentElementRTL, $moveCaretSelection, $moveCharacter, - $selectAll, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, diff --git a/packages/lexical-selection/src/range-selection.ts b/packages/lexical-selection/src/range-selection.ts index e92a81b6188..15b4e66c5c7 100644 --- a/packages/lexical-selection/src/range-selection.ts +++ b/packages/lexical-selection/src/range-selection.ts @@ -452,41 +452,6 @@ export function $moveCharacter( ); } -/** - * Expands the current Selection to cover all of the content in the editor. - * @param selection - The current selection. - */ -export function $selectAll(selection: RangeSelection): void { - const anchor = selection.anchor; - const focus = selection.focus; - const anchorNode = anchor.getNode(); - const topParent = anchorNode.getTopLevelElementOrThrow(); - const root = topParent.getParentOrThrow(); - let firstNode = root.getFirstDescendant(); - let lastNode = root.getLastDescendant(); - let firstType: 'element' | 'text' = 'element'; - let lastType: 'element' | 'text' = 'element'; - let lastOffset = 0; - - if ($isTextNode(firstNode)) { - firstType = 'text'; - } else if (!$isElementNode(firstNode) && firstNode !== null) { - firstNode = firstNode.getParentOrThrow(); - } - - if ($isTextNode(lastNode)) { - lastType = 'text'; - lastOffset = lastNode.getTextContentSize(); - } else if (!$isElementNode(lastNode) && lastNode !== null) { - lastNode = lastNode.getParentOrThrow(); - } - - if (firstNode && lastNode) { - anchor.set(firstNode.getKey(), 0, firstType); - focus.set(lastNode.getKey(), lastOffset, lastType); - } -} - /** * Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue. * @param node - The node whose style value to get. diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index f4cc88ac86a..d4bc2d1ea46 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1084,10 +1084,25 @@ export function isSelectAll( return key.toLowerCase() === 'a' && controlOrMeta(metaKey, ctrlKey); } -export function $selectAll(): void { +export function $selectAll(selection?: RangeSelection | null): RangeSelection { const root = $getRoot(); - const selection = root.select(0, root.getChildrenSize()); - $setSelection($normalizeSelection(selection)); + + if ($isRangeSelection(selection)) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = anchor.getNode(); + const topParent = anchorNode.getTopLevelElementOrThrow(); + const rootNode = topParent.getParentOrThrow(); + anchor.set(rootNode.getKey(), 0, 'element'); + focus.set(rootNode.getKey(), rootNode.getChildrenSize(), 'element'); + $normalizeSelection(selection); + return selection; + } else { + // Create a new RangeSelection + const newSelection = root.select(0, root.getChildrenSize()); + $setSelection($normalizeSelection(newSelection)); + return newSelection; + } } export function getCachedClassNameArray( From 7776cea0f1862cb57b3c2d2118da629e098b784c Mon Sep 17 00:00:00 2001 From: Basile Savouret <47100280+basile-savouret@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:26:53 +0100 Subject: [PATCH 133/133] [lexical-playground]: Fix empty layout item causes 100% CPU usage (#6906) Co-authored-by: Ivaylo Pavlov --- .../src/plugins/LayoutPlugin/LayoutPlugin.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx b/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx index cbdeee1fe23..dd226849e62 100644 --- a/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx +++ b/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx @@ -97,6 +97,25 @@ export function LayoutPlugin(): null { return false; }; + const $fillLayoutItemIfEmpty = (node: LayoutItemNode) => { + if (node.isEmpty()) { + node.append($createParagraphNode()); + } + }; + + const $removeIsolatedLayoutItem = (node: LayoutItemNode): boolean => { + const parent = node.getParent(); + if (!$isLayoutContainerNode(parent)) { + const children = node.getChildren(); + for (const child of children) { + node.insertBefore(child); + } + node.remove(); + return true; + } + return false; + }; + return mergeRegister( // When layout is the last child pressing down/right arrow will insert paragraph // below it to allow adding more content. It's similar what $insertBlockNode @@ -186,17 +205,17 @@ export function LayoutPlugin(): null { }, COMMAND_PRIORITY_EDITOR, ), - // Structure enforcing transformers for each node type. In case nesting structure is not - // "Container > Item" it'll unwrap nodes and convert it back - // to regular content. + editor.registerNodeTransform(LayoutItemNode, (node) => { - const parent = node.getParent(); - if (!$isLayoutContainerNode(parent)) { - const children = node.getChildren(); - for (const child of children) { - node.insertBefore(child); - } - node.remove(); + // Structure enforcing transformers for each node type. In case nesting structure is not + // "Container > Item" it'll unwrap nodes and convert it back + // to regular content. + const isRemoved = $removeIsolatedLayoutItem(node); + + if (!isRemoved) { + // Layout item should always have a child. this function will listen + // for any empty layout item and fill it with a paragraph node + $fillLayoutItemIfEmpty(node); } }), editor.registerNodeTransform(LayoutContainerNode, (node) => {