From 4e8c7cc05496c5db7d6160d848af0d83c10a9ad6 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Thu, 4 Jul 2024 01:14:49 +0300 Subject: [PATCH 001/103] [lexical-playground] Table Hover Action Buttons (#6355) Co-authored-by: Ivaylo Pavlov --- packages/lexical-playground/src/Editor.tsx | 2 + .../plugins/TableHoverActionsPlugin/index.tsx | 247 ++++++++++++++++++ .../src/themes/PlaygroundEditorTheme.css | 14 +- .../src/themes/PlaygroundEditorTheme.ts | 2 - 4 files changed, 252 insertions(+), 13 deletions(-) create mode 100644 packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 096770f184e..a6973c68b60 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -63,6 +63,7 @@ import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'; import TabFocusPlugin from './plugins/TabFocusPlugin'; import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; import TableCellResizer from './plugins/TableCellResizer'; +import TableHoverActionsPlugin from './plugins/TableHoverActionsPlugin'; import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'; import ToolbarPlugin from './plugins/ToolbarPlugin'; import TreeViewPlugin from './plugins/TreeViewPlugin'; @@ -185,6 +186,7 @@ export default function Editor(): JSX.Element { hasCellBackgroundColor={tableCellBackgroundColor} /> + diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx new file mode 100644 index 00000000000..b95c2935817 --- /dev/null +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -0,0 +1,247 @@ +/** + * 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 { + $getTableColumnIndexFromTableCellNode, + $getTableRowIndexFromTableCellNode, + $insertTableColumn__EXPERIMENTAL, + $insertTableRow__EXPERIMENTAL, + $isTableCellNode, + $isTableNode, + TableCellNode, + TableNode, + TableRowNode, +} from '@lexical/table'; +import {$findMatchingParent, mergeRegister} from '@lexical/utils'; +import {$getNearestNodeFromDOMNode} from 'lexical'; +import {useEffect, useRef, useState} from 'react'; +import * as React from 'react'; +import {createPortal} from 'react-dom'; + +import {useDebounce} from '../CodeActionMenuPlugin/utils'; + +const BUTTON_WIDTH_PX = 20; + +function TableHoverActionsContainer({ + anchorElem, +}: { + anchorElem: HTMLElement; +}): JSX.Element { + const [editor] = useLexicalComposerContext(); + const [isShownRow, setShownRow] = useState(false); + const [isShownColumn, setShownColumn] = useState(false); + const [shouldListenMouseMove, setShouldListenMouseMove] = + useState(false); + const [position, setPosition] = useState({}); + const codeSetRef = useRef>(new Set()); + const tableDOMNodeRef = useRef(null); + + const debouncedOnMouseMove = useDebounce( + (event: MouseEvent) => { + const {isOutside, tableDOMNode} = getMouseInfo(event); + + if (isOutside) { + setShownRow(false); + setShownColumn(false); + return; + } + + if (!tableDOMNode) { + return; + } + + tableDOMNodeRef.current = tableDOMNode; + + let hoveredRowNode: TableCellNode | null = null; + 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 = 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; + } + } + } + }); + + if (tableDOMElement) { + const { + width: tableElemWidth, + y: tableElemY, + x: tableElemX, + right: tableElemRight, + bottom: tableElemBottom, + height: tableElemHeight, + } = (tableDOMElement as HTMLTableElement).getBoundingClientRect(); + + const {y: editorElemY} = anchorElem.getBoundingClientRect(); + + if (hoveredRowNode) { + setShownRow(true); + setPosition({ + height: BUTTON_WIDTH_PX, + left: tableElemX, + top: tableElemBottom - editorElemY + 5, + width: tableElemWidth, + }); + } else if (hoveredColumnNode) { + setShownColumn(true); + setPosition({ + height: tableElemHeight, + left: tableElemRight + 5, + top: tableElemY - editorElemY, + width: BUTTON_WIDTH_PX, + }); + } + } + }, + 50, + 250, + ); + + useEffect(() => { + if (!shouldListenMouseMove) { + return; + } + + document.addEventListener('mousemove', debouncedOnMouseMove); + + return () => { + setShownRow(false); + setShownColumn(false); + debouncedOnMouseMove.cancel(); + document.removeEventListener('mousemove', debouncedOnMouseMove); + }; + }, [shouldListenMouseMove, debouncedOnMouseMove]); + + useEffect(() => { + return mergeRegister( + editor.registerMutationListener(TableNode, (mutations) => { + editor.getEditorState().read(() => { + for (const [key, type] of mutations) { + switch (type) { + case 'created': + codeSetRef.current.add(key); + setShouldListenMouseMove(codeSetRef.current.size > 0); + break; + + case 'destroyed': + codeSetRef.current.delete(key); + setShouldListenMouseMove(codeSetRef.current.size > 0); + break; + + default: + break; + } + } + }); + }), + ); + }, []); + + const insertAction = (insertRow: boolean) => { + editor.update(() => { + if (tableDOMNodeRef.current) { + const maybeTableNode = $getNearestNodeFromDOMNode( + tableDOMNodeRef.current, + ); + maybeTableNode?.selectEnd(); + if (insertRow) { + $insertTableRow__EXPERIMENTAL(); + setShownRow(false); + } else { + $insertTableColumn__EXPERIMENTAL(); + setShownColumn(false); + } + } + }); + }; + + return ( + <> + {isShownRow && ( + @@ -175,9 +175,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -237,9 +237,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -249,9 +249,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -277,9 +277,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -328,9 +328,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -340,9 +340,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -370,9 +370,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
Yellow flower in tilt shift lens
@@ -415,9 +415,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
lexical logo
@@ -427,9 +427,9 @@ test.describe('Images', () => { data-lexical-decorator="true">
a pretty yellow flower :)
diff --git a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs index a9c8300992a..a0b460c8225 100644 --- a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs @@ -99,8 +99,8 @@ test.describe('Identation', () => { dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> code


@@ -177,8 +177,8 @@ test.describe('Identation', () => { dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> code

{ dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> code

{ dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> code

{ dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> code


diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index c21db1548c2..289ccf34996 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -65,10 +65,10 @@ test.describe.parallel('Links', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> + dir="ltr" + href="https://" + rel="noreferrer"> Hello

@@ -92,10 +92,10 @@ test.describe.parallel('Links', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> + dir="ltr" + href="https://facebook.com" + rel="noreferrer"> Hello

@@ -1499,10 +1499,10 @@ test.describe.parallel('Links', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> + dir="ltr" + href="https://" + rel="noreferrer"> An Awesome Website

@@ -1522,10 +1522,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hey, check this out: + dir="ltr" + href="https://" + rel="noreferrer"> An Awesome Website ! @@ -1591,10 +1591,10 @@ test.describe.parallel('Links', () => { dir="ltr"> This is an + dir="ltr" + href="https://" + rel="noreferrer"> Awesome Website , right? @@ -1636,10 +1636,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://" + rel="noreferrer"> world

@@ -1662,10 +1662,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://facebook.com" + rel="noreferrer"> world

@@ -1742,10 +1742,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://" + rel="noreferrer"> world

@@ -1778,10 +1778,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://facebook.com" + rel="noreferrer"> world

@@ -1853,10 +1853,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://" + rel="noreferrer"> world

@@ -1876,10 +1876,10 @@ test.describe.parallel('Links', () => { dir="ltr"> Hello + dir="ltr" + href="https://" + rel="noreferrer"> world diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index 8dcd61d0fe0..ef1b7018ff7 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -98,9 +98,9 @@ test.describe.parallel('Nested List', () => { html`
  • + dir="ltr" + value="1"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam venenatis risus ac cursus efficitur. Cras efficitur magna odio, @@ -258,17 +258,17 @@ test.describe.parallel('Nested List', () => {
      • -
      • +
      • foo
    • -
    • +
    • bar
      • -
      • +
      • baz
      @@ -487,10 +487,10 @@ test.describe.parallel('Nested List', () => { dir="ltr"> One + dir="ltr" + href="https://" + rel="noreferrer"> two three @@ -518,10 +518,10 @@ test.describe.parallel('Nested List', () => { dir="ltr"> One + dir="ltr" + href="https://" + rel="noreferrer"> two three @@ -1059,7 +1059,7 @@ test.describe.parallel('Nested List', () => { page, html`
        -
      • +
      • a
      @@ -1090,10 +1090,10 @@ test.describe.parallel('Nested List', () => { page, html`
        -
      • +
      • a
      • -
      • +
      • b
      @@ -1106,7 +1106,7 @@ test.describe.parallel('Nested List', () => { page, html`
        -
      • +
      • a
      @@ -1132,13 +1132,13 @@ test.describe.parallel('Nested List', () => { page, html`
        -
      • +
      • a
      • -
      • +
      • b
      • -
      • +
      • c
      @@ -1151,13 +1151,13 @@ test.describe.parallel('Nested List', () => { page, html`
        -
      • +
      • a

      b

        -
      • +
      • c
      @@ -1187,21 +1187,21 @@ test.describe.parallel('Nested List', () => { html`
      • {
        @@ -1247,12 +1247,12 @@ test.describe.parallel('Nested List', () => {
        @@ -1266,21 +1266,21 @@ test.describe.parallel('Nested List', () => { html`
        • {
          diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index 014bc9f6ef7..b434d62be55 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -909,11 +909,11 @@ test.describe.parallel('Markdown', () => { html` + data-language="markdown"> Hello
          { data-lexical-decorator="true">
          Yellow flower in tilt shift lens
          @@ -999,9 +999,9 @@ test.describe.parallel('Markdown', () => { data-lexical-decorator="true">
          Yellow flower in tilt shift lens
          @@ -1010,7 +1010,7 @@ test.describe.parallel('Markdown', () => { class="editor-equation" contenteditable="false" data-lexical-decorator="true"> - + - +

          @@ -1042,9 +1042,9 @@ test.describe.parallel('Markdown', () => { dir="ltr"> Hello + dir="ltr" + href="https://lexical.dev"> link world @@ -1137,9 +1137,9 @@ const TYPED_MARKDOWN_HTML = html` works + dir="ltr" + href="https://lexical.io"> @@ -1184,19 +1184,19 @@ const TYPED_MARKDOWN_HTML = html` data-lexical-decorator="true" />
          • + dir="ltr" + value="1"> List here
          • + class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__nestedListItem" + value="2">
            • + dir="ltr" + value="1"> Nested one
            @@ -1204,11 +1204,11 @@ const TYPED_MARKDOWN_HTML = html`
          + data-language="sql"> Code block

          + dir="ltr" + href="https://lexical.io"> @@ -1343,16 +1343,16 @@ const IMPORTED_MARKDOWN_HTML = html` dir="ltr"> Links + dir="ltr" + href="https://lexical.io/tag_here_and__here__and___here___too"> with underscores and ( + dir="ltr" + href="https://lexical.dev"> parenthesis ) @@ -1422,9 +1422,9 @@ const IMPORTED_MARKDOWN_HTML = html`

          • + dir="ltr" + value="1"> Create a list with + @@ -1439,25 +1439,25 @@ const IMPORTED_MARKDOWN_HTML = html`
          • + class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__nestedListItem" + value="2">
            • + dir="ltr" + value="1"> Lists can be indented with 2 spaces
            • + class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__nestedListItem" + value="2">
              • + dir="ltr" + value="1"> Very easy
              @@ -1470,9 +1470,9 @@ const IMPORTED_MARKDOWN_HTML = html`
              1. + dir="ltr" + value="1"> Oredered lists started with numbers as @@ -1481,13 +1481,13 @@ const IMPORTED_MARKDOWN_HTML = html`
              2. + class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__nestedListItem" + value="2">
                1. + dir="ltr" + value="1"> And can be nested
                  and multiline as well @@ -1498,11 +1498,11 @@ const IMPORTED_MARKDOWN_HTML = html`

                  .

                  -
                    +
                    1. + dir="ltr" + value="31"> Have any starting number
                    @@ -1529,11 +1529,11 @@ const IMPORTED_MARKDOWN_HTML = html` + data-language="javascript"> // Some comments diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 27bc2daddc0..428b177d53b 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -151,11 +151,11 @@ test.describe.parallel('Selection', () => {

                    + data-language="javascript"> Line2 `, diff --git a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs index 692d2526671..0019f2fb25c 100644 --- a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs @@ -98,8 +98,8 @@ test.describe('Tab', () => { dir="ltr" spellcheck="false" data-gutter="1" - data-language="javascript" - data-highlight-language="javascript"> + data-highlight-language="javascript" + data-language="javascript"> { data-lexical-decorator="true">
                    Yellow flower in tilt shift lens diff --git a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs index 513ca31c5b9..eb039765480 100644 --- a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs @@ -84,10 +84,10 @@ test.describe('Toolbar', () => {

                    @@ -271,8 +271,8 @@ test.describe('Toolbar', () => { data-lexical-decorator="true">

                    Yellow flower in tilt shift lens @@ -307,8 +307,8 @@ test.describe('Toolbar', () => { data-lexical-decorator="true">
                    Yellow flower in tilt shift lens diff --git a/packages/lexical-playground/__tests__/regression/1083-backspace-with-element-at-front.spec.mjs b/packages/lexical-playground/__tests__/regression/1083-backspace-with-element-at-front.spec.mjs index adca688f6d3..c8cbacc7cdb 100644 --- a/packages/lexical-playground/__tests__/regression/1083-backspace-with-element-at-front.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/1083-backspace-with-element-at-front.spec.mjs @@ -44,10 +44,10 @@ test.describe('Regression test #1083', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> + dir="ltr" + href="https://" + rel="noreferrer"> Hello World @@ -91,10 +91,10 @@ test.describe('Regression test #1083', () => { dir="ltr"> Say + dir="ltr" + href="https://" + rel="noreferrer"> Hello World diff --git a/packages/lexical-playground/__tests__/regression/1113-link-newline-at-end.spec.mjs b/packages/lexical-playground/__tests__/regression/1113-link-newline-at-end.spec.mjs index f07fa119978..ba419ff50ab 100644 --- a/packages/lexical-playground/__tests__/regression/1113-link-newline-at-end.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/1113-link-newline-at-end.spec.mjs @@ -34,9 +34,9 @@ test.describe('Regression test #1113', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> + dir="ltr" + href="https://www.example.com"> https://www.example.com
                    diff --git a/packages/lexical-playground/__tests__/regression/3433-merge-markdown-lists.spec.mjs b/packages/lexical-playground/__tests__/regression/3433-merge-markdown-lists.spec.mjs index 728a6c0883d..dbf5ce4c8aa 100644 --- a/packages/lexical-playground/__tests__/regression/3433-merge-markdown-lists.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/3433-merge-markdown-lists.spec.mjs @@ -31,15 +31,15 @@ test.describe('Regression test #3433', () => { html`
                    • + dir="ltr" + value="1"> two
                    • + dir="ltr" + value="2"> one
                    diff --git a/packages/lexical-playground/__tests__/regression/5251-paste-into-inline-element.spec.mjs b/packages/lexical-playground/__tests__/regression/5251-paste-into-inline-element.spec.mjs index 38be80d7478..eb4df543bf7 100644 --- a/packages/lexical-playground/__tests__/regression/5251-paste-into-inline-element.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/5251-paste-into-inline-element.spec.mjs @@ -71,10 +71,10 @@ test.describe('Regression test #5251', () => { dir="ltr"> Hello + dir="ltr" + href="https://" + rel="noreferrer"> World

                    @@ -101,10 +101,10 @@ test.describe('Regression test #5251', () => { bold + dir="ltr" + href="https://" + rel="noreferrer"> ld

                    diff --git a/packages/lexical-playground/esm/index.html b/packages/lexical-playground/esm/index.html index 852501dced6..211ce77ab43 100644 --- a/packages/lexical-playground/esm/index.html +++ b/packages/lexical-playground/esm/index.html @@ -4,10 +4,10 @@ - - - - + + + + Lexical Basic - Vanilla JS with ESM @@ -32,6 +32,6 @@

                    Editor state:

                    } } - + diff --git a/packages/lexical-playground/index.html b/packages/lexical-playground/index.html index 68bad91b424..d810340cd24 100644 --- a/packages/lexical-playground/index.html +++ b/packages/lexical-playground/index.html @@ -5,15 +5,15 @@ - - - + + + Lexical Playground
                    - + diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index 18a814b3669..b1337764ddf 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -39,7 +39,8 @@ import { SerializedTextNode, TextNode, } from 'lexical'; -import {format} from 'prettier'; +import path from 'path'; +import * as prettier from 'prettier'; import * as React from 'react'; import {createRef} from 'react'; import {createRoot} from 'react-dom/client'; @@ -48,6 +49,10 @@ import * as ReactTestUtils from 'shared/react-test-utils'; import {CreateEditorArgs, LexicalNodeReplacement} from '../../LexicalEditor'; import {resetRandomKey} from '../../LexicalUtils'; +const prettierConfig = prettier.resolveConfig.sync( + path.resolve(__dirname, '../../../../.prettierrc'), +); + type TestEnv = { readonly container: HTMLDivElement; readonly editor: LexicalEditor; @@ -779,5 +784,8 @@ export function expectHtmlToBeEqual(expected: string, actual: string): void { } export function prettifyHtml(s: string): string { - return format(s.replace(/\n/g, ''), {parser: 'html'}); + return prettier.format(s.replace(/\n/g, ''), { + ...prettierConfig, + parser: 'html', + }); } From 0dd67308eccc6cbcc706ac6665c0b6f6693c3404 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 10 Jul 2024 23:06:42 +0800 Subject: [PATCH 015/103] CI: tag flaky tests (#6388) --- .../__tests__/e2e/Tables.spec.mjs | 458 +++++++++--------- .../4697-repeated-table-selection.spec.mjs | 58 +-- 2 files changed, 262 insertions(+), 254 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 997e1a529ee..dee1a4637e1 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1611,82 +1611,84 @@ test.describe.parallel('Tables', () => { ); }); - test('Resize merged cells width (2)', 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( + 'Resize merged cells width (2)', + { + 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, 3, 3); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); - await mergeTableCells(page); - await click(page, 'th'); - const resizerBoundingBox = await selectorBoundingBox( - page, - '.TableCellResizer__resizer:first-child', - ); - const x = resizerBoundingBox.x + resizerBoundingBox.width / 2; - const y = resizerBoundingBox.y + resizerBoundingBox.height / 2; - await page.mouse.move(x, y); - await page.mouse.down(); - await page.mouse.move(x + 50, y); - await page.mouse.up(); + await insertTable(page, 3, 3); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); + await mergeTableCells(page); + await click(page, 'th'); + const resizerBoundingBox = await selectorBoundingBox( + page, + '.TableCellResizer__resizer:first-child', + ); + const x = resizerBoundingBox.x + resizerBoundingBox.width / 2; + const y = resizerBoundingBox.y + resizerBoundingBox.height / 2; + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + 50, y); + await page.mouse.up(); - await assertHTML( - page, - html` -


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


                    -
                    -


                    -
                    -


                    -
                    -


                    -
                    -


                    -
                    -


                    -
                    -


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


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


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    + `, + ); + }, + ); test('Resize merged cells height', async ({ browserName, @@ -2459,66 +2461,68 @@ test.describe.parallel('Tables', () => { ); }); - test('Delete rows (with conflicting merged cell)', 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 rows (with conflicting merged cell)', + { + 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, 4, 2); + await insertTable(page, 4, 2); - await selectCellsFromTableCords( - page, - {x: 1, y: 1}, - {x: 1, y: 3}, - false, - false, - ); - await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 1, y: 3}, + false, + false, + ); + await mergeTableCells(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); - await deleteTableRows(page); + await deleteTableRows(page); - await assertHTML( - page, - html` -


                    - - - - - - - - -
                    -


                    -
                    -


                    -
                    -


                    -
                    -


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


                    + + + + + + + + +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    + `, + ); + }, + ); test('Delete columns (with conflicting merged cell)', async ({ page, @@ -2973,107 +2977,109 @@ test.describe.parallel('Tables', () => { ); }); - test('Can align text using Table selection', async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + test( + 'Can align text using Table selection', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await selectFromAlignDropdown(page, '.center-align'); + await selectFromAlignDropdown(page, '.center-align'); - await assertHTML( - page, - html` -


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

                    - a -

                    -
                    -

                    - bb -

                    -
                    -

                    cc

                    -
                    -

                    - d -

                    -
                    -

                    - e -

                    -
                    -

                    f

                    -
                    -


                    - `, - html` -


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

                    - a -

                    -
                    -

                    - bb -

                    -
                    -

                    cc

                    -
                    -

                    - d -

                    -
                    -

                    - e -

                    -
                    -

                    f

                    -
                    -


                    - `, - {ignoreClasses: true}, - ); - }); + await assertHTML( + page, + html` +


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

                    + a +

                    +
                    +

                    + bb +

                    +
                    +

                    cc

                    +
                    +

                    + d +

                    +
                    +

                    + e +

                    +
                    +

                    f

                    +
                    +


                    + `, + html` +


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

                    + a +

                    +
                    +

                    + bb +

                    +
                    +

                    cc

                    +
                    +

                    + d +

                    +
                    +

                    + e +

                    +
                    +

                    f

                    +
                    +


                    + `, + {ignoreClasses: true}, + ); + }, + ); }); 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 977e66d3bcb..9711bab390b 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 @@ -18,37 +18,39 @@ import { test.describe('Regression test #4697', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test('repeated table selection results in table selection', async ({ - page, - isPlainText, - isCollab, - }) => { - test.skip(isPlainText); + test( + 'repeated table selection results in table selection', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 4, 4); + await insertTable(page, 4, 4); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 1, y: 1}, - {x: 2, y: 2}, - false, - false, - ); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 2, y: 2}, + false, + false, + ); - await selectCellsFromTableCords( - page, - {x: 2, y: 1}, - {x: 2, y: 2}, - false, - false, - ); + await selectCellsFromTableCords( + page, + {x: 2, y: 1}, + {x: 2, y: 2}, + false, + false, + ); - await assertTableSelectionCoordinates(page, { - anchor: {x: 2, y: 1}, - focus: {x: 2, y: 2}, - }); - }); + await assertTableSelectionCoordinates(page, { + anchor: {x: 2, y: 1}, + focus: {x: 2, y: 2}, + }); + }, + ); }); From 54c853c0437126bbf435e4e526ec791260e2289f Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 10 Jul 2024 21:55:10 +0100 Subject: [PATCH 016/103] Fix clear rootElement on React (#6389) --- .../src/shared/LexicalContentEditableElement.tsx | 2 ++ .../src/__tests__/unit/LexicalSelection.test.tsx | 5 +++-- packages/lexical/src/LexicalEditor.ts | 1 - packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 25ab75abc26..328da5d4028 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -60,6 +60,8 @@ export function ContentEditableElement({ rootElement.ownerDocument.defaultView ) { editor.setRootElement(rootElement); + } else { + editor.setRootElement(null); } }, [editor], diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index 1d8caa48066..09e56625b59 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -8,6 +8,7 @@ import {$createLinkNode} from '@lexical/link'; import {$createListItemNode, $createListNode} from '@lexical/list'; +import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {ContentEditable} from '@lexical/react/LexicalContentEditable'; import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary'; @@ -187,6 +188,7 @@ describe('LexicalSelection tests', () => { /> + ); } @@ -195,7 +197,6 @@ describe('LexicalSelection tests', () => { reactRoot.render(); await Promise.resolve().then(); }); - editor!.getRootElement()!.focus(); await Promise.resolve().then(); // Focus first element @@ -2269,7 +2270,7 @@ describe('LexicalSelection tests', () => { }); it('adjust offset for inline elements text formatting', async () => { - init(); + await init(); await editor!.update(() => { const root = $getRoot(); diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 7baee50a8a8..b5b93567aec 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -755,7 +755,6 @@ export class LexicalEditor { listener(this._rootElement, null); listenerSetOrMap.add(listener); return () => { - listener(null, this._rootElement); listenerSetOrMap.delete(listener); }; } diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 4d0f0f49cab..860d3196051 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -972,7 +972,7 @@ describe('LexicalEditor tests', () => { [editor] = useLexicalComposerContext(); useEffect(() => { - editor.registerRootListener(listener); + return editor.registerRootListener(listener); }, []); return null; @@ -1011,7 +1011,7 @@ describe('LexicalEditor tests', () => { await Promise.resolve().then(); }); - expect(listener).toHaveBeenCalledTimes(3); + expect(listener).toHaveBeenCalledTimes(4); expect(container.innerHTML).toBe( '


                    ', ); From 37daaed812b595dd096f5540c3e5e844af0c692e Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:54:15 +0800 Subject: [PATCH 017/103] [Gallery] Add option to filter plugins based on tags (#6391) --- .../src/components/Gallery/GalleryCards.tsx | 25 ++++- .../components/Gallery/components/Filters.tsx | 100 ++++++++++++++++++ .../Gallery/components/TagSelect.tsx | 71 +++++++++++++ .../Gallery/components/styles.module.css | 92 ++++++++++++++++ .../src/components/Gallery/pluginList.tsx | 3 + .../src/components/Gallery/tagList.tsx | 27 +++++ .../src/components/Gallery/utils.tsx | 17 ++- 7 files changed, 329 insertions(+), 6 deletions(-) create mode 100644 packages/lexical-website/src/components/Gallery/components/Filters.tsx create mode 100644 packages/lexical-website/src/components/Gallery/components/TagSelect.tsx create mode 100644 packages/lexical-website/src/components/Gallery/tagList.tsx diff --git a/packages/lexical-website/src/components/Gallery/GalleryCards.tsx b/packages/lexical-website/src/components/Gallery/GalleryCards.tsx index 37d959adae4..71c5ec3d75c 100644 --- a/packages/lexical-website/src/components/Gallery/GalleryCards.tsx +++ b/packages/lexical-website/src/components/Gallery/GalleryCards.tsx @@ -12,9 +12,11 @@ import clsx from 'clsx'; import React, {useEffect, useState} from 'react'; import Card from './Card'; +import Filters from './components/Filters'; import SearchBar from './components/SearchBar'; import {Example, plugins} from './pluginList'; import styles from './styles.module.css'; +import {Tag, TagList} from './tagList'; import {useFilteredExamples} from './utils'; function CardList({cards}: {cards: Array}) { @@ -41,26 +43,41 @@ function GalleryCardsImpl() { const [internGalleryCards, setInternGalleryCards] = useState<{ InternGalleryCards: () => Array; } | null>(null); + const [internGalleryTags, setInternGalleryTags] = useState<{ + InternGalleryTags: () => {[type in string]: Tag}; + } | null>(null); const pluginsCombined = plugins(customFields ?? {}).concat( internGalleryCards != null ? internGalleryCards.InternGalleryCards() : [], ); + const tagList = { + ...TagList, + ...(internGalleryTags != null ? internGalleryTags.InternGalleryTags() : {}), + }; + const filteredPlugins = useFilteredExamples(pluginsCombined); useEffect(() => { if (process.env.FB_INTERNAL) { // @ts-ignore runtime dependency for intern builds import('../../../../InternGalleryCards').then(setInternGalleryCards); + // @ts-ignore runtime dependency for intern builds + import('../../../../InternGalleryTags').then(setInternGalleryTags); } }, []); return (
                    -
                    - -
                    - +
                    + +
                    + +
                    + +
                    ); } diff --git a/packages/lexical-website/src/components/Gallery/components/Filters.tsx b/packages/lexical-website/src/components/Gallery/components/Filters.tsx new file mode 100644 index 00000000000..1ba011e21c9 --- /dev/null +++ b/packages/lexical-website/src/components/Gallery/components/Filters.tsx @@ -0,0 +1,100 @@ +/** + * 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 {CSSProperties, ReactNode} from 'react'; + +import Heading from '@theme/Heading'; +import clsx from 'clsx'; +import React from 'react'; + +import {Example} from '../pluginList'; +import {Tag} from '../tagList'; +import styles from './styles.module.css'; +import TagSelect from './TagSelect'; + +function TagCircleIcon({color, style}: {color: string; style?: CSSProperties}) { + return ( + + ); +} + +function TagListItem({tag, tagKey}: {tag: Tag; tagKey: string}) { + const {title, description, color} = tag; + return ( +
                  1. + + } + /> +
                  2. + ); +} + +function TagList({allTags}: {allTags: {[type in string]: Tag}}) { + return ( +
                      + {Object.keys(allTags).map((tag) => { + return ; + })} +
                    + ); +} + +function HeadingText({filteredPlugins}: {filteredPlugins: Array}) { + return ( +
                    + Filters + + {filteredPlugins.length === 1 + ? '1 exampe' + : `${filteredPlugins.length} examples`} + +
                    + ); +} + +function HeadingRow({filteredPlugins}: {filteredPlugins: Array}) { + return ( +
                    + +
                    + ); +} + +export default function Filters({ + filteredPlugins, + tagList, +}: { + filteredPlugins: Array; + tagList: {[type in string]: Tag}; +}): ReactNode { + return ( +
                    + + +
                    + ); +} diff --git a/packages/lexical-website/src/components/Gallery/components/TagSelect.tsx b/packages/lexical-website/src/components/Gallery/components/TagSelect.tsx new file mode 100644 index 00000000000..5d5798b0b30 --- /dev/null +++ b/packages/lexical-website/src/components/Gallery/components/TagSelect.tsx @@ -0,0 +1,71 @@ +/** + * 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 React, { + type ComponentProps, + type ReactElement, + type ReactNode, + useCallback, + useId, +} from 'react'; + +import {useTags} from '../utils'; +import styles from './styles.module.css'; + +function useTagState(tag: string) { + const [tags, setTags] = useTags(); + const isSelected = tags.includes(tag); + const toggle = useCallback(() => { + setTags((list) => { + return list.includes(tag) + ? list.filter((t) => t !== tag) + : [...list, tag]; + }); + }, [tag, setTags]); + + return [isSelected, toggle] as const; +} + +interface Props extends ComponentProps<'input'> { + tag: string; + label: string; + description: string; + icon: ReactElement>; +} + +export default function TagSelect({ + icon, + label, + description, + tag, + ...rest +}: Props): ReactNode { + const id = useId(); + const [isSelected, toggle] = useTagState(tag); + return ( + <> + { + if (e.key === 'Enter') { + toggle(); + } + }} + {...rest} + /> + + + ); +} diff --git a/packages/lexical-website/src/components/Gallery/components/styles.module.css b/packages/lexical-website/src/components/Gallery/components/styles.module.css index e30db94e530..00c669c1835 100644 --- a/packages/lexical-website/src/components/Gallery/components/styles.module.css +++ b/packages/lexical-website/src/components/Gallery/components/styles.module.css @@ -16,3 +16,95 @@ padding: 10px; border: 1px solid gray; } + +.headingRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.headingText { + display: flex; + align-items: baseline; +} + +.headingText > h2 { + margin-bottom: 0; +} + +.headingText > span { + margin-left: 8px; +} + +.headingButtons { + display: flex; + align-items: center; +} + +.tagList { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.tagListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.tagListItem:last-child { + margin-right: 0; +} + +.checkboxLabel:hover { + opacity: 1; + box-shadow: 0 0 2px 1px var(--ifm-color-secondary-darkest); +} + +input[type='checkbox'] + .checkboxLabel { + display: flex; + align-items: center; + cursor: pointer; + line-height: 1.5; + border-radius: 4px; + padding: 0.275rem 0.8rem; + opacity: 0.85; + transition: opacity 200ms ease-out; + border: 2px solid var(--ifm-color-secondary-darkest); +} + +input:focus-visible + .checkboxLabel { + outline: 2px solid currentColor; +} + +input:checked + .checkboxLabel { + opacity: 0.9; + background-color: hsl(167deg 56% 73% / 25%); + border: 2px solid var(--ifm-color-primary-darkest); +} + +input:checked + .checkboxLabel:hover { + opacity: 0.75; + box-shadow: 0 0 2px 1px var(--ifm-color-primary-dark); +} + +html[data-theme='dark'] input:checked + .checkboxLabel { + background-color: hsl(167deg 56% 73% / 10%); +} + +.screenReaderOnly { + border: 0; + clip: rect(0 0 0 0); + clip-path: polygon(0 0, 0 0, 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} diff --git a/packages/lexical-website/src/components/Gallery/pluginList.tsx b/packages/lexical-website/src/components/Gallery/pluginList.tsx index 6364759d862..97deca02e34 100644 --- a/packages/lexical-website/src/components/Gallery/pluginList.tsx +++ b/packages/lexical-website/src/components/Gallery/pluginList.tsx @@ -14,6 +14,7 @@ export type Example = { uri?: string; preview?: string; renderPreview?: () => ReactNode; + tags: Array; }; export const plugins = (customFields: { @@ -21,11 +22,13 @@ export const plugins = (customFields: { }): Array => [ { description: 'Learn how to create an editor with Emojis', + tags: ['opensource'], title: 'EmojiPlugin', uri: `${customFields.STACKBLITZ_PREFIX}examples/vanilla-js-plugin?embed=1&file=src%2Femoji-plugin%2FEmojiPlugin.ts&terminalHeight=0&ctl=0`, }, { description: 'Learn how to create an editor with Real Time Collaboration', + tags: ['opensource', 'favorite'], title: 'Collab RichText', uri: 'https://stackblitz.com/github/facebook/lexical/tree/fix/collab_example/examples/react-rich-collab?ctl=0&file=src%2Fmain.tsx&terminalHeight=0&embed=1', }, diff --git a/packages/lexical-website/src/components/Gallery/tagList.tsx b/packages/lexical-website/src/components/Gallery/tagList.tsx new file mode 100644 index 00000000000..daf38b9aae4 --- /dev/null +++ b/packages/lexical-website/src/components/Gallery/tagList.tsx @@ -0,0 +1,27 @@ +/** + * 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. + * + */ + +export type Tag = { + color: string; + description: string; + title: string; +}; + +export const TagList: {[type in string]: Tag} = { + favorite: { + color: '#e9669e', + description: + 'Our favorite Docusaurus sites that you must absolutely check out!', + title: 'Favorite', + }, + opensource: { + color: '#39ca30', + description: 'Open-Source Lexical plugins for inspiration', + title: 'Open-Source', + }, +}; diff --git a/packages/lexical-website/src/components/Gallery/utils.tsx b/packages/lexical-website/src/components/Gallery/utils.tsx index 436c53d4ee7..4a908e835e8 100644 --- a/packages/lexical-website/src/components/Gallery/utils.tsx +++ b/packages/lexical-website/src/components/Gallery/utils.tsx @@ -6,7 +6,7 @@ * */ -import {useQueryString} from '@docusaurus/theme-common'; +import {useQueryString, useQueryStringList} from '@docusaurus/theme-common'; import {useMemo} from 'react'; import {Example} from './pluginList'; @@ -15,28 +15,41 @@ export function useSearchName() { return useQueryString('title'); } +export function useTags() { + return useQueryStringList('tags'); +} + function filterExamples({ examples, searchName, + tags, }: { examples: Array; searchName: string; + tags: Array; }) { if (searchName) { - return examples.filter((example) => + examples = examples.filter((example) => example.title.toLowerCase().includes(searchName.toLowerCase()), ); } + if (tags.length !== 0) { + examples = examples.filter((example) => + example.tags.some((tag) => tags.includes(tag)), + ); + } return examples; } export function useFilteredExamples(examples: Array) { const [searchName] = useSearchName(); + const [tags] = useTags(); return useMemo( () => filterExamples({ examples, searchName, + tags, }), [examples, searchName], ); From 1ecef21b524859f7c5672757453db77a3d50e040 Mon Sep 17 00:00:00 2001 From: Serey Roth <88986106+serey-roth@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:02:35 -0700 Subject: [PATCH 018/103] [lexical-playground][lexical-table] Bug Fix: Fix `Shift`+ `Down Arrow` regression for table sequence. (#6393) --- .../src/LexicalTableSelectionHelpers.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 8ecf7befe2d..eb75797d994 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -1346,8 +1346,8 @@ function $handleArrowKey( event.shiftKey && (direction === 'up' || direction === 'down') ) { - const anchorNode = selection.anchor.getNode(); - if ($isRootOrShadowRoot(anchorNode)) { + const focusNode = selection.focus.getNode(); + if ($isRootOrShadowRoot(focusNode)) { const selectedNode = selection.getNodes()[0]; if (selectedNode) { const tableCellNode = $findMatchingParent( @@ -1387,17 +1387,17 @@ function $handleArrowKey( } return false; } else { - const anchorParentNode = $findMatchingParent( - anchorNode, + const focusParentNode = $findMatchingParent( + focusNode, (n) => $isElementNode(n) && !n.isInline(), ); - if (!anchorParentNode) { + if (!focusParentNode) { return false; } const sibling = direction === 'down' - ? anchorParentNode.getNextSibling() - : anchorParentNode.getPreviousSibling(); + ? focusParentNode.getNextSibling() + : focusParentNode.getPreviousSibling(); if ( $isTableNode(sibling) && tableObserver.tableNodeKey === sibling.getKey() From 2e26b744ce2ccaa5c43a6db0c1861ce574632075 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 12 Jul 2024 14:44:42 +0800 Subject: [PATCH 019/103] [lexical-html] Feature: support pasting empty block nodes (#6392) --- packages/lexical-html/src/index.ts | 23 ++++++++++-- .../unit/LexicalEventHelpers.test.tsx | 36 +++++++++++++++++++ .../__tests__/unit/HTMLCopyAndPaste.test.ts | 19 ---------- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 2ef1ebbdad5..858c5fdf0c2 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -31,6 +31,7 @@ import { $isTextNode, ArtificialNode__DO_NOT_USE, ElementNode, + isInlineDomNode, } from 'lexical'; /** @@ -294,9 +295,16 @@ function $createNodesFromDOM( } if (currentLexicalNode == null) { - // If it hasn't been converted to a LexicalNode, we hoist its children - // up to the same level as it. - lexicalNodes = lexicalNodes.concat(childLexicalNodes); + if (childLexicalNodes.length > 0) { + // If it hasn't been converted to a LexicalNode, we hoist its children + // up to the same level as it. + lexicalNodes = lexicalNodes.concat(childLexicalNodes); + } else { + if (isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) { + // Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes + lexicalNodes = lexicalNodes.concat($createLineBreakNode()); + } + } } else { if ($isElementNode(currentLexicalNode)) { // If the current node is a ElementNode after conversion, @@ -359,3 +367,12 @@ function $unwrapArtificalNodes( node.remove(); } } + +function isDomNodeBetweenTwoInlineNodes(node: Node): boolean { + if (node.nextSibling == null || node.previousSibling == null) { + return false; + } + return ( + isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling) + ); +} diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx index 4847d838d5b..4ab0ea03d91 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx +++ b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx @@ -678,6 +678,42 @@ describe('LexicalEventHelpers', () => { ], name: 'two lines and br in spans', }, + { + expectedHTML: + '
                    1. 1
                      2

                    2. 3
                    ', + inputs: [ + pasteHTML('
                    1. 1
                      2
                    2. 3
                    '), + ], + name: 'empty block node in li behaves like a line break', + }, + { + expectedHTML: + '

                    1
                    2

                    ', + inputs: [pasteHTML('
                    1
                    2
                    ')], + name: 'empty block node in div behaves like a line break', + }, + { + expectedHTML: + '

                    12

                    ', + inputs: [pasteHTML('
                    12
                    ')], + name: 'empty inline node does not behave like a line break', + }, + { + expectedHTML: + '

                    1

                    2

                    ', + inputs: [pasteHTML('
                    1
                    2
                    ')], + name: 'empty block node between non inline siblings does not behave like a line break', + }, + { + expectedHTML: + '

                    a

                    b b

                    c

                    z

                    d e

                    fg

                    ', + inputs: [ + pasteHTML( + `
                    a
                    b b
                    c
                    z
                    d e
                    fg
                    `, + ), + ], + name: 'nested divs', + }, ]; suite.forEach((testUnit, i) => { diff --git a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts index 014681abf43..b146548383c 100644 --- a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts +++ b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts @@ -51,25 +51,6 @@ describe('HTMLCopyAndPaste tests', () => { 456
                    `, }, - { - expectedHTML: `

                    a

                    b b

                    c

                    z

                    d e

                    fg

                    `, - name: 'nested divs', - pastedHTML: `
                    - a -
                    - b b -
                    - c -
                    -
                    - z -
                    -
                    - d e -
                    - fg -
                    `, - }, { expectedHTML: `

                    a b c d e

                    f g h

                    `, name: 'multiple nested spans and divs', From e6099f301ac158387d1d0baadfb8de1dbfff06c0 Mon Sep 17 00:00:00 2001 From: Sherry Date: Sat, 13 Jul 2024 09:50:00 +0800 Subject: [PATCH 020/103] [lexical] Bug Fix: more accurate line break pasting (#6395) --- .../unit/LexicalEventHelpers.test.tsx | 12 +++++++ .../lexical/src/nodes/LexicalLineBreakNode.ts | 32 ++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx index 4ab0ea03d91..ec1436e3a21 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx +++ b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx @@ -714,6 +714,18 @@ describe('LexicalEventHelpers', () => { ], name: 'nested divs', }, + { + expectedHTML: + '
                    1. 1

                    2. 3
                    ', + inputs: [pasteHTML('
                    1. 1

                    2. 3
                    ')], + name: 'only br in a li', + }, + { + expectedHTML: + '

                    1

                    2

                    3

                    ', + inputs: [pasteHTML('1

                    2

                    3')], + name: 'last br in a block node is ignored', + }, ]; suite.forEach((testUnit, i) => { diff --git a/packages/lexical/src/nodes/LexicalLineBreakNode.ts b/packages/lexical/src/nodes/LexicalLineBreakNode.ts index dbf3f94a724..2d28db08c12 100644 --- a/packages/lexical/src/nodes/LexicalLineBreakNode.ts +++ b/packages/lexical/src/nodes/LexicalLineBreakNode.ts @@ -16,7 +16,7 @@ import type { import {DOM_TEXT_TYPE} from '../LexicalConstants'; import {LexicalNode} from '../LexicalNode'; -import {$applyNodeReplacement} from '../LexicalUtils'; +import {$applyNodeReplacement, isBlockDomNode} from '../LexicalUtils'; export type SerializedLineBreakNode = SerializedLexicalNode; @@ -50,7 +50,7 @@ export class LineBreakNode extends LexicalNode { static importDOM(): DOMConversionMap | null { return { br: (node: Node) => { - if (isOnlyChildInParagraph(node)) { + if (isOnlyChildInBlockNode(node) || isLastChildInBlockNode(node)) { return null; } return { @@ -89,9 +89,9 @@ export function $isLineBreakNode( return node instanceof LineBreakNode; } -function isOnlyChildInParagraph(node: Node): boolean { +function isOnlyChildInBlockNode(node: Node): boolean { const parentElement = node.parentElement; - if (parentElement !== null && parentElement.tagName === 'P') { + if (parentElement !== null && isBlockDomNode(parentElement)) { const firstChild = parentElement.firstChild!; if ( firstChild === node || @@ -110,6 +110,30 @@ function isOnlyChildInParagraph(node: Node): boolean { return false; } +function isLastChildInBlockNode(node: Node): boolean { + const parentElement = node.parentElement; + if (parentElement !== null && isBlockDomNode(parentElement)) { + // check if node is first child, because only childs dont count + const firstChild = parentElement.firstChild!; + if ( + firstChild === node || + (firstChild.nextSibling === node && isWhitespaceDomTextNode(firstChild)) + ) { + return false; + } + + // check if its last child + const lastChild = parentElement.lastChild!; + if ( + lastChild === node || + (lastChild.previousSibling === node && isWhitespaceDomTextNode(lastChild)) + ) { + return true; + } + } + return false; +} + function isWhitespaceDomTextNode(node: Node): boolean { return ( node.nodeType === DOM_TEXT_TYPE && From 154c5d974b71314133cd2a72bf6d07b941ce303a Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 14 Jul 2024 13:47:27 -0700 Subject: [PATCH 021/103] [lexical] Feature: Implement Editor.read and EditorState.read with editor argument (#6347) --- README.md | 26 +++- packages/lexical-website/docs/intro.md | 26 +++- packages/lexical/flow/Lexical.js.flow | 9 +- packages/lexical/src/LexicalEditor.ts | 15 +- packages/lexical/src/LexicalEditorState.ts | 14 +- packages/lexical/src/LexicalUpdates.ts | 7 +- .../src/__tests__/unit/LexicalEditor.test.tsx | 129 +++++++++++++++++- .../__tests__/unit/LexicalEditorState.test.ts | 7 + packages/lexical/src/index.ts | 6 +- 9 files changed, 215 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3ed6cbcc7de..26fd8abc127 100644 --- a/README.md +++ b/README.md @@ -153,11 +153,11 @@ Editor States are also fully serializable to JSON and can easily be serialized b ### Reading and Updating Editor State When you want to read and/or update the Lexical node tree, you must do it via `editor.update(() => {...})`. You may also do -read-only operations with the editor state via `editor.getEditorState().read(() => {...})`. The closure passed to the update or read -call is important, and must be synchronous. It's the only place where you have full "lexical" context of the active editor state, -and providing you with access to the Editor State's node tree. We promote using the convention of using `$` prefixed functions -(such as `$getRoot()`) to convey that these functions must be called in this context. Attempting to use them outside of a read -or update will trigger a runtime error. +read-only operations with the editor state via `editor.read(() => {...})` or `editor.getEditorState().read(() => {...})`. +The closure passed to the update or read call is important, and must be synchronous. It's the only place where you have full +"lexical" context of the active editor state, and providing you with access to the Editor State's node tree. We promote using +the convention of using `$` prefixed functions (such as `$getRoot()`) to convey that these functions must be called in this +context. Attempting to use them outside of a read or update will trigger a runtime error. For those familiar with React Hooks, you can think of these $functions as having similar functionality: | *Feature* | React Hooks | Lexical $functions | @@ -170,8 +170,10 @@ For those familiar with React Hooks, you can think of these $functions as having Node Transforms and Command Listeners are called with an implicit `editor.update(() => {...})` context. -It is permitted to do nested updates within reads and updates, but an update may not be nested in a read. -For example, `editor.update(() => editor.update(() => {...}))` is allowed. +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 +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 and access properties of a Lexical Node while in a read or update call (just like `$` functions). Methods @@ -186,6 +188,16 @@ first call `node.getWritable()`, which will create a writable clone of a frozen mean that any existing references (such as local variables) would refer to a stale version of the node, but having Lexical Nodes always refer to the editor state allows for a simpler and less error-prone data model. +:::tip + +If you use `editor.read(() => { /* callback */ })` it will first flush any pending updates, so you will +always see a consistent state. When you are in an `editor.update`, you will always be working with the +pending state, where node transforms and DOM reconciliation may not have run yet. +`editor.getEditorState().read()` will use the latest reconciled `EditorState` (after any node transforms, +DOM reconciliation, etc. have already run), any pending `editor.update` mutations will not yet be visible. + +::: + ### DOM Reconciler Lexical has its own DOM reconciler that takes a set of Editor States (always the "current" and the "pending") and applies a "diff" diff --git a/packages/lexical-website/docs/intro.md b/packages/lexical-website/docs/intro.md index 73174077bb3..823bef0e564 100644 --- a/packages/lexical-website/docs/intro.md +++ b/packages/lexical-website/docs/intro.md @@ -62,11 +62,11 @@ Editor States are also fully serializable to JSON and can easily be serialized b ### Reading and Updating Editor State When you want to read and/or update the Lexical node tree, you must do it via `editor.update(() => {...})`. You may also do -read-only operations with the editor state via `editor.getEditorState().read(() => {...})`. The closure passed to the update or read -call is important, and must be synchronous. It's the only place where you have full "lexical" context of the active editor state, -and providing you with access to the Editor State's node tree. We promote using the convention of using `$` prefixed functions -(such as `$getRoot()`) to convey that these functions must be called in this context. Attempting to use them outside of a read -or update will trigger a runtime error. +read-only operations with the editor state via `editor.read(() => {...})` or `editor.getEditorState().read(() => {...})`. +The closure passed to the update or read call is important, and must be synchronous. It's the only place where you have full +"lexical" context of the active editor state, and providing you with access to the Editor State's node tree. We promote using +the convention of using `$` prefixed functions (such as `$getRoot()`) to convey that these functions must be called in this +context. Attempting to use them outside of a read or update will trigger a runtime error. For those familiar with React Hooks, you can think of these $functions as having similar functionality: | *Feature* | React Hooks | Lexical $functions | @@ -79,8 +79,10 @@ For those familiar with React Hooks, you can think of these $functions as having Node Transforms and Command Listeners are called with an implicit `editor.update(() => {...})` context. -It is permitted to do nest updates within reads and updates, but an update may not be nested in a read. -For example, `editor.update(() => editor.update(() => {...}))` is allowed. +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 +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 and access properties of a Lexical Node while in a read or update call (just like `$` functions). Methods @@ -95,6 +97,16 @@ first call `node.getWritable()`, which will create a writable clone of a frozen mean that any existing references (such as local variables) would refer to a stale version of the node, but having Lexical Nodes always refer to the editor state allows for a simpler and less error-prone data model. +:::tip + +If you use `editor.read(() => { /* callback */ })` it will first flush any pending updates, so you will +always see a consistent state. When you are in an `editor.update`, you will always be working with the +pending state, where node transforms and DOM reconciliation may not have run yet. +`editor.getEditorState().read()` will use the latest reconciled `EditorState` (after any node transforms, +DOM reconciliation, etc. have already run), any pending `editor.update` mutations will not yet be visible. + +::: + ### DOM Reconciler Lexical has its own DOM reconciler that takes a set of Editor States (always the "current" and the "pending") and applies a "diff" diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 3a31a243567..916a2dc7dc6 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -198,6 +198,7 @@ declare export class LexicalEditor { maybeStringifiedEditorState: string | SerializedEditorState, updateFn?: () => void, ): EditorState; + read(callbackFn: () => V, options?: EditorReadOptions): V; update(updateFn: () => void, options?: EditorUpdateOptions): boolean; focus(callbackFn?: () => void, options?: EditorFocusOptions): void; blur(): void; @@ -205,6 +206,9 @@ declare export class LexicalEditor { setEditable(editable: boolean): void; toJSON(): SerializedEditor; } +type EditorReadOptions = { + pending?: boolean, +}; type EditorUpdateOptions = { onUpdate?: () => void, tag?: string, @@ -324,10 +328,13 @@ export interface EditorState { _readOnly: boolean; constructor(nodeMap: NodeMap, selection?: BaseSelection | null): void; isEmpty(): boolean; - read(callbackFn: () => V): V; + read(callbackFn: () => V, options?: EditorStateReadOptions): V; toJSON(): SerializedEditorState; clone(selection?: BaseSelection | null): EditorState; } +type EditorStateReadOptions = { + editor?: LexicalEditor | null; +} /** * LexicalNode diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index b5b93567aec..29cd66b7598 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -1096,7 +1096,7 @@ export class LexicalEditor { /** * Parses a SerializedEditorState (usually produced by {@link EditorState.toJSON}) and returns * and EditorState object that can be, for example, passed to {@link LexicalEditor.setEditorState}. Typically, - * deserliazation from JSON stored in a database uses this method. + * deserialization from JSON stored in a database uses this method. * @param maybeStringifiedEditorState * @param updateFn * @returns @@ -1112,6 +1112,19 @@ export class LexicalEditor { return parseEditorState(serializedEditorState, this, updateFn); } + /** + * Executes a read of the editor's state, with the + * editor context available (useful for exporting and read-only DOM + * operations). Much like update, but prevents any mutation of the + * editor's state. Any pending updates will be flushed immediately before + * the read. + * @param callbackFn - A function that has access to read-only editor state. + */ + read(callbackFn: () => T): T { + $commitPendingUpdates(this); + return this.getEditorState().read(callbackFn, {editor: this}); + } + /** * Executes an update to the editor state. The updateFn callback is the ONLY place * where Lexical editor state can be safely mutated. diff --git a/packages/lexical/src/LexicalEditorState.ts b/packages/lexical/src/LexicalEditorState.ts index 19bc5fe5c2d..aa14f45d87b 100644 --- a/packages/lexical/src/LexicalEditorState.ts +++ b/packages/lexical/src/LexicalEditorState.ts @@ -91,6 +91,10 @@ function exportNodeToJSON( return serializedNode; } +export interface EditorStateReadOptions { + editor?: LexicalEditor | null; +} + export class EditorState { _nodeMap: NodeMap; _selection: null | BaseSelection; @@ -108,8 +112,12 @@ export class EditorState { return this._nodeMap.size === 1 && this._selection === null; } - read(callbackFn: () => V): V { - return readEditorState(this, callbackFn); + read(callbackFn: () => V, options?: EditorStateReadOptions): V { + return readEditorState( + (options && options.editor) || null, + this, + callbackFn, + ); } clone(selection?: null | BaseSelection): EditorState { @@ -122,7 +130,7 @@ export class EditorState { return editorState; } toJSON(): SerializedEditorState { - return readEditorState(this, () => ({ + return readEditorState(null, this, () => ({ root: exportNodeToJSON($getRoot()), })); } diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index e16368bf5c4..50479fb10a1 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -96,7 +96,7 @@ export function getActiveEditorState(): EditorState { 'Unable to find an active editor state. ' + 'State helpers or node methods can only be used ' + 'synchronously during the callback of ' + - 'editor.update() or editorState.read().', + 'editor.update(), editor.read(), or editorState.read().', ); } @@ -110,7 +110,7 @@ export function getActiveEditor(): LexicalEditor { 'Unable to find an active editor. ' + 'This method can only be used ' + 'synchronously during the callback of ' + - 'editor.update().', + 'editor.update() or editor.read().', ); } @@ -397,6 +397,7 @@ export function parseEditorState( // function here export function readEditorState( + editor: LexicalEditor | null, editorState: EditorState, callbackFn: () => V, ): V { @@ -406,7 +407,7 @@ export function readEditorState( activeEditorState = editorState; isReadOnlyMode = true; - activeEditor = null; + activeEditor = editor; try { return callbackFn(); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 860d3196051..5cc8fb8054e 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -22,8 +22,11 @@ import { $createNodeSelection, $createParagraphNode, $createTextNode, + $getEditor, + $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, + $isParagraphNode, $isTextNode, $parseSerializedNode, $setCompositionKey, @@ -113,7 +116,7 @@ describe('LexicalEditor tests', () => { let editor: LexicalEditor; - function init(onError?: () => void) { + function init(onError?: (error: Error) => void) { const ref = createRef(); function TestBase() { @@ -133,6 +136,130 @@ describe('LexicalEditor tests', () => { return Promise.resolve().then(); } + describe('read()', () => { + it('Can read the editor state', async () => { + init(function onError(err) { + throw err; + }); + expect(editor.read(() => $getRoot().getTextContent())).toEqual(''); + expect(editor.read(() => $getEditor())).toBe(editor); + const onUpdate = jest.fn(); + editor.update( + () => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const text = $createTextNode('This works!'); + root.append(paragraph); + paragraph.append(text); + }, + {onUpdate}, + ); + expect(onUpdate).toHaveBeenCalledTimes(0); + // This read will flush pending updates + expect(editor.read(() => $getRoot().getTextContent())).toEqual( + 'This works!', + ); + expect(onUpdate).toHaveBeenCalledTimes(1); + // Check to make sure there is not an unexpected reconciliation + await Promise.resolve().then(); + expect(onUpdate).toHaveBeenCalledTimes(1); + editor.read(() => { + const rootElement = editor.getRootElement(); + expect(rootElement).toBeDefined(); + // The root never works for this call + expect($getNearestNodeFromDOMNode(rootElement!)).toBe(null); + const paragraphDom = rootElement!.querySelector('p'); + expect(paragraphDom).toBeDefined(); + expect( + $isParagraphNode($getNearestNodeFromDOMNode(paragraphDom!)), + ).toBe(true); + expect( + $getNearestNodeFromDOMNode(paragraphDom!)!.getTextContent(), + ).toBe('This works!'); + const textDom = paragraphDom!.querySelector('span'); + expect(textDom).toBeDefined(); + expect($isTextNode($getNearestNodeFromDOMNode(textDom!))).toBe(true); + expect($getNearestNodeFromDOMNode(textDom!)!.getTextContent()).toBe( + 'This works!', + ); + expect( + $getNearestNodeFromDOMNode(textDom!.firstChild!)!.getTextContent(), + ).toBe('This works!'); + }); + expect(onUpdate).toHaveBeenCalledTimes(1); + }); + it('runs transforms the editor state', async () => { + init(function onError(err) { + throw err; + }); + expect(editor.read(() => $getRoot().getTextContent())).toEqual(''); + expect(editor.read(() => $getEditor())).toBe(editor); + editor.registerNodeTransform(TextNode, (node) => { + if (node.getTextContent() === 'This works!') { + node.replace($createTextNode('Transforms work!')); + } + }); + const onUpdate = jest.fn(); + editor.update( + () => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const text = $createTextNode('This works!'); + root.append(paragraph); + paragraph.append(text); + }, + {onUpdate}, + ); + expect(onUpdate).toHaveBeenCalledTimes(0); + // This read will flush pending updates + expect(editor.read(() => $getRoot().getTextContent())).toEqual( + 'Transforms work!', + ); + expect(editor.getRootElement()!.textContent).toEqual('Transforms work!'); + expect(onUpdate).toHaveBeenCalledTimes(1); + // Check to make sure there is not an unexpected reconciliation + await Promise.resolve().then(); + expect(onUpdate).toHaveBeenCalledTimes(1); + expect(editor.read(() => $getRoot().getTextContent())).toEqual( + 'Transforms work!', + ); + }); + it('can be nested in an update or read', async () => { + init(function onError(err) { + throw err; + }); + editor.update(() => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const text = $createTextNode('This works!'); + root.append(paragraph); + paragraph.append(text); + editor.read(() => { + expect($getRoot().getTextContent()).toBe('This works!'); + }); + editor.read(() => { + // Nesting update in read works, although it is discouraged in the documentation. + editor.update(() => { + expect($getRoot().getTextContent()).toBe('This works!'); + }); + }); + // Updating after a nested read will fail as it has already been committed + expect(() => { + root.append( + $createParagraphNode().append( + $createTextNode('update-read-update'), + ), + ); + }).toThrow(); + }); + editor.read(() => { + editor.read(() => { + expect($getRoot().getTextContent()).toBe('This works!'); + }); + }); + }); + }); + it('Should create an editor with an initial editor state', async () => { const rootElement = document.createElement('div'); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts b/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts index 9dac096e159..021a968b67d 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts @@ -9,6 +9,7 @@ import { $createParagraphNode, $createTextNode, + $getEditor, $getRoot, ParagraphNode, TextNode, @@ -89,6 +90,12 @@ describe('LexicalEditorState tests', () => { __text: 'foo', __type: 'text', }); + expect(() => editor.getEditorState().read(() => $getEditor())).toThrow( + /Unable to find an active editor/, + ); + expect( + editor.getEditorState().read(() => $getEditor(), {editor: editor}), + ).toBe(editor); }); test('toJSON()', async () => { diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index b76ab832332..bf5e51bcb84 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -30,7 +30,11 @@ export type { Spread, Transform, } from './LexicalEditor'; -export type {EditorState, SerializedEditorState} from './LexicalEditorState'; +export type { + EditorState, + EditorStateReadOptions, + SerializedEditorState, +} from './LexicalEditorState'; export type { DOMChildConversion, DOMConversion, From 5ec347674cca8a5a294a2aeb27dad6211f5f0c2e Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 14 Jul 2024 16:43:30 -0700 Subject: [PATCH 022/103] [lexical] Feature: registerMutationListener should initialize its existing nodes (#6357) --- packages/lexical-code/src/CodeHighlighter.ts | 24 ++- .../plugins/CodeActionMenuPlugin/index.tsx | 46 +++-- .../src/plugins/CommentPlugin/index.tsx | 66 +++---- .../plugins/TableActionMenuPlugin/index.tsx | 26 +-- .../plugins/TableHoverActionsPlugin/index.tsx | 46 ++--- .../src/LexicalAutoEmbedPlugin.tsx | 31 ++-- .../src/LexicalTableOfContentsPlugin.tsx | 4 + .../lexical-react/src/LexicalTablePlugin.ts | 13 +- .../docs/concepts/dom-events.md | 14 +- .../docs/concepts/listeners.md | 8 +- packages/lexical/flow/Lexical.js.flow | 4 + packages/lexical/src/LexicalEditor.ts | 111 +++++++---- packages/lexical/src/LexicalUtils.ts | 42 ++++- .../src/__tests__/unit/LexicalEditor.test.tsx | 173 ++++++++++++++++-- .../src/__tests__/unit/LexicalUtils.test.ts | 54 ++++++ 15 files changed, 479 insertions(+), 183 deletions(-) diff --git a/packages/lexical-code/src/CodeHighlighter.ts b/packages/lexical-code/src/CodeHighlighter.ts index 329e2ed19a2..e4a4f626f28 100644 --- a/packages/lexical-code/src/CodeHighlighter.ts +++ b/packages/lexical-code/src/CodeHighlighter.ts @@ -818,18 +818,22 @@ export function registerCodeHighlighting( } return mergeRegister( - editor.registerMutationListener(CodeNode, (mutations) => { - editor.update(() => { - for (const [key, type] of mutations) { - if (type !== 'destroyed') { - const node = $getNodeByKey(key); - if (node !== null) { - updateCodeGutter(node as CodeNode, editor); + editor.registerMutationListener( + CodeNode, + (mutations) => { + editor.update(() => { + for (const [key, type] of mutations) { + if (type !== 'destroyed') { + const node = $getNodeByKey(key); + if (node !== null) { + updateCodeGutter(node as CodeNode, editor); + } } } - } - }); - }), + }); + }, + {skipInitialization: false}, + ), editor.registerNodeTransform(CodeNode, (node) => codeNodeTransform(node, editor, tokenizer as Tokenizer), ), diff --git a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx index 53809f04cda..b200b279e69 100644 --- a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx @@ -109,26 +109,32 @@ function CodeActionMenuContainer({ }; }, [shouldListenMouseMove, debouncedOnMouseMove]); - editor.registerMutationListener(CodeNode, (mutations) => { - editor.getEditorState().read(() => { - for (const [key, type] of mutations) { - switch (type) { - case 'created': - codeSetRef.current.add(key); - setShouldListenMouseMove(codeSetRef.current.size > 0); - break; - - case 'destroyed': - codeSetRef.current.delete(key); - setShouldListenMouseMove(codeSetRef.current.size > 0); - break; - - default: - break; - } - } - }); - }); + useEffect(() => { + return editor.registerMutationListener( + CodeNode, + (mutations) => { + editor.getEditorState().read(() => { + for (const [key, type] of mutations) { + switch (type) { + case 'created': + codeSetRef.current.add(key); + break; + + case 'destroyed': + codeSetRef.current.delete(key); + break; + + default: + break; + } + } + }); + setShouldListenMouseMove(codeSetRef.current.size > 0); + }, + {skipInitialization: false}, + ); + }, [editor]); + const normalizedLang = normalizeCodeLang(lang); const codeFriendlyName = getLanguageFriendlyName(lang); diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx index 66915aacf01..1fc15288d41 100644 --- a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx @@ -840,43 +840,47 @@ export default function CommentPlugin({ }); }, ), - editor.registerMutationListener(MarkNode, (mutations) => { - editor.getEditorState().read(() => { - for (const [key, mutation] of mutations) { - const node: null | MarkNode = $getNodeByKey(key); - let ids: NodeKey[] = []; - - if (mutation === 'destroyed') { - ids = markNodeKeysToIDs.get(key) || []; - } else if ($isMarkNode(node)) { - ids = node.getIDs(); - } - - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - let markNodeKeys = markNodeMap.get(id); - markNodeKeysToIDs.set(key, ids); + editor.registerMutationListener( + MarkNode, + (mutations) => { + editor.getEditorState().read(() => { + for (const [key, mutation] of mutations) { + const node: null | MarkNode = $getNodeByKey(key); + let ids: NodeKey[] = []; if (mutation === 'destroyed') { - if (markNodeKeys !== undefined) { - markNodeKeys.delete(key); - if (markNodeKeys.size === 0) { - markNodeMap.delete(id); + ids = markNodeKeysToIDs.get(key) || []; + } else if ($isMarkNode(node)) { + ids = node.getIDs(); + } + + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + let markNodeKeys = markNodeMap.get(id); + markNodeKeysToIDs.set(key, ids); + + if (mutation === 'destroyed') { + if (markNodeKeys !== undefined) { + markNodeKeys.delete(key); + if (markNodeKeys.size === 0) { + markNodeMap.delete(id); + } + } + } else { + if (markNodeKeys === undefined) { + markNodeKeys = new Set(); + markNodeMap.set(id, markNodeKeys); + } + if (!markNodeKeys.has(key)) { + markNodeKeys.add(key); } - } - } else { - if (markNodeKeys === undefined) { - markNodeKeys = new Set(); - markNodeMap.set(id, markNodeKeys); - } - if (!markNodeKeys.has(key)) { - markNodeKeys.add(key); } } } - } - }); - }), + }); + }, + {skipInitialization: false}, + ), editor.registerUpdateListener(({editorState, tags}) => { editorState.read(() => { const selection = $getSelection(); diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 79a1d150590..eb72fd9d7dc 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -183,17 +183,21 @@ function TableActionMenu({ ); useEffect(() => { - return editor.registerMutationListener(TableCellNode, (nodeMutations) => { - const nodeUpdated = - nodeMutations.get(tableCellNode.getKey()) === 'updated'; - - if (nodeUpdated) { - editor.getEditorState().read(() => { - updateTableCellNode(tableCellNode.getLatest()); - }); - setBackgroundColor(currentCellBackgroundColor(editor) || ''); - } - }); + return editor.registerMutationListener( + TableCellNode, + (nodeMutations) => { + const nodeUpdated = + nodeMutations.get(tableCellNode.getKey()) === 'updated'; + + if (nodeUpdated) { + editor.getEditorState().read(() => { + updateTableCellNode(tableCellNode.getLatest()); + }); + setBackgroundColor(currentCellBackgroundColor(editor) || ''); + } + }, + {skipInitialization: true}, + ); }, [editor, tableCellNode]); useEffect(() => { diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx index f53b43ec785..e7f186bc57c 100644 --- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -19,7 +19,7 @@ import { TableRowNode, } from '@lexical/table'; import {$findMatchingParent, mergeRegister} from '@lexical/utils'; -import {$getNearestNodeFromDOMNode} from 'lexical'; +import {$getNearestNodeFromDOMNode, NodeKey} from 'lexical'; import {useEffect, useRef, useState} from 'react'; import * as React from 'react'; import {createPortal} from 'react-dom'; @@ -39,7 +39,7 @@ function TableHoverActionsContainer({ const [shouldListenMouseMove, setShouldListenMouseMove] = useState(false); const [position, setPosition] = useState({}); - const codeSetRef = useRef>(new Set()); + const codeSetRef = useRef>(new Set()); const tableDOMNodeRef = useRef(null); const debouncedOnMouseMove = useDebounce( @@ -148,26 +148,30 @@ function TableHoverActionsContainer({ useEffect(() => { return mergeRegister( - editor.registerMutationListener(TableNode, (mutations) => { - editor.getEditorState().read(() => { - for (const [key, type] of mutations) { - switch (type) { - case 'created': - codeSetRef.current.add(key); - setShouldListenMouseMove(codeSetRef.current.size > 0); - break; - - case 'destroyed': - codeSetRef.current.delete(key); - setShouldListenMouseMove(codeSetRef.current.size > 0); - break; - - default: - break; + editor.registerMutationListener( + TableNode, + (mutations) => { + editor.getEditorState().read(() => { + for (const [key, type] of mutations) { + switch (type) { + case 'created': + codeSetRef.current.add(key); + setShouldListenMouseMove(codeSetRef.current.size > 0); + break; + + case 'destroyed': + codeSetRef.current.delete(key); + setShouldListenMouseMove(codeSetRef.current.size > 0); + break; + + default: + break; + } } - } - }); - }), + }); + }, + {skipInitialization: false}, + ), ); }, [editor]); diff --git a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx index 2879d1bafff..49ebbb1a270 100644 --- a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx +++ b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx @@ -105,24 +105,23 @@ export function LexicalAutoEmbedPlugin({ }, []); const checkIfLinkNodeIsEmbeddable = useCallback( - (key: NodeKey) => { - editor.getEditorState().read(async function () { + async (key: NodeKey) => { + const url = editor.getEditorState().read(function () { const linkNode = $getNodeByKey(key); if ($isLinkNode(linkNode)) { - for (let i = 0; i < embedConfigs.length; i++) { - const embedConfig = embedConfigs[i]; - - const urlMatch = await Promise.resolve( - embedConfig.parseUrl(linkNode.__url), - ); - - if (urlMatch != null) { - setActiveEmbedConfig(embedConfig); - setNodeKey(linkNode.getKey()); - } - } + return linkNode.getURL(); } }); + if (url === undefined) { + return; + } + for (const embedConfig of embedConfigs) { + const urlMatch = await Promise.resolve(embedConfig.parseUrl(url)); + if (urlMatch != null) { + setActiveEmbedConfig(embedConfig); + setNodeKey(key); + } + } }, [editor, embedConfigs], ); @@ -146,7 +145,9 @@ export function LexicalAutoEmbedPlugin({ }; return mergeRegister( ...[LinkNode, AutoLinkNode].map((Klass) => - editor.registerMutationListener(Klass, (...args) => listener(...args)), + editor.registerMutationListener(Klass, (...args) => listener(...args), { + skipInitialization: true, + }), ), ); }, [checkIfLinkNodeIsEmbeddable, editor, embedConfigs, nodeKey, reset]); diff --git a/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx b/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx index ceaa1da772f..86f280e0d44 100644 --- a/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx +++ b/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx @@ -234,6 +234,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element { setTableOfContents(currentTableOfContents); }); }, + // Initialization is handled separately + {skipInitialization: true}, ); // Listen to text node mutation updates @@ -258,6 +260,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element { } }); }, + // Initialization is handled separately + {skipInitialization: true}, ); return () => { diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts index e237e67b339..2ae1dfd94f7 100644 --- a/packages/lexical-react/src/LexicalTablePlugin.ts +++ b/packages/lexical-react/src/LexicalTablePlugin.ts @@ -38,7 +38,6 @@ import { $createParagraphNode, $getNodeByKey, $isTextNode, - $nodesOfType, COMMAND_PRIORITY_EDITOR, } from 'lexical'; import {useEffect} from 'react'; @@ -129,17 +128,6 @@ export function TablePlugin({ } }; - // Plugins might be loaded _after_ initial content is set, hence existing table nodes - // won't be initialized from mutation[create] listener. Instead doing it here, - editor.getEditorState().read(() => { - const tableNodes = $nodesOfType(TableNode); - for (const tableNode of tableNodes) { - if ($isTableNode(tableNode)) { - initializeTableNode(tableNode); - } - } - }); - const unregisterMutationListener = editor.registerMutationListener( TableNode, (nodeMutations) => { @@ -161,6 +149,7 @@ export function TablePlugin({ } } }, + {skipInitialization: false}, ); return () => { diff --git a/packages/lexical-website/docs/concepts/dom-events.md b/packages/lexical-website/docs/concepts/dom-events.md index ad411d07a17..c45b5d7615b 100644 --- a/packages/lexical-website/docs/concepts/dom-events.md +++ b/packages/lexical-website/docs/concepts/dom-events.md @@ -30,20 +30,20 @@ This can be a simple, efficient way to handle some use cases, since it's not nec ## 2. Directly Attach Handlers -In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that you're handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners). +In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners). ```js +const registeredElements: WeakSet = new WeakSet(); const removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => { - const registeredElements: WeakSet = new WeakSet(); editor.getEditorState().read(() => { for (const [key, mutation] of mutations) { const element: null | HTMLElement = editor.getElementByKey(key); if ( - // Updated might be a move, so that might mean a new DOM element - // is created. In this case, we need to add and event listener too. - (mutation === 'created' || mutation === 'updated') && - element !== null && - !registeredElements.has(element) + // Updated might be a move, so that might mean a new DOM element + // is created. In this case, we need to add and event listener too. + (mutation === 'created' || mutation === 'updated') && + element !== null && + !registeredElements.has(element) ) { registeredElements.add(element); element.addEventListener('click', (event: Event) => { diff --git a/packages/lexical-website/docs/concepts/listeners.md b/packages/lexical-website/docs/concepts/listeners.md index ca890fbefbc..fb5036223b0 100644 --- a/packages/lexical-website/docs/concepts/listeners.md +++ b/packages/lexical-website/docs/concepts/listeners.md @@ -81,15 +81,21 @@ Get notified when a specific type of Lexical node has been mutated. There are th Mutation listeners are great for tracking the lifecycle of specific types of node. They can be used to 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.16.x but will change to false in 0.17.0). + ```js const removeMutationListener = editor.registerMutationListener( MyCustomNode, - (mutatedNodes) => { + (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => { // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation. for (let [nodeKey, mutation] of mutatedNodes) { console.log(nodeKey, mutation) } }, + {skipInitialization: false} ); // Do not forget to unregister the listener when no longer needed! diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 916a2dc7dc6..6744da5ed5d 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -109,6 +109,9 @@ export type MutationListener = ( prevEditorState: EditorState, }, ) => void; +export type MutationListenerOptions = { + skipInitialization?: boolean; +}; export type EditableListener = (editable: boolean) => void; type Listeners = { decorator: Set, @@ -178,6 +181,7 @@ declare export class LexicalEditor { registerMutationListener( klass: Class, listener: MutationListener, + options?: MutationListenerOptions, ): () => void; registerNodeTransform( klass: Class, diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 29cd66b7598..ce7a22b5992 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -33,6 +33,7 @@ import { createUID, dispatchCommand, getCachedClassNameArray, + getCachedTypeToNodeMap, getDefaultView, getDOMSelection, markAllNodesAsDirty, @@ -211,6 +212,18 @@ export type MutatedNodes = Map, Map>; export type NodeMutation = 'created' | 'updated' | 'destroyed'; +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. + */ + skipInitialization?: boolean; +} + +const DEFAULT_SKIP_INITIALIZATION = true; + export type UpdateListener = (arg0: { dirtyElements: Map; dirtyLeaves: Set; @@ -824,15 +837,43 @@ export class LexicalEditor { * One common use case for this is to attach DOM event listeners to the underlying DOM nodes as Lexical nodes are created. * {@link LexicalEditor.getElementByKey} can be used for this. * + * 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). + * * @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. + * @param options - see {@link MutationListenerOptions} * @returns a teardown function that can be used to cleanup the listener. */ registerMutationListener( klass: Klass, listener: MutationListener, + options?: MutationListenerOptions, ): () => void { - let registeredNode = this._nodes.get(klass.getType()); + const klassToMutate = this.resolveRegisteredNodeAfterReplacements( + this.getRegisteredNode(klass), + ).klass; + const mutations = this._listeners.mutation; + mutations.set(listener, klassToMutate); + const skipInitialization = options && options.skipInitialization; + if ( + !(skipInitialization === undefined + ? DEFAULT_SKIP_INITIALIZATION + : skipInitialization) + ) { + this.initializeMutationListener(listener, klassToMutate); + } + + return () => { + mutations.delete(listener); + }; + } + + /** @internal */ + private getRegisteredNode(klass: Klass): RegisteredNode { + const registeredNode = this._nodes.get(klass.getType()); if (registeredNode === undefined) { invariant( @@ -842,29 +883,42 @@ export class LexicalEditor { ); } - let klassToMutate = klass; - - let replaceKlass: Klass | null = null; - while ((replaceKlass = registeredNode.replaceWithKlass)) { - klassToMutate = replaceKlass; - - registeredNode = this._nodes.get(replaceKlass.getType()); + return registeredNode; + } - if (registeredNode === undefined) { - invariant( - false, - 'Node %s has not been registered. Ensure node has been passed to createEditor.', - replaceKlass.name, - ); - } + /** @internal */ + private resolveRegisteredNodeAfterReplacements( + registeredNode: RegisteredNode, + ): RegisteredNode { + while (registeredNode.replaceWithKlass) { + registeredNode = this.getRegisteredNode(registeredNode.replaceWithKlass); } + return registeredNode; + } - const mutations = this._listeners.mutation; - mutations.set(listener, klassToMutate); - - return () => { - mutations.delete(listener); - }; + /** @internal */ + private initializeMutationListener( + listener: MutationListener, + klass: Klass, + ): void { + const prevEditorState = this._editorState; + const nodeMap = getCachedTypeToNodeMap(this._editorState).get( + klass.getType(), + ); + if (!nodeMap) { + return; + } + const nodeMutationMap = new Map(); + for (const k of nodeMap.keys()) { + nodeMutationMap.set(k, 'created'); + } + if (nodeMutationMap.size > 0) { + listener(nodeMutationMap, { + dirtyLeaves: new Set(), + prevEditorState, + updateTags: new Set(['registerMutationListener']), + }); + } } /** @internal */ @@ -872,19 +926,8 @@ export class LexicalEditor { klass: Klass, listener: Transform, ): RegisteredNode { - const type = klass.getType(); - - const registeredNode = this._nodes.get(type); - - if (registeredNode === undefined) { - invariant( - false, - 'Node %s has not been registered. Ensure node has been passed to createEditor.', - klass.name, - ); - } - const transforms = registeredNode.transforms; - transforms.add(listener as Transform); + const registeredNode = this.getRegisteredNode(klass); + registeredNode.transforms.add(listener as Transform); return registeredNode; } diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index eda211b64ed..8cc3099462d 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1118,16 +1118,21 @@ export function setMutatedNode( } export function $nodesOfType(klass: Klass): Array { - const editorState = getActiveEditorState(); - const readOnly = editorState._readOnly; const klassType = klass.getType(); + const editorState = getActiveEditorState(); + if (editorState._readOnly) { + const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as + | undefined + | Map; + return nodes ? [...nodes.values()] : []; + } const nodes = editorState._nodeMap; const nodesOfType: Array = []; for (const [, node] of nodes) { if ( node instanceof klass && node.__type === klassType && - (readOnly || node.isAttached()) + node.isAttached() ) { nodesOfType.push(node as T); } @@ -1691,3 +1696,34 @@ export function $getAncestor( export function $getEditor(): LexicalEditor { return getActiveEditor(); } + +/** @internal */ +export type TypeToNodeMap = Map; +/** + * @internal + * Compute a cached Map of node type to nodes for a frozen EditorState + */ +const cachedNodeMaps = new WeakMap(); +export function getCachedTypeToNodeMap( + editorState: EditorState, +): TypeToNodeMap { + invariant( + editorState._readOnly, + 'getCachedTypeToNodeMap called with a writable EditorState', + ); + let typeToNodeMap = cachedNodeMaps.get(editorState); + if (!typeToNodeMap) { + typeToNodeMap = new Map(); + cachedNodeMaps.set(editorState, typeToNodeMap); + for (const [nodeKey, node] of editorState._nodeMap) { + const nodeType = node.__type; + let nodeMap = typeToNodeMap.get(nodeType); + if (!nodeMap) { + nodeMap = new Map(); + typeToNodeMap.set(nodeType, nodeMap); + } + nodeMap.set(nodeKey, node); + } + } + return typeToNodeMap; +} diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 5cc8fb8054e..1f069be7194 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -1713,8 +1713,12 @@ describe('LexicalEditor tests', () => { const paragraphNodeMutations = jest.fn(); const textNodeMutations = jest.fn(); - editor.registerMutationListener(ParagraphNode, paragraphNodeMutations); - editor.registerMutationListener(TextNode, textNodeMutations); + editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, { + skipInitialization: false, + }); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); const paragraphKeys: string[] = []; const textNodeKeys: string[] = []; @@ -1786,7 +1790,9 @@ describe('LexicalEditor tests', () => { const initialEditorState = editor.getEditorState(); const textNodeMutations = jest.fn(); - editor.registerMutationListener(TextNode, textNodeMutations); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); const textNodeKeys: string[] = []; await editor.update(() => { @@ -1856,7 +1862,10 @@ describe('LexicalEditor tests', () => { }); const textNodeMutations = jest.fn(); - editor.registerMutationListener(TextNode, textNodeMutations); + const textNodeMutationsB = jest.fn(); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); const textNodeKeys: string[] = []; // No await intentional (batch with next) @@ -1879,6 +1888,10 @@ describe('LexicalEditor tests', () => { textNodeKeys.push(textNode3.getKey()); }); + editor.registerMutationListener(TextNode, textNodeMutationsB, { + skipInitialization: false, + }); + await editor.update(() => { $getRoot().clear(); }); @@ -1893,6 +1906,7 @@ describe('LexicalEditor tests', () => { }); expect(textNodeMutations.mock.calls.length).toBe(2); + expect(textNodeMutationsB.mock.calls.length).toBe(2); const [textNodeMutation1, textNodeMutation2] = textNodeMutations.mock.calls; @@ -1900,10 +1914,28 @@ describe('LexicalEditor tests', () => { expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created'); expect(textNodeMutation1[0].get(textNodeKeys[1])).toBe('created'); expect(textNodeMutation1[0].get(textNodeKeys[2])).toBe('created'); + expect([...textNodeMutation1[1].updateTags]).toEqual([]); expect(textNodeMutation2[0].size).toBe(3); expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('destroyed'); expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('destroyed'); expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('destroyed'); + expect([...textNodeMutation2[1].updateTags]).toEqual([]); + + const [textNodeMutationB1, textNodeMutationB2] = + textNodeMutationsB.mock.calls; + + expect(textNodeMutationB1[0].size).toBe(3); + expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created'); + expect(textNodeMutationB1[0].get(textNodeKeys[1])).toBe('created'); + expect(textNodeMutationB1[0].get(textNodeKeys[2])).toBe('created'); + expect([...textNodeMutationB1[1].updateTags]).toEqual([ + 'registerMutationListener', + ]); + expect(textNodeMutationB2[0].size).toBe(3); + expect(textNodeMutationB2[0].get(textNodeKeys[0])).toBe('destroyed'); + expect(textNodeMutationB2[0].get(textNodeKeys[1])).toBe('destroyed'); + expect(textNodeMutationB2[0].get(textNodeKeys[2])).toBe('destroyed'); + expect([...textNodeMutationB2[1].updateTags]).toEqual([]); }); it('mutation listener should work with the replaced node', async () => { @@ -1927,10 +1959,12 @@ describe('LexicalEditor tests', () => { }); const textNodeMutations = jest.fn(); - editor.registerMutationListener(TestTextNode, textNodeMutations); + const textNodeMutationsB = jest.fn(); + editor.registerMutationListener(TestTextNode, textNodeMutations, { + skipInitialization: false, + }); const textNodeKeys: string[] = []; - // No await intentional (batch with next) await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); @@ -1940,12 +1974,25 @@ describe('LexicalEditor tests', () => { textNodeKeys.push(textNode.getKey()); }); + editor.registerMutationListener(TestTextNode, textNodeMutationsB, { + skipInitialization: false, + }); + expect(textNodeMutations.mock.calls.length).toBe(1); const [textNodeMutation1] = textNodeMutations.mock.calls; expect(textNodeMutation1[0].size).toBe(1); expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created'); + expect([...textNodeMutation1[1].updateTags]).toEqual([]); + + const [textNodeMutationB1] = textNodeMutationsB.mock.calls; + + expect(textNodeMutationB1[0].size).toBe(1); + expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created'); + expect([...textNodeMutationB1[1].updateTags]).toEqual([ + 'registerMutationListener', + ]); }); it('mutation listeners does not trigger when other node types are mutated', async () => { @@ -1953,8 +2000,12 @@ describe('LexicalEditor tests', () => { const paragraphNodeMutations = jest.fn(); const textNodeMutations = jest.fn(); - editor.registerMutationListener(ParagraphNode, paragraphNodeMutations); - editor.registerMutationListener(TextNode, textNodeMutations); + editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, { + skipInitialization: false, + }); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); await editor.update(() => { $getRoot().append($createParagraphNode()); @@ -1968,7 +2019,9 @@ describe('LexicalEditor tests', () => { init(); const textNodeMutations = jest.fn(); - editor.registerMutationListener(TextNode, textNodeMutations); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); const textNodeKeys: string[] = []; await editor.update(() => { @@ -2014,8 +2067,12 @@ describe('LexicalEditor tests', () => { const paragraphNodeMutations = jest.fn(); const textNodeMutations = jest.fn(); - editor.registerMutationListener(ParagraphNode, paragraphNodeMutations); - editor.registerMutationListener(TextNode, textNodeMutations); + editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, { + skipInitialization: false, + }); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); const paragraphNodeKeys: string[] = []; const textNodeKeys: string[] = []; @@ -2074,8 +2131,12 @@ describe('LexicalEditor tests', () => { const tableCellMutations = jest.fn(); const tableRowMutations = jest.fn(); - editor.registerMutationListener(TableCellNode, tableCellMutations); - editor.registerMutationListener(TableRowNode, tableRowMutations); + editor.registerMutationListener(TableCellNode, tableCellMutations, { + skipInitialization: false, + }); + editor.registerMutationListener(TableRowNode, tableRowMutations, { + skipInitialization: false, + }); // Create Table await editor.update(() => { @@ -2154,12 +2215,20 @@ describe('LexicalEditor tests', () => { }); }); - editor.registerMutationListener(TextNode, (map) => { - mutationListener(); - editor.registerMutationListener(TextNode, () => { + editor.registerMutationListener( + TextNode, + (map) => { mutationListener(); - }); - }); + editor.registerMutationListener( + TextNode, + () => { + mutationListener(); + }, + {skipInitialization: true}, + ); + }, + {skipInitialization: false}, + ); editor.registerNodeTransform(ParagraphNode, () => { nodeTransformListener(); @@ -2214,6 +2283,74 @@ describe('LexicalEditor tests', () => { expect(mutationListener).toHaveBeenCalledTimes(1); }); + it('calls mutation listener with initial state', async () => { + // TODO add tests for node replacement + const mutationListenerA = jest.fn(); + const mutationListenerB = jest.fn(); + const mutationListenerC = jest.fn(); + init(); + + editor.registerMutationListener(TextNode, mutationListenerA, { + skipInitialization: false, + }); + expect(mutationListenerA).toHaveBeenCalledTimes(0); + + await update(() => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Hello world')), + ); + }); + + function asymmetricMatcher(asymmetricMatch: (x: T) => boolean) { + return {asymmetricMatch}; + } + + expect(mutationListenerA).toHaveBeenCalledTimes(1); + expect(mutationListenerA).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + updateTags: asymmetricMatcher( + (s: Set) => !s.has('registerMutationListener'), + ), + }), + ); + editor.registerMutationListener(TextNode, mutationListenerB, { + skipInitialization: false, + }); + editor.registerMutationListener(TextNode, mutationListenerC, { + skipInitialization: true, + }); + expect(mutationListenerA).toHaveBeenCalledTimes(1); + expect(mutationListenerB).toHaveBeenCalledTimes(1); + expect(mutationListenerB).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + updateTags: asymmetricMatcher((s: Set) => + s.has('registerMutationListener'), + ), + }), + ); + expect(mutationListenerC).toHaveBeenCalledTimes(0); + await update(() => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Another update!')), + ); + }); + expect(mutationListenerA).toHaveBeenCalledTimes(2); + expect(mutationListenerB).toHaveBeenCalledTimes(2); + expect(mutationListenerC).toHaveBeenCalledTimes(1); + [mutationListenerA, mutationListenerB, mutationListenerC].forEach((fn) => { + expect(fn).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + updateTags: asymmetricMatcher( + (s: Set) => !s.has('registerMutationListener'), + ), + }), + ); + }); + }); + it('can use flushSync for synchronous updates', () => { init(); const onUpdate = jest.fn(); diff --git a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts index b495d174322..0026cf5d6ad 100644 --- a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts @@ -13,6 +13,7 @@ import { $nodesOfType, emptyFunction, generateRandomKey, + getCachedTypeToNodeMap, getTextDirection, isArray, isSelectionWithinEditor, @@ -235,5 +236,58 @@ describe('LexicalUtils tests', () => { ); }); }); + + test('getCachedTypeToNodeMap', async () => { + const {editor} = testEnv; + const paragraphKeys: string[] = []; + + const initialTypeToNodeMap = getCachedTypeToNodeMap( + editor.getEditorState(), + ); + expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe( + initialTypeToNodeMap, + ); + expect([...initialTypeToNodeMap.keys()]).toEqual(['root']); + expect(initialTypeToNodeMap.get('root')).toMatchObject({size: 1}); + + editor.update( + () => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode().append( + $createTextNode('a'), + ); + const paragraph2 = $createParagraphNode().append( + $createTextNode('b'), + ); + // these will be garbage collected and not in the readonly map + $createParagraphNode().append($createTextNode('c')); + root.append(paragraph1, paragraph2); + paragraphKeys.push(paragraph1.getKey(), paragraph2.getKey()); + }, + {discrete: true}, + ); + + const typeToNodeMap = getCachedTypeToNodeMap(editor.getEditorState()); + // verify that the initial cache was not used + expect(typeToNodeMap).not.toBe(initialTypeToNodeMap); + // verify that the cache is used for subsequent calls + expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe( + typeToNodeMap, + ); + expect(typeToNodeMap.size).toEqual(3); + expect([...typeToNodeMap.keys()]).toEqual( + expect.arrayContaining(['root', 'paragraph', 'text']), + ); + const paragraphMap = typeToNodeMap.get('paragraph')!; + expect(paragraphMap.size).toEqual(paragraphKeys.length); + expect([...paragraphMap.keys()]).toEqual( + expect.arrayContaining(paragraphKeys), + ); + const textMap = typeToNodeMap.get('text')!; + expect(textMap.size).toEqual(2); + expect( + [...textMap.values()].map((node) => (node as TextNode).__text), + ).toEqual(expect.arrayContaining(['a', 'b'])); + }); }); }); From 663bbd4abd8c94614c1e5050ed1f7662cf700970 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 15 Jul 2024 14:06:56 +0100 Subject: [PATCH 023/103] Add ref to contenteditable (#6381) --- .../flow/LexicalContentEditable.js.flow | 66 ++++++++++++--- .../flow/LexicalPlainTextPlugin.js.flow | 5 +- .../flow/LexicalRichTextPlugin.js.flow | 5 +- .../src/LexicalContentEditable.tsx | 67 +++++++++------ .../shared/LexicalContentEditableElement.tsx | 82 ++++++++++--------- .../lexical-react/src/shared/mergeRefs.ts | 24 ++++++ 6 files changed, 172 insertions(+), 77 deletions(-) create mode 100644 packages/lexical-react/src/shared/mergeRefs.ts diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 8f2752fe6d0..99004d8b137 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -7,16 +7,53 @@ * @flow strict */ +import type { LexicalEditor } from 'lexical'; +// $FlowFixMe - Not able to type this with a flow extension +import type {TRefFor} from 'CoreTypes.flow'; + import * as React from 'react'; +import type { AbstractComponent } from "react"; + +type InlineStyle = { + [key: string]: mixed; +} + +// Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement +type HTMLDivElementDOMProps = $ReadOnly<{ + 'aria-label'?: void | string, + 'aria-labeledby'?: void | string, + 'title'?: void | string, + onClick?: void | (e: SyntheticEvent) => mixed, + autoCapitalize?: void | boolean, + autoComplete?: void | boolean, + autoCorrect?: void | boolean, + id?: void | string, + className?: void | string, + 'data-testid'?: void | string, + role?: void | string, + spellCheck?: void | boolean, + suppressContentEditableWarning?: void | boolean, + tabIndex?: void | number, + style?: void | InlineStyle | CSSStyleDeclaration, + 'data-testid'?: void | string, +}>; + +export type PlaceholderProps = + | $ReadOnly<{ + 'aria-placeholder'?: void, + placeholder?: null, + }> + | $ReadOnly<{ + 'aria-placeholder': string, + placeholder: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node, + }>; -export type Props = ({...Partial,...} | $ReadOnly<{ - 'aria-placeholder': string; - placeholder: - | ((isEditable: boolean) => null | React$Node) - | null - | React$Node; -}>) & $ReadOnly<{ - ...Partial, +export type Props = $ReadOnly<{ + ...HTMLDivElementDOMProps, + editor__DEPRECATED?: LexicalEditor; ariaActiveDescendant?: string, ariaAutoComplete?: string, ariaControls?: string, @@ -26,10 +63,13 @@ export type Props = ({...Partial,...} | $ReadOnly<{ ariaLabelledBy?: string, ariaMultiline?: boolean, ariaOwns?: string, - ariaRequired?: boolean, + ariaRequired?: string, autoCapitalize?: boolean, - 'data-testid'?: string | null, - ... -}>; + ref?: TRefFor, + ...PlaceholderProps +}> -declare export function ContentEditable(props: Props): React$Node; +declare export var ContentEditable: AbstractComponent< + Props, + HTMLDivElement, +>; diff --git a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow index be50e13f492..7a9e8b403e2 100644 --- a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow @@ -21,6 +21,9 @@ type InitialEditorStateType = declare export function PlainTextPlugin({ contentEditable: React$Node, - placeholder: ((isEditable: boolean) => React$Node) | React$Node, + placeholder?: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node; ErrorBoundary: LexicalErrorBoundary, }): React$Node; diff --git a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow index a99380c5fe4..a07bf9e92f4 100644 --- a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow @@ -21,6 +21,9 @@ type InitialEditorStateType = declare export function RichTextPlugin({ contentEditable: React$Node, - placeholder: ((isEditable: boolean) => React$Node) | React$Node, + placeholder?: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node; ErrorBoundary: LexicalErrorBoundary, }): React$Node; diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 2ca5839087e..30829f6fb40 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -7,50 +7,69 @@ */ import type {Props as ElementProps} from './shared/LexicalContentEditableElement'; +import type {LexicalEditor} from 'lexical'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; +import {forwardRef, Ref, useLayoutEffect, useState} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; /* eslint-disable @typescript-eslint/ban-types */ -export type Props = ( - | {} - | { - 'aria-placeholder': string; - placeholder: - | ((isEditable: boolean) => null | JSX.Element) - | null - | JSX.Element; - } -) & - ElementProps; +export type Props = Omit & { + editor__DEPRECATED?: LexicalEditor; +} & ( + | { + 'aria-placeholder'?: void; + placeholder?: null; + } + | { + 'aria-placeholder': string; + placeholder: + | ((isEditable: boolean) => null | JSX.Element) + | JSX.Element; + } + ); + /* eslint-enable @typescript-eslint/ban-types */ -export function ContentEditable(props: Props): JSX.Element { - let placeholder = null; - let rest = props; - if ('placeholder' in props) { - ({placeholder, ...rest} = props); - } +export const ContentEditable = forwardRef(ContentEditableImpl); + +function ContentEditableImpl( + props: Props, + ref: Ref, +): JSX.Element { + const {placeholder, editor__DEPRECATED, ...rest} = props; + // editor__DEPRECATED will always be defined for non MLC surfaces + // eslint-disable-next-line react-hooks/rules-of-hooks + const editor = editor__DEPRECATED || useLexicalComposerContext()[0]; return ( <> - - + + {placeholder != null && ( + + )} ); } function Placeholder({ content, + editor, }: { - content: ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element; + editor: LexicalEditor; + content: ((isEditable: boolean) => null | JSX.Element) | JSX.Element; }): null | JSX.Element { - const [editor] = useLexicalComposerContext(); const showPlaceholder = useCanShowPlaceholder(editor); - const editable = useLexicalEditable(); + + const [isEditable, setEditable] = useState(editor.isEditable()); + useLayoutEffect(() => { + setEditable(editor.isEditable()); + return editor.registerEditableListener((currentIsEditable) => { + setEditable(currentIsEditable); + }); + }, [editor]); if (!showPlaceholder) { return null; @@ -58,7 +77,7 @@ function Placeholder({ let placeholder = null; if (typeof content === 'function') { - placeholder = content(editable); + placeholder = content(isEditable); } else if (content !== null) { placeholder = content; } diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 328da5d4028..2e1208e0d64 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -6,12 +6,16 @@ * */ -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import type {LexicalEditor} from 'lexical'; + import * as React from 'react'; -import {useCallback, useState} from 'react'; +import {forwardRef, Ref, useCallback, useMemo, useState} from 'react'; import useLayoutEffect from 'shared/useLayoutEffect'; +import {mergeRefs} from './mergeRefs'; + export type Props = { + editor: LexicalEditor; ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']; ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; ariaControls?: React.AriaAttributes['aria-controls']; @@ -26,31 +30,34 @@ export type Props = { 'data-testid'?: string | null | undefined; } & Omit, 'placeholder'>; -export function ContentEditableElement({ - ariaActiveDescendant, - ariaAutoComplete, - ariaControls, - ariaDescribedBy, - ariaExpanded, - ariaLabel, - ariaLabelledBy, - ariaMultiline, - ariaOwns, - ariaRequired, - autoCapitalize, - className, - id, - role = 'textbox', - spellCheck = true, - style, - tabIndex, - 'data-testid': testid, - ...rest -}: Props): JSX.Element { - const [editor] = useLexicalComposerContext(); - const [isEditable, setEditable] = useState(false); +function ContentEditableElementImpl( + { + editor, + ariaActiveDescendant, + ariaAutoComplete, + ariaControls, + ariaDescribedBy, + ariaExpanded, + ariaLabel, + ariaLabelledBy, + ariaMultiline, + ariaOwns, + ariaRequired, + autoCapitalize, + className, + id, + role = 'textbox', + spellCheck = true, + style, + tabIndex, + 'data-testid': testid, + ...rest + }: Props, + ref: Ref, +): JSX.Element { + const [isEditable, setEditable] = useState(editor.isEditable()); - const ref = useCallback( + const handleRef = useCallback( (rootElement: null | HTMLElement) => { // defaultView is required for a root element. // In multi-window setups, the defaultView may not exist at certain points. @@ -66,6 +73,7 @@ export function ContentEditableElement({ }, [editor], ); + const mergedRefs = useMemo(() => mergeRefs(ref, handleRef), [handleRef, ref]); useLayoutEffect(() => { setEditable(editor.isEditable()); @@ -77,33 +85,31 @@ export function ContentEditableElement({ return (
                    ); } + +export const ContentEditableElement = forwardRef(ContentEditableElementImpl); diff --git a/packages/lexical-react/src/shared/mergeRefs.ts b/packages/lexical-react/src/shared/mergeRefs.ts new file mode 100644 index 00000000000..23ddadd0620 --- /dev/null +++ b/packages/lexical-react/src/shared/mergeRefs.ts @@ -0,0 +1,24 @@ +/** + * 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. + * + */ +// Source: https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx + +export function mergeRefs( + ...refs: Array< + React.MutableRefObject | React.LegacyRef | undefined | null + > +): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === 'function') { + ref(value); + } else if (ref != null) { + (ref as React.MutableRefObject).current = value; + } + }); + }; +} From 0fbab8fd38e8122a78cfaf1279f64e42e6ba5834 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 15 Jul 2024 23:34:27 +0100 Subject: [PATCH 024/103] Restore registerRootListener null call (#6403) --- packages/lexical/src/LexicalEditor.ts | 1 + packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index ce7a22b5992..4ee42ca838d 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -768,6 +768,7 @@ export class LexicalEditor { listener(this._rootElement, null); listenerSetOrMap.add(listener); return () => { + listener(null, this._rootElement); listenerSetOrMap.delete(listener); }; } diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 1f069be7194..d3d4e822e8c 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -1138,7 +1138,7 @@ describe('LexicalEditor tests', () => { await Promise.resolve().then(); }); - expect(listener).toHaveBeenCalledTimes(4); + expect(listener).toHaveBeenCalledTimes(5); expect(container.innerHTML).toBe( '


                    ', ); From cad2861343c1cffdfdb27bd5aed59198f43dc32f Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 16 Jul 2024 15:49:46 +0100 Subject: [PATCH 025/103] Fix transpile nodesOfType (#6408) --- packages/lexical/src/LexicalUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 8cc3099462d..52ac015239e 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1124,7 +1124,7 @@ export function $nodesOfType(klass: Klass): Array { const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as | undefined | Map; - return nodes ? [...nodes.values()] : []; + return nodes ? Array.from(nodes.values()) : []; } const nodes = editorState._nodeMap; const nodesOfType: Array = []; From bd26794d34a0b81263d422b544d7c979972c468d Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 17 Jul 2024 14:16:06 +0800 Subject: [PATCH 026/103] [lexical-react][lexical-playground] sync draggable block plugin to www (#6397) --- .flowconfig | 1 + packages/lexical-devtools/tsconfig.json | 3 + .../plugins/DraggableBlockPlugin/index.tsx | 438 +---------------- .../flow/LexicalDraggableBlockPlugin.js.flow | 10 + packages/lexical-react/package.json | 30 ++ .../src/LexicalDraggableBlockPlugin.tsx | 456 ++++++++++++++++++ .../src/shared}/point.ts | 0 .../src/shared}/rect.ts | 30 +- tsconfig.build.json | 3 + tsconfig.json | 3 + 10 files changed, 543 insertions(+), 431 deletions(-) create mode 100644 packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow create mode 100644 packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx rename packages/{lexical-playground/src/utils => lexical-react/src/shared}/point.ts (100%) rename packages/{lexical-playground/src/utils => lexical-react/src/shared}/rect.ts (81%) diff --git a/.flowconfig b/.flowconfig index c0e6df641cb..8d966996cb3 100644 --- a/.flowconfig +++ b/.flowconfig @@ -52,6 +52,7 @@ module.name_mapper='^@lexical/react/LexicalComposerContext$' -> '/ module.name_mapper='^@lexical/react/LexicalContentEditable$' -> '/packages/lexical-react/flow/LexicalContentEditable.js.flow' module.name_mapper='^@lexical/react/LexicalContextMenuPlugin$' -> '/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow' module.name_mapper='^@lexical/react/LexicalDecoratorBlockNode$' -> '/packages/lexical-react/flow/LexicalDecoratorBlockNode.js.flow' +module.name_mapper='^@lexical/react/LexicalDraggableBlockPlugin$' -> '/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow' module.name_mapper='^@lexical/react/LexicalEditorRefPlugin$' -> '/packages/lexical-react/flow/LexicalEditorRefPlugin.js.flow' module.name_mapper='^@lexical/react/LexicalErrorBoundary$' -> '/packages/lexical-react/flow/LexicalErrorBoundary.js.flow' module.name_mapper='^@lexical/react/LexicalHashtagPlugin$' -> '/packages/lexical-react/flow/LexicalHashtagPlugin.js.flow' diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json index b82d880249d..a6fd399e833 100644 --- a/packages/lexical-devtools/tsconfig.json +++ b/packages/lexical-devtools/tsconfig.json @@ -72,6 +72,9 @@ "@lexical/react/LexicalDecoratorBlockNode": [ "../lexical-react/src/LexicalDecoratorBlockNode.ts" ], + "@lexical/react/LexicalDraggableBlockPlugin": [ + "../lexical-react/src/LexicalDraggableBlockPlugin.tsx" + ], "@lexical/react/LexicalEditorRefPlugin": [ "../lexical-react/src/LexicalEditorRefPlugin.tsx" ], diff --git a/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx b/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx index 9eb3b2acf16..3675cb786e2 100644 --- a/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx @@ -7,431 +7,37 @@ */ import './index.css'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {eventFiles} from '@lexical/rich-text'; -import {calculateZoomLevel, mergeRegister} from '@lexical/utils'; -import { - $getNearestNodeFromDOMNode, - $getNodeByKey, - $getRoot, - COMMAND_PRIORITY_HIGH, - COMMAND_PRIORITY_LOW, - DRAGOVER_COMMAND, - DROP_COMMAND, - LexicalEditor, -} from 'lexical'; -import * as React from 'react'; -import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react'; -import {createPortal} from 'react-dom'; +import {DraggableBlockPlugin_EXPERIMENTAL} from '@lexical/react/LexicalDraggableBlockPlugin'; +import {useRef} from 'react'; -import {isHTMLElement} from '../../utils/guard'; -import {Point} from '../../utils/point'; -import {Rect} from '../../utils/rect'; - -const SPACE = 4; -const TARGET_LINE_HALF_HEIGHT = 2; const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'; -const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'; -const TEXT_BOX_HORIZONTAL_PADDING = 28; - -const Downward = 1; -const Upward = -1; -const Indeterminate = 0; - -let prevIndex = Infinity; - -function getCurrentIndex(keysLength: number): number { - if (keysLength === 0) { - return Infinity; - } - if (prevIndex >= 0 && prevIndex < keysLength) { - return prevIndex; - } - - return Math.floor(keysLength / 2); -} - -function getTopLevelNodeKeys(editor: LexicalEditor): string[] { - return editor.getEditorState().read(() => $getRoot().getChildrenKeys()); -} - -function getCollapsedMargins(elem: HTMLElement): { - marginTop: number; - marginBottom: number; -} { - const getMargin = ( - element: Element | null, - margin: 'marginTop' | 'marginBottom', - ): number => - element ? parseFloat(window.getComputedStyle(element)[margin]) : 0; - - const {marginTop, marginBottom} = window.getComputedStyle(elem); - const prevElemSiblingMarginBottom = getMargin( - elem.previousElementSibling, - 'marginBottom', - ); - const nextElemSiblingMarginTop = getMargin( - elem.nextElementSibling, - 'marginTop', - ); - const collapsedTopMargin = Math.max( - parseFloat(marginTop), - prevElemSiblingMarginBottom, - ); - const collapsedBottomMargin = Math.max( - parseFloat(marginBottom), - nextElemSiblingMarginTop, - ); - - return {marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin}; -} - -function getBlockElement( - anchorElem: HTMLElement, - editor: LexicalEditor, - event: MouseEvent, - useEdgeAsDefault = false, -): HTMLElement | null { - const anchorElementRect = anchorElem.getBoundingClientRect(); - const topLevelNodeKeys = getTopLevelNodeKeys(editor); - - let blockElem: HTMLElement | null = null; - - editor.getEditorState().read(() => { - if (useEdgeAsDefault) { - const [firstNode, lastNode] = [ - editor.getElementByKey(topLevelNodeKeys[0]), - editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]), - ]; - - const [firstNodeRect, lastNodeRect] = [ - firstNode?.getBoundingClientRect(), - lastNode?.getBoundingClientRect(), - ]; - - if (firstNodeRect && lastNodeRect) { - const firstNodeZoom = calculateZoomLevel(firstNode); - const lastNodeZoom = calculateZoomLevel(lastNode); - if (event.y / firstNodeZoom < firstNodeRect.top) { - blockElem = firstNode; - } else if (event.y / lastNodeZoom > lastNodeRect.bottom) { - blockElem = lastNode; - } - - if (blockElem) { - return; - } - } - } - - let index = getCurrentIndex(topLevelNodeKeys.length); - let direction = Indeterminate; - - while (index >= 0 && index < topLevelNodeKeys.length) { - const key = topLevelNodeKeys[index]; - const elem = editor.getElementByKey(key); - if (elem === null) { - break; - } - const zoom = calculateZoomLevel(elem); - const point = new Point(event.x / zoom, event.y / zoom); - const domRect = Rect.fromDOM(elem); - const {marginTop, marginBottom} = getCollapsedMargins(elem); - const rect = domRect.generateNewRect({ - bottom: domRect.bottom + marginBottom, - left: anchorElementRect.left, - right: anchorElementRect.right, - top: domRect.top - marginTop, - }); - - const { - result, - reason: {isOnTopSide, isOnBottomSide}, - } = rect.contains(point); - - if (result) { - blockElem = elem; - prevIndex = index; - break; - } - - if (direction === Indeterminate) { - if (isOnTopSide) { - direction = Upward; - } else if (isOnBottomSide) { - direction = Downward; - } else { - // stop search block element - direction = Infinity; - } - } - - index += direction; - } - }); - - return blockElem; -} function isOnMenu(element: HTMLElement): boolean { return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`); } -function setMenuPosition( - targetElem: HTMLElement | null, - floatingElem: HTMLElement, - anchorElem: HTMLElement, -) { - if (!targetElem) { - floatingElem.style.opacity = '0'; - floatingElem.style.transform = 'translate(-10000px, -10000px)'; - return; - } - - const targetRect = targetElem.getBoundingClientRect(); - const targetStyle = window.getComputedStyle(targetElem); - const floatingElemRect = floatingElem.getBoundingClientRect(); - const anchorElementRect = anchorElem.getBoundingClientRect(); - - const top = - targetRect.top + - (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - - anchorElementRect.top; - - const left = SPACE; - - floatingElem.style.opacity = '1'; - floatingElem.style.transform = `translate(${left}px, ${top}px)`; -} - -function setDragImage( - dataTransfer: DataTransfer, - draggableBlockElem: HTMLElement, -) { - const {transform} = draggableBlockElem.style; - - // Remove dragImage borders - draggableBlockElem.style.transform = 'translateZ(0)'; - dataTransfer.setDragImage(draggableBlockElem, 0, 0); - - setTimeout(() => { - draggableBlockElem.style.transform = transform; - }); -} - -function setTargetLine( - targetLineElem: HTMLElement, - targetBlockElem: HTMLElement, - mouseY: number, - anchorElem: HTMLElement, -) { - const {top: targetBlockElemTop, height: targetBlockElemHeight} = - targetBlockElem.getBoundingClientRect(); - const {top: anchorTop, width: anchorWidth} = - anchorElem.getBoundingClientRect(); - const {marginTop, marginBottom} = getCollapsedMargins(targetBlockElem); - let lineTop = targetBlockElemTop; - if (mouseY >= targetBlockElemTop) { - lineTop += targetBlockElemHeight + marginBottom / 2; - } else { - lineTop -= marginTop / 2; - } - - const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT; - const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE; - - targetLineElem.style.transform = `translate(${left}px, ${top}px)`; - targetLineElem.style.width = `${ - anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2 - }px`; - targetLineElem.style.opacity = '.4'; -} - -function hideTargetLine(targetLineElem: HTMLElement | null) { - if (targetLineElem) { - targetLineElem.style.opacity = '0'; - targetLineElem.style.transform = 'translate(-10000px, -10000px)'; - } -} - -function useDraggableBlockMenu( - editor: LexicalEditor, - anchorElem: HTMLElement, - isEditable: boolean, -): JSX.Element { - const scrollerElem = anchorElem.parentElement; - - const menuRef = useRef(null); - const targetLineRef = useRef(null); - const isDraggingBlockRef = useRef(false); - const [draggableBlockElem, setDraggableBlockElem] = - useState(null); - - useEffect(() => { - function onMouseMove(event: MouseEvent) { - const target = event.target; - if (!isHTMLElement(target)) { - setDraggableBlockElem(null); - return; - } - - if (isOnMenu(target)) { - return; - } - - const _draggableBlockElem = getBlockElement(anchorElem, editor, event); - - setDraggableBlockElem(_draggableBlockElem); - } - - function onMouseLeave() { - setDraggableBlockElem(null); - } - - scrollerElem?.addEventListener('mousemove', onMouseMove); - scrollerElem?.addEventListener('mouseleave', onMouseLeave); - - return () => { - scrollerElem?.removeEventListener('mousemove', onMouseMove); - scrollerElem?.removeEventListener('mouseleave', onMouseLeave); - }; - }, [scrollerElem, anchorElem, editor]); - - useEffect(() => { - if (menuRef.current) { - setMenuPosition(draggableBlockElem, menuRef.current, anchorElem); - } - }, [anchorElem, draggableBlockElem]); - - useEffect(() => { - function onDragover(event: DragEvent): boolean { - if (!isDraggingBlockRef.current) { - return false; - } - const [isFileTransfer] = eventFiles(event); - if (isFileTransfer) { - return false; - } - const {pageY, target} = event; - if (!isHTMLElement(target)) { - return false; - } - const targetBlockElem = getBlockElement(anchorElem, editor, event, true); - const targetLineElem = targetLineRef.current; - if (targetBlockElem === null || targetLineElem === null) { - return false; - } - setTargetLine( - targetLineElem, - targetBlockElem, - pageY / calculateZoomLevel(target), - anchorElem, - ); - // Prevent default event to be able to trigger onDrop events - event.preventDefault(); - return true; - } - - function $onDrop(event: DragEvent): boolean { - if (!isDraggingBlockRef.current) { - return false; - } - const [isFileTransfer] = eventFiles(event); - if (isFileTransfer) { - return false; - } - const {target, dataTransfer, pageY} = event; - const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || ''; - const draggedNode = $getNodeByKey(dragData); - if (!draggedNode) { - return false; - } - if (!isHTMLElement(target)) { - return false; - } - const targetBlockElem = getBlockElement(anchorElem, editor, event, true); - if (!targetBlockElem) { - return false; - } - const targetNode = $getNearestNodeFromDOMNode(targetBlockElem); - if (!targetNode) { - return false; - } - if (targetNode === draggedNode) { - return true; - } - const targetBlockElemTop = targetBlockElem.getBoundingClientRect().top; - if (pageY / calculateZoomLevel(target) >= targetBlockElemTop) { - targetNode.insertAfter(draggedNode); - } else { - targetNode.insertBefore(draggedNode); - } - setDraggableBlockElem(null); - - return true; - } - - return mergeRegister( - editor.registerCommand( - DRAGOVER_COMMAND, - (event) => { - return onDragover(event); - }, - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - DROP_COMMAND, - (event) => { - return $onDrop(event); - }, - COMMAND_PRIORITY_HIGH, - ), - ); - }, [anchorElem, editor]); - - function onDragStart(event: ReactDragEvent): void { - const dataTransfer = event.dataTransfer; - if (!dataTransfer || !draggableBlockElem) { - return; - } - setDragImage(dataTransfer, draggableBlockElem); - let nodeKey = ''; - editor.update(() => { - const node = $getNearestNodeFromDOMNode(draggableBlockElem); - if (node) { - nodeKey = node.getKey(); - } - }); - isDraggingBlockRef.current = true; - dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey); - } - - function onDragEnd(): void { - isDraggingBlockRef.current = false; - hideTargetLine(targetLineRef.current); - } - - return createPortal( - <> -
                    -
                    -
                    -
                    - , - anchorElem, - ); -} - -export default function DraggableBlockPlugin({ +export default function PlaygroundDraggableBlockPlugin({ anchorElem = document.body, }: { anchorElem?: HTMLElement; }): JSX.Element { - const [editor] = useLexicalComposerContext(); - return useDraggableBlockMenu(editor, anchorElem, editor._editable); + const menuRef = useRef(null); + const targetLineRef = useRef(null); + + return ( + +
                    +
                    + } + targetLineComponent={ +
                    + } + isOnMenu={isOnMenu} + /> + ); } diff --git a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow new file mode 100644 index 00000000000..ef306bb38a6 --- /dev/null +++ b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow @@ -0,0 +1,10 @@ +/** + * 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 + */ + +declare export function DraggableBlockPlugin(): React$MixedElement; diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index 005a91755b8..a95ba4ed46a 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -491,6 +491,36 @@ "default": "./LexicalDecoratorBlockNode.js" } }, + "./LexicalDraggableBlockPlugin": { + "import": { + "types": "./LexicalDraggableBlockPlugin.d.ts", + "development": "./LexicalDraggableBlockPlugin.dev.mjs", + "production": "./LexicalDraggableBlockPlugin.prod.mjs", + "node": "./LexicalDraggableBlockPlugin.node.mjs", + "default": "./LexicalDraggableBlockPlugin.mjs" + }, + "require": { + "types": "./LexicalDraggableBlockPlugin.d.ts", + "development": "./LexicalDraggableBlockPlugin.dev.js", + "production": "./LexicalDraggableBlockPlugin.prod.js", + "default": "./LexicalDraggableBlockPlugin.js" + } + }, + "./LexicalDraggableBlockPlugin.js": { + "import": { + "types": "./LexicalDraggableBlockPlugin.d.ts", + "development": "./LexicalDraggableBlockPlugin.dev.mjs", + "production": "./LexicalDraggableBlockPlugin.prod.mjs", + "node": "./LexicalDraggableBlockPlugin.node.mjs", + "default": "./LexicalDraggableBlockPlugin.mjs" + }, + "require": { + "types": "./LexicalDraggableBlockPlugin.d.ts", + "development": "./LexicalDraggableBlockPlugin.dev.js", + "production": "./LexicalDraggableBlockPlugin.prod.js", + "default": "./LexicalDraggableBlockPlugin.js" + } + }, "./LexicalEditorRefPlugin": { "import": { "types": "./LexicalEditorRefPlugin.d.ts", diff --git a/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx new file mode 100644 index 00000000000..d37bd69f3c9 --- /dev/null +++ b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx @@ -0,0 +1,456 @@ +/** + * 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 {eventFiles} from '@lexical/rich-text'; +import {calculateZoomLevel, isHTMLElement, mergeRegister} from '@lexical/utils'; +import { + $getNearestNodeFromDOMNode, + $getNodeByKey, + $getRoot, + COMMAND_PRIORITY_HIGH, + COMMAND_PRIORITY_LOW, + DRAGOVER_COMMAND, + DROP_COMMAND, + LexicalEditor, +} from 'lexical'; +import { + DragEvent as ReactDragEvent, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; +import {createPortal} from 'react-dom'; + +import {Point} from './shared/point'; +import {Rectangle} from './shared/rect'; + +const SPACE = 4; +const TARGET_LINE_HALF_HEIGHT = 2; +const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'; +const TEXT_BOX_HORIZONTAL_PADDING = 28; + +const Downward = 1; +const Upward = -1; +const Indeterminate = 0; + +let prevIndex = Infinity; + +function getCurrentIndex(keysLength: number): number { + if (keysLength === 0) { + return Infinity; + } + if (prevIndex >= 0 && prevIndex < keysLength) { + return prevIndex; + } + + return Math.floor(keysLength / 2); +} + +function getTopLevelNodeKeys(editor: LexicalEditor): string[] { + return editor.getEditorState().read(() => $getRoot().getChildrenKeys()); +} + +function getCollapsedMargins(elem: HTMLElement): { + marginTop: number; + marginBottom: number; +} { + const getMargin = ( + element: Element | null, + margin: 'marginTop' | 'marginBottom', + ): number => + element ? parseFloat(window.getComputedStyle(element)[margin]) : 0; + + const {marginTop, marginBottom} = window.getComputedStyle(elem); + const prevElemSiblingMarginBottom = getMargin( + elem.previousElementSibling, + 'marginBottom', + ); + const nextElemSiblingMarginTop = getMargin( + elem.nextElementSibling, + 'marginTop', + ); + const collapsedTopMargin = Math.max( + parseFloat(marginTop), + prevElemSiblingMarginBottom, + ); + const collapsedBottomMargin = Math.max( + parseFloat(marginBottom), + nextElemSiblingMarginTop, + ); + + return {marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin}; +} + +function getBlockElement( + anchorElem: HTMLElement, + editor: LexicalEditor, + event: MouseEvent, + useEdgeAsDefault = false, +): HTMLElement | null { + const anchorElementRect = anchorElem.getBoundingClientRect(); + const topLevelNodeKeys = getTopLevelNodeKeys(editor); + + let blockElem: HTMLElement | null = null; + + editor.getEditorState().read(() => { + if (useEdgeAsDefault) { + const [firstNode, lastNode] = [ + editor.getElementByKey(topLevelNodeKeys[0]), + editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]), + ]; + + const [firstNodeRect, lastNodeRect] = [ + firstNode != null ? firstNode.getBoundingClientRect() : undefined, + lastNode != null ? lastNode.getBoundingClientRect() : undefined, + ]; + + if (firstNodeRect && lastNodeRect) { + const firstNodeZoom = calculateZoomLevel(firstNode); + const lastNodeZoom = calculateZoomLevel(lastNode); + if (event.y / firstNodeZoom < firstNodeRect.top) { + blockElem = firstNode; + } else if (event.y / lastNodeZoom > lastNodeRect.bottom) { + blockElem = lastNode; + } + + if (blockElem) { + return; + } + } + } + + let index = getCurrentIndex(topLevelNodeKeys.length); + let direction = Indeterminate; + + while (index >= 0 && index < topLevelNodeKeys.length) { + const key = topLevelNodeKeys[index]; + const elem = editor.getElementByKey(key); + if (elem === null) { + break; + } + const zoom = calculateZoomLevel(elem); + const point = new Point(event.x / zoom, event.y / zoom); + const domRect = Rectangle.fromDOM(elem); + const {marginTop, marginBottom} = getCollapsedMargins(elem); + const rect = domRect.generateNewRect({ + bottom: domRect.bottom + marginBottom, + left: anchorElementRect.left, + right: anchorElementRect.right, + top: domRect.top - marginTop, + }); + + const { + result, + reason: {isOnTopSide, isOnBottomSide}, + } = rect.contains(point); + + if (result) { + blockElem = elem; + prevIndex = index; + break; + } + + if (direction === Indeterminate) { + if (isOnTopSide) { + direction = Upward; + } else if (isOnBottomSide) { + direction = Downward; + } else { + // stop search block element + direction = Infinity; + } + } + + index += direction; + } + }); + + return blockElem; +} + +function setMenuPosition( + targetElem: HTMLElement | null, + floatingElem: HTMLElement, + anchorElem: HTMLElement, +) { + if (!targetElem) { + floatingElem.style.opacity = '0'; + floatingElem.style.transform = 'translate(-10000px, -10000px)'; + return; + } + + const targetRect = targetElem.getBoundingClientRect(); + const targetStyle = window.getComputedStyle(targetElem); + const floatingElemRect = floatingElem.getBoundingClientRect(); + const anchorElementRect = anchorElem.getBoundingClientRect(); + + const top = + targetRect.top + + (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - + anchorElementRect.top; + + const left = SPACE; + + floatingElem.style.opacity = '1'; + floatingElem.style.transform = `translate(${left}px, ${top}px)`; +} + +function setDragImage( + dataTransfer: DataTransfer, + draggableBlockElem: HTMLElement, +) { + const {transform} = draggableBlockElem.style; + + // Remove dragImage borders + draggableBlockElem.style.transform = 'translateZ(0)'; + dataTransfer.setDragImage(draggableBlockElem, 0, 0); + + setTimeout(() => { + draggableBlockElem.style.transform = transform; + }); +} + +function setTargetLine( + targetLineElem: HTMLElement, + targetBlockElem: HTMLElement, + mouseY: number, + anchorElem: HTMLElement, +) { + const {top: targetBlockElemTop, height: targetBlockElemHeight} = + targetBlockElem.getBoundingClientRect(); + const {top: anchorTop, width: anchorWidth} = + anchorElem.getBoundingClientRect(); + const {marginTop, marginBottom} = getCollapsedMargins(targetBlockElem); + let lineTop = targetBlockElemTop; + if (mouseY >= targetBlockElemTop) { + lineTop += targetBlockElemHeight + marginBottom / 2; + } else { + lineTop -= marginTop / 2; + } + + const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT; + const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE; + + targetLineElem.style.transform = `translate(${left}px, ${top}px)`; + targetLineElem.style.width = `${ + anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2 + }px`; + targetLineElem.style.opacity = '.4'; +} + +function hideTargetLine(targetLineElem: HTMLElement | null) { + if (targetLineElem) { + targetLineElem.style.opacity = '0'; + targetLineElem.style.transform = 'translate(-10000px, -10000px)'; + } +} + +function useDraggableBlockMenu( + editor: LexicalEditor, + anchorElem: HTMLElement, + menuRef: React.RefObject, + targetLineRef: React.RefObject, + isEditable: boolean, + menuComponent: ReactNode, + targetLineComponent: ReactNode, + isOnMenu: (element: HTMLElement) => boolean, +): JSX.Element { + const scrollerElem = anchorElem.parentElement; + + const isDraggingBlockRef = useRef(false); + const [draggableBlockElem, setDraggableBlockElem] = + useState(null); + + useEffect(() => { + function onMouseMove(event: MouseEvent) { + const target = event.target; + if (target != null && !isHTMLElement(target)) { + setDraggableBlockElem(null); + return; + } + + if (target != null && isOnMenu(target as HTMLElement)) { + return; + } + + const _draggableBlockElem = getBlockElement(anchorElem, editor, event); + + setDraggableBlockElem(_draggableBlockElem); + } + + function onMouseLeave() { + setDraggableBlockElem(null); + } + + if (scrollerElem != null) { + scrollerElem.addEventListener('mousemove', onMouseMove); + scrollerElem.addEventListener('mouseleave', onMouseLeave); + } + + return () => { + if (scrollerElem != null) { + scrollerElem.removeEventListener('mousemove', onMouseMove); + scrollerElem.removeEventListener('mouseleave', onMouseLeave); + } + }; + }, [scrollerElem, anchorElem, editor, isOnMenu]); + + useEffect(() => { + if (menuRef.current) { + setMenuPosition(draggableBlockElem, menuRef.current, anchorElem); + } + }, [anchorElem, draggableBlockElem, menuRef]); + + useEffect(() => { + function onDragover(event: DragEvent): boolean { + if (!isDraggingBlockRef.current) { + return false; + } + const [isFileTransfer] = eventFiles(event); + if (isFileTransfer) { + return false; + } + const {pageY, target} = event; + if (target != null && !isHTMLElement(target)) { + return false; + } + const targetBlockElem = getBlockElement(anchorElem, editor, event, true); + const targetLineElem = targetLineRef.current; + if (targetBlockElem === null || targetLineElem === null) { + return false; + } + setTargetLine( + targetLineElem, + targetBlockElem, + pageY / calculateZoomLevel(target), + anchorElem, + ); + // Prevent default event to be able to trigger onDrop events + event.preventDefault(); + return true; + } + + function $onDrop(event: DragEvent): boolean { + if (!isDraggingBlockRef.current) { + return false; + } + const [isFileTransfer] = eventFiles(event); + if (isFileTransfer) { + return false; + } + const {target, dataTransfer, pageY} = event; + const dragData = + dataTransfer != null ? dataTransfer.getData(DRAG_DATA_FORMAT) : ''; + const draggedNode = $getNodeByKey(dragData); + if (!draggedNode) { + return false; + } + if (target != null && !isHTMLElement(target)) { + return false; + } + const targetBlockElem = getBlockElement(anchorElem, editor, event, true); + if (!targetBlockElem) { + return false; + } + const targetNode = $getNearestNodeFromDOMNode(targetBlockElem); + if (!targetNode) { + return false; + } + if (targetNode === draggedNode) { + return true; + } + const targetBlockElemTop = targetBlockElem.getBoundingClientRect().top; + if (pageY / calculateZoomLevel(target) >= targetBlockElemTop) { + targetNode.insertAfter(draggedNode); + } else { + targetNode.insertBefore(draggedNode); + } + setDraggableBlockElem(null); + + return true; + } + + return mergeRegister( + editor.registerCommand( + DRAGOVER_COMMAND, + (event) => { + return onDragover(event); + }, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + DROP_COMMAND, + (event) => { + return $onDrop(event); + }, + COMMAND_PRIORITY_HIGH, + ), + ); + }, [anchorElem, editor, targetLineRef]); + + function onDragStart(event: ReactDragEvent): void { + const dataTransfer = event.dataTransfer; + if (!dataTransfer || !draggableBlockElem) { + return; + } + setDragImage(dataTransfer, draggableBlockElem); + let nodeKey = ''; + editor.update(() => { + const node = $getNearestNodeFromDOMNode(draggableBlockElem); + if (node) { + nodeKey = node.getKey(); + } + }); + isDraggingBlockRef.current = true; + dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey); + } + + function onDragEnd(): void { + isDraggingBlockRef.current = false; + hideTargetLine(targetLineRef.current); + } + return createPortal( + <> +
                    + {isEditable && menuComponent} +
                    + {targetLineComponent} + , + anchorElem, + ); +} + +export function DraggableBlockPlugin_EXPERIMENTAL({ + anchorElem = document.body, + menuRef, + targetLineRef, + menuComponent, + targetLineComponent, + isOnMenu, +}: { + anchorElem?: HTMLElement; + menuRef: React.RefObject; + targetLineRef: React.RefObject; + menuComponent: ReactNode; + targetLineComponent: ReactNode; + isOnMenu: (element: HTMLElement) => boolean; +}): JSX.Element { + const [editor] = useLexicalComposerContext(); + return useDraggableBlockMenu( + editor, + anchorElem, + menuRef, + targetLineRef, + editor._editable, + menuComponent, + targetLineComponent, + isOnMenu, + ); +} diff --git a/packages/lexical-playground/src/utils/point.ts b/packages/lexical-react/src/shared/point.ts similarity index 100% rename from packages/lexical-playground/src/utils/point.ts rename to packages/lexical-react/src/shared/point.ts diff --git a/packages/lexical-playground/src/utils/rect.ts b/packages/lexical-react/src/shared/rect.ts similarity index 81% rename from packages/lexical-playground/src/utils/rect.ts rename to packages/lexical-react/src/shared/rect.ts index be118c35e2a..2118352c845 100644 --- a/packages/lexical-playground/src/utils/rect.ts +++ b/packages/lexical-react/src/shared/rect.ts @@ -17,7 +17,7 @@ type ContainsPointReturn = { }; }; -export class Rect { +export class Rectangle { private readonly _left: number; private readonly _top: number; private readonly _right: number; @@ -60,7 +60,7 @@ export class Rect { return Math.abs(this._bottom - this._top); } - public equals({top, left, bottom, right}: Rect): boolean { + public equals({top, left, bottom, right}: Rectangle): boolean { return ( top === this._top && bottom === this._bottom && @@ -70,8 +70,8 @@ export class Rect { } public contains({x, y}: Point): ContainsPointReturn; - public contains({top, left, bottom, right}: Rect): boolean; - public contains(target: Point | Rect): boolean | ContainsPointReturn { + public contains({top, left, bottom, right}: Rectangle): boolean; + public contains(target: Point | Rectangle): boolean | ContainsPointReturn { if (isPoint(target)) { const {x, y} = target; @@ -108,7 +108,7 @@ export class Rect { } } - public intersectsWith(rect: Rect): boolean { + public intersectsWith(rect: Rectangle): boolean { const {left: x1, top: y1, width: w1, height: h1} = rect; const {left: x2, top: y2, width: w2, height: h2} = this; const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2; @@ -123,8 +123,8 @@ export class Rect { top = this.top, right = this.right, bottom = this.bottom, - }): Rect { - return new Rect(left, top, right, bottom); + }): Rectangle { + return new Rectangle(left, top, right, bottom); } static fromLTRB( @@ -132,8 +132,8 @@ export class Rect { top: number, right: number, bottom: number, - ): Rect { - return new Rect(left, top, right, bottom); + ): Rectangle { + return new Rectangle(left, top, right, bottom); } static fromLWTH( @@ -141,18 +141,18 @@ export class Rect { width: number, top: number, height: number, - ): Rect { - return new Rect(left, top, left + width, top + height); + ): Rectangle { + return new Rectangle(left, top, left + width, top + height); } - static fromPoints(startPoint: Point, endPoint: Point): Rect { + static fromPoints(startPoint: Point, endPoint: Point): Rectangle { const {y: top, x: left} = startPoint; const {y: bottom, x: right} = endPoint; - return Rect.fromLTRB(left, top, right, bottom); + return Rectangle.fromLTRB(left, top, right, bottom); } - static fromDOM(dom: HTMLElement): Rect { + static fromDOM(dom: HTMLElement): Rectangle { const {top, width, left, height} = dom.getBoundingClientRect(); - return Rect.fromLWTH(left, width, top, height); + return Rectangle.fromLWTH(left, width, top, height); } } diff --git a/tsconfig.build.json b/tsconfig.build.json index 40b574094d5..febc18a834a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -73,6 +73,9 @@ "@lexical/react/LexicalDecoratorBlockNode": [ "./packages/lexical-react/src/LexicalDecoratorBlockNode.ts" ], + "@lexical/react/LexicalDraggableBlockPlugin": [ + "./packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx" + ], "@lexical/react/LexicalEditorRefPlugin": [ "./packages/lexical-react/src/LexicalEditorRefPlugin.tsx" ], diff --git a/tsconfig.json b/tsconfig.json index 7ec705788c3..a090e0449b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -81,6 +81,9 @@ "@lexical/react/LexicalDecoratorBlockNode": [ "./packages/lexical-react/src/LexicalDecoratorBlockNode.ts" ], + "@lexical/react/LexicalDraggableBlockPlugin": [ + "./packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx" + ], "@lexical/react/LexicalEditorRefPlugin": [ "./packages/lexical-react/src/LexicalEditorRefPlugin.tsx" ], From 96b6214c94780e9f05d5639d851532f8aea37158 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 17 Jul 2024 14:53:04 +0800 Subject: [PATCH 027/103] CI: tag flaky tests (#6405) --- .../__tests__/e2e/Tables.spec.mjs | 260 +++++++++--------- .../4872-full-row-span-cell-merge.spec.mjs | 60 ++-- 2 files changed, 163 insertions(+), 157 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index dee1a4637e1..424205955fe 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -966,145 +966,149 @@ test.describe.parallel('Tables', () => { ); }); - test(`Can copy + paste (internal) using Table selection`, async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + test( + `Can copy + paste (internal) using Table selection`, + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - const clipboard = await copyToClipboard(page); + const clipboard = await copyToClipboard(page); - // For some reason you need to click the paragraph twice for this to pass - // on Collab Firefox. - await click(page, 'div.ContentEditable__root > p:first-of-type'); - await click(page, 'div.ContentEditable__root > p:first-of-type'); + // For some reason you need to click the paragraph twice for this to pass + // on Collab Firefox. + await click(page, 'div.ContentEditable__root > p:first-of-type'); + await click(page, 'div.ContentEditable__root > p:first-of-type'); - await pasteFromClipboard(page, clipboard); + await pasteFromClipboard(page, clipboard); - // Check that the character styles are applied. - await assertHTML( - page, - html` - - - - - - - - - -
                    -

                    a

                    -
                    -

                    bb

                    -
                    -

                    d

                    -
                    -

                    e

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

                    a

                    -
                    -

                    bb

                    -
                    -

                    cc

                    -
                    -

                    d

                    -
                    -

                    e

                    -
                    -

                    f

                    -
                    -


                    - `, - undefined, - {ignoreClasses: true}, - ); - }); + // Check that the character styles are applied. + await assertHTML( + page, + html` + + + + + + + + + +
                    +

                    a

                    +
                    +

                    bb

                    +
                    +

                    d

                    +
                    +

                    e

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

                    a

                    +
                    +

                    bb

                    +
                    +

                    cc

                    +
                    +

                    d

                    +
                    +

                    e

                    +
                    +

                    f

                    +
                    +


                    + `, + undefined, + {ignoreClasses: true}, + ); + }, + ); - test(`Can clear text using Table selection`, async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + test( + `Can clear text using Table selection`, + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); - // Check that the text was cleared. - await assertHTML( - page, - html` -


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


                    -
                    -


                    -
                    -

                    cc

                    -
                    -


                    -
                    -


                    -
                    -

                    f

                    -
                    -


                    - `, - undefined, - {ignoreClasses: true}, - ); - }); + // Check that the text was cleared. + await assertHTML( + page, + html` +


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


                    +
                    +


                    +
                    +

                    cc

                    +
                    +


                    +
                    +


                    +
                    +

                    f

                    +
                    +


                    + `, + undefined, + {ignoreClasses: true}, + ); + }, + ); test(`Range Selection is corrected when it contains a partial Table.`, async ({ page, diff --git a/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs b/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs index b02d4b59659..7463fbe4ae6 100644 --- a/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs @@ -18,39 +18,41 @@ import { test.describe('Regression test #4872', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test('merging two full rows does not break table selection', async ({ - page, - isPlainText, - isCollab, - }) => { - test.skip(isPlainText); + test( + 'merging two full rows does not break table selection', + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 5, 5); + await insertTable(page, 5, 5); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 0, y: 1}, - {x: 4, y: 2}, - true, - false, - ); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 1}, + {x: 4, y: 2}, + true, + false, + ); - await mergeTableCells(page); + await mergeTableCells(page); - await selectCellsFromTableCords( - page, - {x: 1, y: 4}, - {x: 2, y: 4}, - false, - false, - ); + await selectCellsFromTableCords( + page, + {x: 1, y: 4}, + {x: 2, y: 4}, + false, + false, + ); - await assertTableSelectionCoordinates(page, { - anchor: {x: 1, y: 4}, - focus: {x: 2, y: 4}, - }); - }); + await assertTableSelectionCoordinates(page, { + anchor: {x: 1, y: 4}, + focus: {x: 2, y: 4}, + }); + }, + ); }); From 2c90ee08e6cb9a073ed4cb7a9fca3c8f273d9f47 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 17 Jul 2024 16:01:30 +0800 Subject: [PATCH 028/103] CI: run flaky tests on firefox browsers (#6411) --- .github/workflows/call-e2e-all-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/call-e2e-all-tests.yml b/.github/workflows/call-e2e-all-tests.yml index 2474748b55f..27dfb597a80 100644 --- a/.github/workflows/call-e2e-all-tests.yml +++ b/.github/workflows/call-e2e-all-tests.yml @@ -146,8 +146,7 @@ jobs: strategy: matrix: node-version: [18.18.0] - # Currently using single browser & os combination for flaky tests to reduce cost impact - browser: ['chromium'] + browser: ['chromium', 'firefox'] editor-mode: ['rich-text', 'plain-text', 'rich-text-with-collab'] events-mode: ['modern-events'] uses: ./.github/workflows/call-e2e-test.yml From 85abcdecac9e29a948c494b4e33e9d38cc3b70ad Mon Sep 17 00:00:00 2001 From: wnhlee <40269597+2wheeh@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:31:41 +0900 Subject: [PATCH 029/103] CI: fix build failure on astro integration tests (#6414) --- .github/workflows/call-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/call-integration-tests.yml b/.github/workflows/call-integration-tests.yml index 9cc38f3d41a..6a1ab24ae7e 100644 --- a/.github/workflows/call-integration-tests.yml +++ b/.github/workflows/call-integration-tests.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.18.0] + node-version: [20.15.1] env: CI: true steps: From 3fcf02d7206f3b4c218b1f2f0c503f085c491533 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 18 Jul 2024 21:56:33 +0100 Subject: [PATCH 030/103] Fix discrete nested updates (#6419) --- packages/lexical/src/LexicalUpdates.ts | 8 ++ .../src/__tests__/unit/LexicalEditor.test.tsx | 113 +++++++++++++++++- .../lexical/src/__tests__/utils/index.tsx | 6 +- 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 50479fb10a1..45f01b7815a 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -802,6 +802,14 @@ function processNestedUpdates( if (options.skipTransforms) { skipTransforms = true; } + if (options.discrete) { + const pendingEditorState = editor._pendingEditorState; + invariant( + pendingEditorState !== null, + 'Unexpected empty pending editor state on discrete nested update', + ); + pendingEditorState._flushSync = true; + } if (onUpdate) { editor._deferred.push(onUpdate); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index d3d4e822e8c..aa63aa14ac5 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -21,6 +21,7 @@ import { $createLineBreakNode, $createNodeSelection, $createParagraphNode, + $createRangeSelection, $createTextNode, $getEditor, $getNearestNodeFromDOMNode, @@ -62,6 +63,7 @@ import { $createTestElementNode, $createTestInlineElementNode, createTestEditor, + createTestHeadlessEditor, TestComposer, TestTextNode, } from '../utils'; @@ -307,9 +309,11 @@ describe('LexicalEditor tests', () => { it('Should handle nested updates in the correct sequence', async () => { init(); + const onUpdate = jest.fn(); let log: Array = []; + editor.registerUpdateListener(onUpdate); editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); @@ -354,6 +358,7 @@ describe('LexicalEditor tests', () => { // Wait for update to complete await Promise.resolve().then(); + expect(onUpdate).toHaveBeenCalledTimes(1); expect(log).toEqual(['A1', 'B1', 'C1', 'D1', 'E1', 'F1']); log = []; @@ -447,6 +452,28 @@ describe('LexicalEditor tests', () => { ]); }); + it('nested update after selection update triggers exactly 1 update', async () => { + init(); + const onUpdate = jest.fn(); + editor.registerUpdateListener(onUpdate); + editor.update(() => { + $setSelection($createRangeSelection()); + editor.update(() => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Sync update')), + ); + }); + }); + + await Promise.resolve().then(); + + const textContent = editor + .getEditorState() + .read(() => $getRoot().getTextContent()); + expect(textContent).toBe('Sync update'); + expect(onUpdate).toHaveBeenCalledTimes(1); + }); + it('update does not call onUpdate callback when no dirty nodes', () => { init(); @@ -2351,7 +2378,7 @@ describe('LexicalEditor tests', () => { }); }); - it('can use flushSync for synchronous updates', () => { + it('can use discrete for synchronous updates', () => { init(); const onUpdate = jest.fn(); editor.registerUpdateListener(onUpdate); @@ -2373,6 +2400,90 @@ describe('LexicalEditor tests', () => { expect(onUpdate).toHaveBeenCalledTimes(1); }); + it('can use discrete after a non-discrete update to flush the entire queue', () => { + const headless = createTestHeadlessEditor(); + const onUpdate = jest.fn(); + headless.registerUpdateListener(onUpdate); + headless.update(() => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Async update')), + ); + }); + headless.update( + () => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Sync update')), + ); + }, + { + discrete: true, + }, + ); + + const textContent = headless + .getEditorState() + .read(() => $getRoot().getTextContent()); + expect(textContent).toBe('Async update\n\nSync update'); + expect(onUpdate).toHaveBeenCalledTimes(1); + }); + + it('can use discrete after a non-discrete setEditorState to flush the entire queue', () => { + init(); + editor.update( + () => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Async update')), + ); + }, + { + discrete: true, + }, + ); + + const headless = createTestHeadlessEditor(editor.getEditorState()); + headless.update( + () => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Sync update')), + ); + }, + { + discrete: true, + }, + ); + const textContent = headless + .getEditorState() + .read(() => $getRoot().getTextContent()); + expect(textContent).toBe('Async update\n\nSync update'); + }); + + it('can use discrete in a nested update to flush the entire queue', () => { + init(); + const onUpdate = jest.fn(); + editor.registerUpdateListener(onUpdate); + editor.update(() => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Async update')), + ); + editor.update( + () => { + $getRoot().append( + $createParagraphNode().append($createTextNode('Sync update')), + ); + }, + { + discrete: true, + }, + ); + }); + + const textContent = editor + .getEditorState() + .read(() => $getRoot().getTextContent()); + expect(textContent).toBe('Async update\n\nSync update'); + expect(onUpdate).toHaveBeenCalledTimes(1); + }); + it('does not include linebreak into inline elements', async () => { init(); diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index b1337764ddf..739fc760b54 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -534,9 +534,11 @@ export function createTestEditor( return editor; } -export function createTestHeadlessEditor(): LexicalEditor { +export function createTestHeadlessEditor( + editorState?: EditorState, +): LexicalEditor { return createHeadlessEditor({ - namespace: '', + editorState, onError: (error) => { throw error; }, From 666ccb2e4a20cd7d453d5b255f52426a954242df Mon Sep 17 00:00:00 2001 From: Francois Polo Date: Fri, 19 Jul 2024 00:48:15 -0700 Subject: [PATCH 031/103] fix(docs): correct typo in Lexical collaboration guide (#6421) --- packages/lexical-website/docs/collaboration/react.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-website/docs/collaboration/react.md b/packages/lexical-website/docs/collaboration/react.md index e5b7efa89f5..e7ebeab7b59 100644 --- a/packages/lexical-website/docs/collaboration/react.md +++ b/packages/lexical-website/docs/collaboration/react.md @@ -31,7 +31,7 @@ $ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs **Get WebSocket server running:** -This allows different browser windows and different borwsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing. +This allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing. ```bash $ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket From ca033da27606aae9aafd65a5b5dab663277603be Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Fri, 19 Jul 2024 19:30:12 +0800 Subject: [PATCH 032/103] docs: fix typo in editor.registerCommand() usage (#6429) --- 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 823bef0e564..825fc2e5d65 100644 --- a/packages/lexical-website/docs/intro.md +++ b/packages/lexical-website/docs/intro.md @@ -132,5 +132,5 @@ unregisterListener(); Commands are the communication system used to wire everything together in Lexical. Custom commands can be created using `createCommand()` and dispatched to an editor using `editor.dispatchCommand(command, payload)`. Lexical dispatches commands internally when key presses are triggered -and when other important signals occur. Commands can also be handled using `editor.registerCommand(handler, priority)`, and incoming commands are +and when other important signals occur. Commands can also be handled using `editor.registerCommand(command, handler, priority)`, and incoming commands are propagated through all handlers by priority until a handler stops the propagation (in a similar way to event propagation in the browser). From 40f6cac12c1f71a68c4b78c908cb02d86c008df5 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 19 Jul 2024 21:05:42 +0800 Subject: [PATCH 033/103] [lexical-react] update flow typing for draggable block plugin (#6426) --- .../src/plugins/DraggableBlockPlugin/index.tsx | 2 +- .../flow/LexicalDraggableBlockPlugin.js.flow | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx b/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx index 3675cb786e2..891932c94d0 100644 --- a/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/DraggableBlockPlugin/index.tsx @@ -16,7 +16,7 @@ function isOnMenu(element: HTMLElement): boolean { return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`); } -export default function PlaygroundDraggableBlockPlugin({ +export default function DraggableBlockPlugin({ anchorElem = document.body, }: { anchorElem?: HTMLElement; diff --git a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow index ef306bb38a6..e3eea057e2d 100644 --- a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow @@ -7,4 +7,15 @@ * @flow strict */ -declare export function DraggableBlockPlugin(): React$MixedElement; +import * as React from 'react'; + +type Props = $ReadOnly<{ + anchorElem?: HTMLElement, + menuRef: React.RefObject, + targetLineRef: React.RefObject, + menuComponent: React.Node, + targetLineComponent: React.Node, + isOnMenu: (element: HTMLElement) => boolean, +}>; + +declare export function DraggableBlockPlugin_EXPERIMENTAL(props: Props): React$MixedElement; From 0a3ecf4ad2a77f8c2515a08bbf065fe43df7550f Mon Sep 17 00:00:00 2001 From: JBWereRuss <64123760+JBWereRuss@users.noreply.github.com> Date: Sun, 21 Jul 2024 11:20:34 +1200 Subject: [PATCH 034/103] [lexical-playground][TableCellResizer] Bug Fix: Register event handlers on root element (#6416) Co-authored-by: Ivaylo Pavlov --- .../src/plugins/TableCellResizer/index.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 43290761f08..5551802209e 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -134,15 +134,19 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { }, 0); }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mousedown', onMouseDown); - document.addEventListener('mouseup', onMouseUp); + const removeRootListener = editor.registerRootListener((rootElement, prevRootElement) => { + rootElement?.addEventListener('mousemove', onMouseMove); + rootElement?.addEventListener('mousedown', onMouseDown); + rootElement?.addEventListener('mouseup', onMouseUp); + + prevRootElement?.removeEventListener('mousemove', onMouseMove); + prevRootElement?.removeEventListener('mousedown', onMouseDown); + prevRootElement?.removeEventListener('mouseup', onMouseUp); + }); return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mousedown', onMouseDown); - document.removeEventListener('mouseup', onMouseUp); - }; + removeRootListener(); + } }, [activeCell, draggingDirection, editor, resetState]); const isHeightChanging = (direction: MouseDraggingDirection) => { From 71880d22f63a8f2631352a7032a22f639238b306 Mon Sep 17 00:00:00 2001 From: Xuan <97ssps30212@gmail.com> Date: Sun, 21 Jul 2024 07:36:03 +0800 Subject: [PATCH 035/103] fix(LexicalNode): fix inline decorator isSelected (#5948) Co-authored-by: Ivaylo Pavlov --- packages/lexical/src/LexicalNode.ts | 40 +++++++-- .../src/__tests__/unit/LexicalNode.test.ts | 87 ++++++++++++++++++- 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index ebe8d56b8ea..f1b1559b59c 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -15,6 +15,7 @@ import invariant from 'shared/invariant'; import { $createParagraphNode, + $isDecoratorNode, $isElementNode, $isParagraphNode, $isRootNode, @@ -281,14 +282,41 @@ export class LexicalNode { } // For inline images inside of element nodes. // Without this change the image will be selected if the cursor is before or after it. - if ( + const isElementRangeSelection = $isRangeSelection(targetSelection) && targetSelection.anchor.type === 'element' && - targetSelection.focus.type === 'element' && - targetSelection.anchor.key === targetSelection.focus.key && - targetSelection.anchor.offset === targetSelection.focus.offset - ) { - return false; + targetSelection.focus.type === 'element'; + + if (isElementRangeSelection) { + if (targetSelection.isCollapsed()) { + return false; + } + + const parentNode = this.getParent(); + if ($isDecoratorNode(this) && this.isInline() && parentNode) { + const {anchor, focus} = targetSelection; + + if (anchor.isBefore(focus)) { + const anchorNode = anchor.getNode() as ElementNode; + const isAnchorPointToLast = + anchor.offset === anchorNode.getChildrenSize(); + const isAnchorNodeIsParent = anchorNode.is(parentNode); + const isLastChild = anchorNode.getLastChildOrThrow().is(this); + + if (isAnchorPointToLast && isAnchorNodeIsParent && isLastChild) { + return false; + } + } else { + const focusNode = focus.getNode() as ElementNode; + const isFocusPointToLast = + focus.offset === focusNode.getChildrenSize(); + const isFocusNodeIsParent = focusNode.is(parentNode); + const isLastChild = focusNode.getLastChildOrThrow().is(this); + if (isFocusPointToLast && isFocusNodeIsParent && isLastChild) { + return false; + } + } + } } return isSelected; } diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index 1f11b93b846..c34ad1a2643 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -10,6 +10,7 @@ import { $getRoot, $getSelection, $isRangeSelection, + DecoratorNode, ParagraphNode, TextNode, } from 'lexical'; @@ -47,6 +48,40 @@ class TestNode extends LexicalNode { } } +class InlineDecoratorNode extends DecoratorNode { + static getType(): string { + return 'inline-decorator'; + } + + static clone(): InlineDecoratorNode { + return new InlineDecoratorNode(); + } + + static importJSON() { + return new InlineDecoratorNode(); + } + + exportJSON() { + return {type: 'inline-decorator', version: 1}; + } + + createDOM(): HTMLElement { + return document.createElement('span'); + } + + isInline(): true { + return true; + } + + isParentRequired(): true { + return true; + } + + decorate() { + return 'inline-decorator'; + } +} + // This is a hack to bypass the node type validation on LexicalNode. We never want to create // an LexicalNode directly but we're testing the base functionality in this module. LexicalNode.getType = function () { @@ -266,6 +301,56 @@ describe('LexicalNode tests', () => { await Promise.resolve().then(); }); + test('LexicalNode.isSelected(): with inline decorator node', async () => { + const {editor} = testEnv; + let paragraphNode1: ParagraphNode; + let paragraphNode2: ParagraphNode; + let inlineDecoratorNode: InlineDecoratorNode; + + editor.update(() => { + paragraphNode1 = $createParagraphNode(); + paragraphNode2 = $createParagraphNode(); + inlineDecoratorNode = new InlineDecoratorNode(); + paragraphNode1.append(inlineDecoratorNode); + $getRoot().append(paragraphNode1, paragraphNode2); + paragraphNode1.selectEnd(); + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + expect(selection.anchor.getNode().is(paragraphNode1)).toBe(true); + } + }); + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + expect(selection.anchor.key).toBe(paragraphNode1.getKey()); + + selection.focus.set(paragraphNode2.getKey(), 1, 'element'); + } + }); + + await Promise.resolve().then(); + + editor.getEditorState().read(() => { + expect(inlineDecoratorNode.isSelected()).toBe(false); + }); + + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.anchor.set(paragraphNode2.getKey(), 0, 'element'); + selection.focus.set(paragraphNode1.getKey(), 1, 'element'); + } + }); + + await Promise.resolve().then(); + + editor.getEditorState().read(() => { + expect(inlineDecoratorNode.isSelected()).toBe(false); + }); + }); + test('LexicalNode.getKey()', async () => { expect(textNode.getKey()).toEqual(textNode.__key); }); @@ -1206,7 +1291,7 @@ describe('LexicalNode tests', () => { }, { namespace: '', - nodes: [LexicalNode, TestNode], + nodes: [LexicalNode, TestNode, InlineDecoratorNode], theme: {}, }, ); From 3a96788a2d01f7953d0a3bc954b59f91bf7ceb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Sun, 21 Jul 2024 07:40:49 -0300 Subject: [PATCH 036/103] [lexical-playground] Refactor: run prettier to fix CI (#6436) --- .../src/plugins/TableCellResizer/index.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 5551802209e..494a2c5e85a 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -134,19 +134,21 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { }, 0); }; - const removeRootListener = editor.registerRootListener((rootElement, prevRootElement) => { - rootElement?.addEventListener('mousemove', onMouseMove); - rootElement?.addEventListener('mousedown', onMouseDown); - rootElement?.addEventListener('mouseup', onMouseUp); - - prevRootElement?.removeEventListener('mousemove', onMouseMove); - prevRootElement?.removeEventListener('mousedown', onMouseDown); - prevRootElement?.removeEventListener('mouseup', onMouseUp); - }); + const removeRootListener = editor.registerRootListener( + (rootElement, prevRootElement) => { + rootElement?.addEventListener('mousemove', onMouseMove); + rootElement?.addEventListener('mousedown', onMouseDown); + rootElement?.addEventListener('mouseup', onMouseUp); + + prevRootElement?.removeEventListener('mousemove', onMouseMove); + prevRootElement?.removeEventListener('mousedown', onMouseDown); + prevRootElement?.removeEventListener('mouseup', onMouseUp); + }, + ); return () => { removeRootListener(); - } + }; }, [activeCell, draggingDirection, editor, resetState]); const isHeightChanging = (direction: MouseDraggingDirection) => { From 78abd30d0b2342f5a5ebd1ca0b0df4525756e8d1 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 22 Jul 2024 06:30:12 -0700 Subject: [PATCH 037/103] [lexical-history][lexical-selection][lexical-react] Fix: #6409 TextNode change detection (#6420) --- packages/lexical-clipboard/src/clipboard.ts | 13 +- .../__tests__/unit/LexicalHistory.test.tsx | 142 ++++++++++++++++-- packages/lexical-history/src/index.ts | 35 ++--- packages/lexical-html/src/index.ts | 8 +- .../src/LexicalContentEditable.tsx | 3 - .../lexical-react/src/LexicalHistoryPlugin.ts | 4 +- .../__tests__/unit/LexicalSelection.test.tsx | 58 +++---- packages/lexical-selection/src/index.ts | 5 +- .../lexical-selection/src/lexical-node.ts | 62 -------- packages/lexical-utils/src/index.ts | 9 +- .../src/components/Gallery/utils.tsx | 2 +- packages/lexical/flow/Lexical.js.flow | 1 + packages/lexical/src/LexicalNode.ts | 27 +--- packages/lexical/src/LexicalUtils.ts | 51 ++++++- packages/lexical/src/index.ts | 1 + 15 files changed, 251 insertions(+), 170 deletions(-) diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index cfd32fe6171..92f7d0a9c07 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -7,13 +7,10 @@ */ import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; -import { - $addNodeStyle, - $cloneWithProperties, - $sliceSelectedTextNodeContent, -} from '@lexical/selection'; +import {$addNodeStyle, $sliceSelectedTextNodeContent} from '@lexical/selection'; import {objectKlassEquals} from '@lexical/utils'; import { + $cloneWithProperties, $createTabNode, $getRoot, $getSelection, @@ -256,7 +253,7 @@ function $appendNodesToJSON( let target = currentNode; if (selection !== null) { - let clone = $cloneWithProperties(currentNode); + let clone = $cloneWithProperties(currentNode); clone = $isTextNode(clone) && selection !== null ? $sliceSelectedTextNodeContent(selection, clone) @@ -267,11 +264,11 @@ function $appendNodesToJSON( const serializedNode = exportNodeToJSON(target); - // TODO: TextNode calls getTextContent() (NOT node.__text) within it's exportJSON method + // TODO: TextNode calls getTextContent() (NOT node.__text) within its exportJSON method // which uses getLatest() to get the text from the original node with the same key. // This is a deeper issue with the word "clone" here, it's still a reference to the // same node as far as the LexicalEditor is concerned since it shares a key. - // We need a way to create a clone of a Node in memory with it's own key, but + // We need a way to create a clone of a Node in memory with its own key, but // until then this hack will work for the selected text extract use case. if ($isTextNode(target)) { const text = target.__text; diff --git a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx index 3a495a42e48..0cef7f59210 100644 --- a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx +++ b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx @@ -14,7 +14,9 @@ import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; import {$createQuoteNode} from '@lexical/rich-text'; import {$setBlocksType} from '@lexical/selection'; +import {$restoreEditorState} from '@lexical/utils'; import { + $applyNodeReplacement, $createNodeSelection, $createParagraphNode, $createRangeSelection, @@ -26,17 +28,81 @@ import { CAN_UNDO_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_CRITICAL, + type KlassConstructor, LexicalEditor, + LexicalNode, + type NodeKey, REDO_COMMAND, SerializedElementNode, - SerializedTextNode, + type SerializedTextNode, + type Spread, + TextNode, UNDO_COMMAND, -} from 'lexical/src'; +} from 'lexical'; import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils'; -import React from 'react'; import {createRoot, Root} from 'react-dom/client'; import * as ReactTestUtils from 'shared/react-test-utils'; +type SerializedCustomTextNode = Spread< + {type: ReturnType; classes: string[]}, + SerializedTextNode +>; + +class CustomTextNode extends TextNode { + ['constructor']!: KlassConstructor; + + __classes: Set; + constructor(text: string, classes: Iterable, key?: NodeKey) { + super(text, key); + this.__classes = new Set(classes); + } + static getType(): 'custom-text' { + return 'custom-text'; + } + static clone(node: CustomTextNode): CustomTextNode { + return new CustomTextNode(node.__text, node.__classes, node.__key); + } + addClass(className: string): this { + const self = this.getWritable(); + self.__classes.add(className); + return self; + } + removeClass(className: string): this { + const self = this.getWritable(); + self.__classes.delete(className); + return self; + } + setClasses(classes: Iterable): this { + const self = this.getWritable(); + self.__classes = new Set(classes); + return self; + } + getClasses(): ReadonlySet { + return this.getLatest().__classes; + } + static importJSON({text, classes}: SerializedCustomTextNode): CustomTextNode { + return $createCustomTextNode(text, classes); + } + exportJSON(): SerializedCustomTextNode { + return { + ...super.exportJSON(), + classes: Array.from(this.getClasses()), + type: this.constructor.getType(), + }; + } +} +function $createCustomTextNode( + text: string, + classes: string[] = [], +): CustomTextNode { + return $applyNodeReplacement(new CustomTextNode(text, classes)); +} +function $isCustomTextNode( + node: LexicalNode | null | undefined, +): node is CustomTextNode { + return node instanceof CustomTextNode; +} + describe('LexicalHistory tests', () => { let container: HTMLDivElement | null = null; let reactRoot: Root; @@ -59,13 +125,12 @@ describe('LexicalHistory tests', () => { // Shared instance across tests let editor: LexicalEditor; + function TestPlugin() { + // Plugin used just to get our hands on the Editor object + [editor] = useLexicalComposerContext(); + return null; + } function Test(): JSX.Element { - function TestPlugin() { - // Plugin used just to get our hands on the Editor object - [editor] = useLexicalComposerContext(); - return null; - } - return ( { await editor_.dispatchCommand(UNDO_COMMAND, undefined); expect($isNodeSelection(editor_.getEditorState()._selection)).toBe(true); }); + + test('Changes to TextNode leaf are detected properly #6409', async () => { + editor = createTestEditor({ + nodes: [CustomTextNode], + }); + const sharedHistory = createEmptyHistoryState(); + registerHistory(editor, sharedHistory, 0); + editor.update( + () => { + $getRoot() + .clear() + .append( + $createParagraphNode().append( + $createCustomTextNode('Initial text'), + ), + ); + }, + {discrete: true}, + ); + expect(sharedHistory.undoStack).toHaveLength(0); + + editor.update( + () => { + // Mark dirty with no changes + for (const node of $getRoot().getAllTextNodes()) { + node.getWritable(); + } + // Restore the editor state and ensure the history did not change + $restoreEditorState(editor, editor.getEditorState()); + }, + {discrete: true}, + ); + expect(sharedHistory.undoStack).toHaveLength(0); + editor.update( + () => { + // Mark dirty with text change + for (const node of $getRoot().getAllTextNodes()) { + if ($isCustomTextNode(node)) { + node.setTextContent(node.getTextContent() + '!'); + } + } + }, + {discrete: true}, + ); + expect(sharedHistory.undoStack).toHaveLength(1); + + editor.update( + () => { + // Mark dirty with only a change to the class + for (const node of $getRoot().getAllTextNodes()) { + if ($isCustomTextNode(node)) { + node.addClass('updated'); + } + } + }, + {discrete: true}, + ); + expect(sharedHistory.undoStack).toHaveLength(2); + }); }); const $createParagraphNodeWithText = (text: string) => { diff --git a/packages/lexical-history/src/index.ts b/packages/lexical-history/src/index.ts index aeecb524f93..8c731d3aaf4 100644 --- a/packages/lexical-history/src/index.ts +++ b/packages/lexical-history/src/index.ts @@ -193,25 +193,26 @@ function isTextNodeUnchanged( const prevSelection = prevEditorState._selection; const nextSelection = nextEditorState._selection; - let isDeletingLine = false; - - if ($isRangeSelection(prevSelection) && $isRangeSelection(nextSelection)) { - isDeletingLine = - prevSelection.anchor.type === 'element' && - prevSelection.focus.type === 'element' && - nextSelection.anchor.type === 'text' && - nextSelection.focus.type === 'text'; - } + const isDeletingLine = + $isRangeSelection(prevSelection) && + $isRangeSelection(nextSelection) && + prevSelection.anchor.type === 'element' && + prevSelection.focus.type === 'element' && + nextSelection.anchor.type === 'text' && + nextSelection.focus.type === 'text'; - if (!isDeletingLine && $isTextNode(prevNode) && $isTextNode(nextNode)) { + if ( + !isDeletingLine && + $isTextNode(prevNode) && + $isTextNode(nextNode) && + prevNode.__parent === nextNode.__parent + ) { + // This has the assumption that object key order won't change if the + // content did not change, which should normally be safe given + // the manner in which nodes and exportJSON are typically implemented. return ( - prevNode.__type === nextNode.__type && - prevNode.__text === nextNode.__text && - prevNode.__mode === nextNode.__mode && - prevNode.__detail === nextNode.__detail && - prevNode.__style === nextNode.__style && - prevNode.__format === nextNode.__format && - prevNode.__parent === nextNode.__parent + JSON.stringify(prevEditorState.read(() => prevNode.exportJSON())) === + JSON.stringify(nextEditorState.read(() => nextNode.exportJSON())) ); } return false; diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 858c5fdf0c2..2975315cc35 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -16,12 +16,10 @@ import type { LexicalNode, } from 'lexical'; -import { - $cloneWithProperties, - $sliceSelectedTextNodeContent, -} from '@lexical/selection'; +import {$sliceSelectedTextNodeContent} from '@lexical/selection'; import {isBlockDomNode, isHTMLElement} from '@lexical/utils'; import { + $cloneWithProperties, $createLineBreakNode, $createParagraphNode, $getRoot, @@ -103,7 +101,7 @@ function $appendNodesToHTML( let target = currentNode; if (selection !== null) { - let clone = $cloneWithProperties(currentNode); + let clone = $cloneWithProperties(currentNode); clone = $isTextNode(clone) && selection !== null ? $sliceSelectedTextNodeContent(selection, clone) diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 30829f6fb40..f94a5207395 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -15,7 +15,6 @@ import {forwardRef, Ref, useLayoutEffect, useState} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; -/* eslint-disable @typescript-eslint/ban-types */ export type Props = Omit & { editor__DEPRECATED?: LexicalEditor; } & ( @@ -31,8 +30,6 @@ export type Props = Omit & { } ); -/* eslint-enable @typescript-eslint/ban-types */ - export const ContentEditable = forwardRef(ContentEditableImpl); function ContentEditableImpl( diff --git a/packages/lexical-react/src/LexicalHistoryPlugin.ts b/packages/lexical-react/src/LexicalHistoryPlugin.ts index d8a50133ef5..c51f247a8e7 100644 --- a/packages/lexical-react/src/LexicalHistoryPlugin.ts +++ b/packages/lexical-react/src/LexicalHistoryPlugin.ts @@ -17,13 +17,15 @@ export {createEmptyHistoryState} from '@lexical/history'; export type {HistoryState}; export function HistoryPlugin({ + delay, externalHistoryState, }: { + delay?: number; externalHistoryState?: HistoryState; }): null { const [editor] = useLexicalComposerContext(); - useHistory(editor, externalHistoryState); + useHistory(editor, externalHistoryState, delay); return null; } diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index 09e56625b59..73cfd62c0d1 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -2272,42 +2272,44 @@ describe('LexicalSelection tests', () => { it('adjust offset for inline elements text formatting', async () => { await init(); - await editor!.update(() => { - const root = $getRoot(); + await ReactTestUtils.act(async () => { + await editor!.update(() => { + const root = $getRoot(); - const text1 = $createTextNode('--'); - const text2 = $createTextNode('abc'); - const text3 = $createTextNode('--'); + const text1 = $createTextNode('--'); + const text2 = $createTextNode('abc'); + const text3 = $createTextNode('--'); - root.append( - $createParagraphNode().append( - text1, - $createLinkNode('https://lexical.dev').append(text2), - text3, - ), - ); + root.append( + $createParagraphNode().append( + text1, + $createLinkNode('https://lexical.dev').append(text2), + text3, + ), + ); - $setAnchorPoint({ - key: text1.getKey(), - offset: 2, - type: 'text', - }); + $setAnchorPoint({ + key: text1.getKey(), + offset: 2, + type: 'text', + }); - $setFocusPoint({ - key: text3.getKey(), - offset: 0, - type: 'text', - }); + $setFocusPoint({ + key: text3.getKey(), + offset: 0, + type: 'text', + }); - const selection = $getSelection(); + const selection = $getSelection(); - if (!$isRangeSelection(selection)) { - return; - } + if (!$isRangeSelection(selection)) { + return; + } - selection.formatText('bold'); + selection.formatText('bold'); - expect(text2.hasFormat('bold')).toBe(true); + expect(text2.hasFormat('bold')).toBe(true); + }); }); }); }); diff --git a/packages/lexical-selection/src/index.ts b/packages/lexical-selection/src/index.ts index 512310ca7a2..b2d18b1645a 100644 --- a/packages/lexical-selection/src/index.ts +++ b/packages/lexical-selection/src/index.ts @@ -8,7 +8,6 @@ import { $addNodeStyle, - $cloneWithProperties, $isAtNodeEnd, $patchStyleText, $sliceSelectedTextNodeContent, @@ -30,9 +29,11 @@ import { getStyleObjectFromCSS, } from './utils'; +export { + /** @deprecated moved to the lexical package */ $cloneWithProperties, +} from 'lexical'; export { $addNodeStyle, - $cloneWithProperties, $isAtNodeEnd, $patchStyleText, $sliceSelectedTextNodeContent, diff --git a/packages/lexical-selection/src/lexical-node.ts b/packages/lexical-selection/src/lexical-node.ts index 4f6687fcc35..a8711031293 100644 --- a/packages/lexical-selection/src/lexical-node.ts +++ b/packages/lexical-selection/src/lexical-node.ts @@ -11,15 +11,12 @@ import { $getNodeByKey, $getPreviousSelection, $isElementNode, - $isParagraphNode, $isRangeSelection, $isRootNode, $isTextNode, BaseSelection, - ElementNode, LexicalEditor, LexicalNode, - ParagraphNode, Point, RangeSelection, TextNode, @@ -33,65 +30,6 @@ import { getStyleObjectFromRawCSS, } from './utils'; -function $updateElementNodeProperties( - target: T, - source: ElementNode, -): T { - target.__first = source.__first; - target.__last = source.__last; - target.__size = source.__size; - target.__format = source.__format; - target.__indent = source.__indent; - target.__dir = source.__dir; - return target; -} - -function $updateTextNodeProperties( - target: T, - source: TextNode, -): T { - target.__format = source.__format; - target.__style = source.__style; - target.__mode = source.__mode; - target.__detail = source.__detail; - return target; -} - -function $updateParagraphNodeProperties( - target: T, - source: ParagraphNode, -): T { - target.__textFormat = source.__textFormat; - return target; -} - -/** - * Returns a copy of a node, but generates a new key for the copy. - * @param node - The node to be cloned. - * @returns The clone of the node. - */ -export function $cloneWithProperties(node: T): T { - const constructor = node.constructor; - // @ts-expect-error - const clone: T = constructor.clone(node); - clone.__parent = node.__parent; - clone.__next = node.__next; - clone.__prev = node.__prev; - - if ($isElementNode(node) && $isElementNode(clone)) { - return $updateElementNodeProperties(clone, node); - } - - if ($isTextNode(node) && $isTextNode(clone)) { - return $updateTextNodeProperties(clone, node); - } - - if ($isParagraphNode(node) && $isParagraphNode(clone)) { - return $updateParagraphNodeProperties(clone, node); - } - return clone; -} - /** * Generally used to append text content to HTML and JSON. Grabs the text content and "slices" * it to be generated into the new TextNode. diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 38da22531b6..a8f7047bdfa 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -6,8 +6,8 @@ * */ -import {$cloneWithProperties} from '@lexical/selection'; import { + $cloneWithProperties, $createParagraphNode, $getPreviousSelection, $getRoot, @@ -446,12 +446,7 @@ export function $restoreEditorState( const activeEditorState = editor._pendingEditorState; for (const [key, node] of editorState._nodeMap) { - const clone = $cloneWithProperties(node); - if ($isTextNode(clone)) { - invariant($isTextNode(node), 'Expected node be a TextNode'); - clone.__text = node.__text; - } - nodeMap.set(key, clone); + nodeMap.set(key, $cloneWithProperties(node)); } if (activeEditorState) { diff --git a/packages/lexical-website/src/components/Gallery/utils.tsx b/packages/lexical-website/src/components/Gallery/utils.tsx index 4a908e835e8..cc28169f837 100644 --- a/packages/lexical-website/src/components/Gallery/utils.tsx +++ b/packages/lexical-website/src/components/Gallery/utils.tsx @@ -51,6 +51,6 @@ export function useFilteredExamples(examples: Array) { searchName, tags, }), - [examples, searchName], + [examples, searchName, tags], ); } diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 6744da5ed5d..e66e558a311 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -862,6 +862,7 @@ declare export function $hasAncestor( child: LexicalNode, targetNode: LexicalNode, ): boolean; +declare export function $cloneWithProperties(node: T): T; declare export function $copyNode( node: ElementNode, offset: number, diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index f1b1559b59c..8bb4b512f50 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -17,7 +17,6 @@ import { $createParagraphNode, $isDecoratorNode, $isElementNode, - $isParagraphNode, $isRootNode, $isTextNode, ElementNode, @@ -36,6 +35,7 @@ import { getActiveEditorState, } from './LexicalUpdates'; import { + $cloneWithProperties, $getCompositionKey, $getNodeByKey, $isRootOrShadowRoot, @@ -714,7 +714,6 @@ export class LexicalNode { const key = this.__key; // Ensure we get the latest node from pending state const latestNode = this.getLatest(); - const parent = latestNode.__parent; const cloneNotNeeded = editor._cloneNotNeeded; const selection = $getSelection(); if (selection !== null) { @@ -725,34 +724,12 @@ export class LexicalNode { internalMarkNodeAsDirty(latestNode); return latestNode; } - const constructor = latestNode.constructor; - const mutableNode = constructor.clone(latestNode); - mutableNode.__parent = parent; - mutableNode.__next = latestNode.__next; - mutableNode.__prev = latestNode.__prev; - if ($isElementNode(latestNode) && $isElementNode(mutableNode)) { - if ($isParagraphNode(latestNode) && $isParagraphNode(mutableNode)) { - mutableNode.__textFormat = latestNode.__textFormat; - } - mutableNode.__first = latestNode.__first; - mutableNode.__last = latestNode.__last; - mutableNode.__size = latestNode.__size; - mutableNode.__indent = latestNode.__indent; - mutableNode.__format = latestNode.__format; - mutableNode.__dir = latestNode.__dir; - } else if ($isTextNode(latestNode) && $isTextNode(mutableNode)) { - mutableNode.__format = latestNode.__format; - mutableNode.__style = latestNode.__style; - mutableNode.__mode = latestNode.__mode; - mutableNode.__detail = latestNode.__detail; - } + const mutableNode = $cloneWithProperties(latestNode); cloneNotNeeded.add(key); - mutableNode.__key = key; internalMarkNodeAsDirty(mutableNode); // Update reference in node map nodeMap.set(key, mutableNode); - // @ts-expect-error return mutableNode; } diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 52ac015239e..db227610851 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -41,6 +41,7 @@ import { $isDecoratorNode, $isElementNode, $isLineBreakNode, + $isParagraphNode, $isRangeSelection, $isRootNode, $isTextNode, @@ -1381,10 +1382,15 @@ export function $isRootOrShadowRoot( return $isRootNode(node) || ($isElementNode(node) && node.isShadowRoot()); } +/** + * Returns a shallow clone of node with a new key + * + * @param node - The node to be copied. + * @returns The copy of the node. + */ export function $copyNode(node: T): T { - const copy = node.constructor.clone(node); + const copy = node.constructor.clone(node) as T; $setNodeKey(copy, null); - // @ts-expect-error return copy; } @@ -1727,3 +1733,44 @@ export function getCachedTypeToNodeMap( } return typeToNodeMap; } + +/** + * Returns a clone of a node with the same key and parent/next/prev pointers and other + * properties that are not set by the KlassConstructor.clone (format, style, etc.). + * + * Does not mutate the EditorState. + * @param node - The node to be cloned. + * @returns The clone of the node. + */ +export function $cloneWithProperties(latestNode: T): T { + const constructor = latestNode.constructor; + const mutableNode = constructor.clone(latestNode) as T; + mutableNode.__parent = latestNode.__parent; + mutableNode.__next = latestNode.__next; + mutableNode.__prev = latestNode.__prev; + if ($isElementNode(latestNode) && $isElementNode(mutableNode)) { + if ($isParagraphNode(latestNode) && $isParagraphNode(mutableNode)) { + mutableNode.__textFormat = latestNode.__textFormat; + } + mutableNode.__first = latestNode.__first; + mutableNode.__last = latestNode.__last; + mutableNode.__size = latestNode.__size; + mutableNode.__indent = latestNode.__indent; + mutableNode.__format = latestNode.__format; + mutableNode.__dir = latestNode.__dir; + } else if ($isTextNode(latestNode) && $isTextNode(mutableNode)) { + mutableNode.__format = latestNode.__format; + mutableNode.__style = latestNode.__style; + mutableNode.__mode = latestNode.__mode; + mutableNode.__detail = latestNode.__detail; + } + if (__DEV__) { + invariant( + mutableNode.__key === latestNode.__key, + "$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor", + constructor.name, + constructor.getType(), + ); + } + return mutableNode; +} diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index bf5e51bcb84..5c93bcf7577 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -156,6 +156,7 @@ export {$parseSerializedNode, isCurrentlyReadOnlyMode} from './LexicalUpdates'; export { $addUpdateTag, $applyNodeReplacement, + $cloneWithProperties, $copyNode, $getAdjacentNode, $getEditor, From 0b58faf52ec19ec841d9c32f7d20d6f40a92392b Mon Sep 17 00:00:00 2001 From: Adrian Busse Date: Mon, 22 Jul 2024 16:22:27 +0100 Subject: [PATCH 038/103] [lexical][lexical-selection] Bug Fix: Respect mode when patching text style (#6428) Co-authored-by: Ivaylo Pavlov --- .../unit/LexicalSelectionHelpers.test.ts | 47 +++++++++++++++++++ .../lexical-selection/src/lexical-node.ts | 16 ++++--- packages/lexical/src/index.ts | 1 + 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts b/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts index 3d151f31b7d..01390ed7180 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts @@ -30,6 +30,7 @@ import { LexicalNode, ParagraphNode, RangeSelection, + TextModeType, TextNode, } from 'lexical'; import { @@ -3077,6 +3078,52 @@ describe('$patchStyleText', () => { }); }); + test.each(['token', 'segmented'])( + 'can update style of text node that is in %s mode', + async (mode) => { + const editor = createTestEditor(); + + const element = document.createElement('div'); + editor.setRootElement(element); + + await editor.update(() => { + const root = $getRoot(); + + const paragraph = $createParagraphNode(); + root.append(paragraph); + + const text = $createTextNode('first').setFormat('bold'); + paragraph.append(text); + + const textInMode = $createTextNode('second').setMode(mode); + paragraph.append(textInMode); + + $setAnchorPoint({ + key: text.getKey(), + offset: 'fir'.length, + type: 'text', + }); + + $setFocusPoint({ + key: textInMode.getKey(), + offset: 'sec'.length, + type: 'text', + }); + + const selection = $getSelection(); + $patchStyleText(selection!, {'font-size': '15px'}); + }); + + expect(element.innerHTML).toBe( + '

                    ' + + 'fir' + + 'st' + + 'second' + + '

                    ', + ); + }, + ); + test('preserve backward selection when changing style of 2 different text nodes', async () => { const editor = createTestEditor(); diff --git a/packages/lexical-selection/src/lexical-node.ts b/packages/lexical-selection/src/lexical-node.ts index a8711031293..b7b5a753ef0 100644 --- a/packages/lexical-selection/src/lexical-node.ts +++ b/packages/lexical-selection/src/lexical-node.ts @@ -14,6 +14,7 @@ import { $isRangeSelection, $isRootNode, $isTextNode, + $isTokenOrSegmented, BaseSelection, LexicalEditor, LexicalNode, @@ -342,8 +343,11 @@ export function $patchStyleText( return; } - // The entire node is selected, so just format it - if (startOffset === 0 && endOffset === firstNodeTextLength) { + // The entire node is selected or a token/segment, so just format it + if ( + $isTokenOrSegmented(firstNode) || + (startOffset === 0 && endOffset === firstNodeTextLength) + ) { $patchStyle(firstNode, patch); firstNode.select(startOffset, endOffset); } else { @@ -361,8 +365,8 @@ export function $patchStyleText( startOffset < firstNode.getTextContentSize() && firstNode.canHaveFormat() ) { - if (startOffset !== 0) { - // the entire first node isn't selected, so split it + if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) { + // the entire first node isn't selected and it isn't a token or segmented, so split it firstNode = firstNode.splitText(startOffset)[1]; startOffset = 0; if (isBefore) { @@ -387,8 +391,8 @@ export function $patchStyleText( endOffset = lastNodeTextLength; } - // if the entire last node isn't selected, split it - if (endOffset !== lastNodeTextLength) { + // if the entire last node isn't selected and it isn't a token or segmented, split it + if (endOffset !== lastNodeTextLength && !$isTokenOrSegmented(lastNode)) { [lastNode] = lastNode.splitText(endOffset); } diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 5c93bcf7577..de3a78ff645 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -170,6 +170,7 @@ export { $isInlineElementOrDecoratorNode, $isLeafNode, $isRootOrShadowRoot, + $isTokenOrSegmented, $nodesOfType, $selectAll, $setCompositionKey, From b88ce574fed70d0215f622307a157dc22ff06157 Mon Sep 17 00:00:00 2001 From: Maksym Plavinskyi Date: Mon, 22 Jul 2024 11:56:13 -0500 Subject: [PATCH 039/103] [lexical][auto-link] Fix auto link crash editor (#6433) Co-authored-by: Maksym Plavinskyi --- packages/lexical-link/src/index.ts | 10 + .../__tests__/e2e/AutoLinks.spec.mjs | 188 +++++++++++++++++- .../src/LexicalAutoLinkPlugin.ts | 18 +- 3 files changed, 211 insertions(+), 5 deletions(-) diff --git a/packages/lexical-link/src/index.ts b/packages/lexical-link/src/index.ts index 076f2f7e08a..fe2b9757048 100644 --- a/packages/lexical-link/src/index.ts +++ b/packages/lexical-link/src/index.ts @@ -276,6 +276,16 @@ export class LinkNode extends ElementNode { selection.getTextContent().length > 0 ); } + + isEmailURI(): boolean { + return this.__url.startsWith('mailto:'); + } + + isWebSiteURI(): boolean { + return ( + this.__url.startsWith('https://') || this.__url.startsWith('http://') + ); + } } function $convertAnchorElement(domNode: Node): DOMConversionOutput { diff --git a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs index 450fc8f61ff..1c63ec4f020 100644 --- a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs @@ -60,6 +60,35 @@ test.describe('Auto Links', () => { ); }); + test('Can convert url-like text into links for email', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type( + 'Hello name@example.com and anothername@test.example.uk !', + ); + await assertHTML( + page, + html` +

                    + Hello + + name@example.com + + and + + anothername@test.example.uk + + ! +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Can destruct links if add non-spacing text in front or right after it', async ({ page, isPlainText, @@ -159,6 +188,40 @@ test.describe('Auto Links', () => { ); }); + test('Can create link for email when pasting text with urls', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await pasteFromClipboard(page, { + 'text/plain': + 'Hello name@example.com and anothername@test.example.uk and www.example.com !', + }); + await assertHTML( + page, + html` +

                    + Hello + + name@example.com + + and + + anothername@test.example.uk + + and + + www.example.com + + ! +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Does not create redundant auto-link', async ({page, isPlainText}) => { test.skip(isPlainText); await focusEditor(page); @@ -204,7 +267,8 @@ test.describe('Auto Links', () => { test.skip(isPlainText); await focusEditor(page); await pasteFromClipboard(page, { - 'text/plain': 'https://1.com/,https://2.com/;;;https://3.com', + 'text/plain': + 'https://1.com/,https://2.com/;;;https://3.com;name@domain.uk;', }); await assertHTML( page, @@ -221,6 +285,11 @@ test.describe('Auto Links', () => { https://3.com + ; + + name@domain.uk + + ;

                    `, undefined, @@ -233,7 +302,7 @@ test.describe('Auto Links', () => { await focusEditor(page); await pasteFromClipboard(page, { 'text/plain': - 'https://1.com/ https://2.com/ https://3.com/ https://4.com/', + 'https://1.com/ https://2.com/ https://3.com/ https://4.com/ name-lastname@meta.com', }); await assertHTML( page, @@ -254,6 +323,10 @@ test.describe('Auto Links', () => { https://4.com/ + + + name-lastname@meta.com +

                    `, undefined, @@ -284,6 +357,37 @@ test.describe('Auto Links', () => { ); }); + test('Handles autolink following an invalid autolink to email', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type( + 'Hello name@example.c name@example.1 name-lastname@example.com name.lastname@meta.com', + ); + + await assertHTML( + page, + html` +

                    + + Hello name@example.c name@example.1 + + + name-lastname@example.com + + + + name.lastname@meta.com + +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Can convert url-like text with formatting into links', async ({ page, isPlainText, @@ -467,6 +571,47 @@ test.describe('Auto Links', () => { ); }); + test('Can convert URLs into email links', async ({page, isPlainText}) => { + const testUrls = [ + // Email usecases + 'email@domain.com', + 'firstname.lastname@domain.com', + 'email@subdomain.domain.com', + 'firstname+lastname@domain.com', + 'email@[123.123.123.123]', + '"email"@domain.com', + '1234567890@domain.com', + 'email@domain-one.com', + '_______@domain.com', + 'email@domain.name', + 'email@domain.co.uk', + 'firstname-lastname@domain.com', + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ') + ' '); + + let expectedHTML = ''; + for (const url of testUrls) { + expectedHTML += ` + + ${url} + + + `; + } + + await assertHTML( + page, + html` +

                    ${expectedHTML}

                    + `, + undefined, + {ignoreClasses: true}, + ); + }); + test(`Can not convert bad URLs into links`, async ({page, isPlainText}) => { const testUrls = [ // Missing Protocol @@ -515,6 +660,45 @@ test.describe('Auto Links', () => { ); }); + test(`Can not convert bad URLs into email links`, async ({ + page, + isPlainText, + }) => { + const testUrls = [ + '@domain.com', + '@subdomain.domain.com', + + // Invalid Characters + 'email@domain!.com', // Invalid character in domain + 'email@domain.c', // Invalid top level domain + + // Missing Domain + 'email@.com', + 'email@.org', + + // Incomplete URLs + 'email@', // Incomplete URL + + // Just Text + 'not_an_email', // Plain text + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ')); + + await assertHTML( + page, + html` +

                    + ${testUrls.join(' ')} +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Can unlink the autolink and then make it link again', async ({ page, isPlainText, diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index 5eaa8e85300..bb859ca82c6 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -91,8 +91,19 @@ function startsWithSeparator(textContent: string): boolean { return isSeparator(textContent[0]); } -function startsWithFullStop(textContent: string): boolean { - return /^\.[a-zA-Z0-9]{1,}/.test(textContent); +/** + * Check if the text content starts with a fullstop followed by a top-level domain. + * Meaning if the text content can be a beginning of a top level domain. + * @param textContent + * @param isEmail + * @returns boolean + */ +function startsWithTLD(textContent: string, isEmail: boolean): boolean { + if (isEmail) { + return /^\.[a-zA-Z]{2,}/.test(textContent); + } else { + return /^\.[a-zA-Z0-9]{1,}/.test(textContent); + } } function isPreviousNodeValid(node: LexicalNode): boolean { @@ -385,7 +396,8 @@ function handleBadNeighbors( if ( $isAutoLinkNode(previousSibling) && !previousSibling.getIsUnlinked() && - (!startsWithSeparator(text) || startsWithFullStop(text)) + (!startsWithSeparator(text) || + startsWithTLD(text, previousSibling.isEmailURI())) ) { previousSibling.append(textNode); handleLinkEdit(previousSibling, matchers, onChange); From 91b7b3e6414dd0f5336394330f0d1e9e7f377db3 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:51:27 +0800 Subject: [PATCH 040/103] [Lexical][Gallery] Create Simple Tableplugin example (#6445) --- examples/react-table/README.md | 7 + examples/react-table/index.html | 12 + examples/react-table/package-lock.json | 3235 +++++++++++++++++ examples/react-table/package.json | 24 + examples/react-table/public/icons/LICENSE.md | 5 + .../public/icons/arrow-clockwise.svg | 4 + .../public/icons/arrow-counterclockwise.svg | 4 + .../react-table/public/icons/journal-text.svg | 5 + examples/react-table/public/icons/justify.svg | 3 + .../react-table/public/icons/text-center.svg | 3 + .../react-table/public/icons/text-left.svg | 3 + .../public/icons/text-paragraph.svg | 3 + .../react-table/public/icons/text-right.svg | 3 + .../react-table/public/icons/type-bold.svg | 3 + .../react-table/public/icons/type-italic.svg | 3 + .../public/icons/type-strikethrough.svg | 3 + .../public/icons/type-underline.svg | 3 + examples/react-table/src/App.tsx | 95 + examples/react-table/src/ExampleTheme.ts | 56 + examples/react-table/src/main.tsx | 22 + .../react-table/src/plugins/ToolbarPlugin.tsx | 172 + .../src/plugins/TreeViewPlugin.tsx | 25 + examples/react-table/src/styles.css | 602 +++ examples/react-table/src/vite-env.d.ts | 1 + examples/react-table/tsconfig.json | 25 + examples/react-table/tsconfig.node.json | 11 + examples/react-table/vite.config.ts | 14 + 27 files changed, 4346 insertions(+) create mode 100644 examples/react-table/README.md create mode 100644 examples/react-table/index.html create mode 100644 examples/react-table/package-lock.json create mode 100644 examples/react-table/package.json create mode 100644 examples/react-table/public/icons/LICENSE.md create mode 100644 examples/react-table/public/icons/arrow-clockwise.svg create mode 100644 examples/react-table/public/icons/arrow-counterclockwise.svg create mode 100644 examples/react-table/public/icons/journal-text.svg create mode 100644 examples/react-table/public/icons/justify.svg create mode 100644 examples/react-table/public/icons/text-center.svg create mode 100644 examples/react-table/public/icons/text-left.svg create mode 100644 examples/react-table/public/icons/text-paragraph.svg create mode 100644 examples/react-table/public/icons/text-right.svg create mode 100644 examples/react-table/public/icons/type-bold.svg create mode 100644 examples/react-table/public/icons/type-italic.svg create mode 100644 examples/react-table/public/icons/type-strikethrough.svg create mode 100644 examples/react-table/public/icons/type-underline.svg create mode 100644 examples/react-table/src/App.tsx create mode 100644 examples/react-table/src/ExampleTheme.ts create mode 100644 examples/react-table/src/main.tsx create mode 100644 examples/react-table/src/plugins/ToolbarPlugin.tsx create mode 100644 examples/react-table/src/plugins/TreeViewPlugin.tsx create mode 100644 examples/react-table/src/styles.css create mode 100644 examples/react-table/src/vite-env.d.ts create mode 100644 examples/react-table/tsconfig.json create mode 100644 examples/react-table/tsconfig.node.json create mode 100644 examples/react-table/vite.config.ts diff --git a/examples/react-table/README.md b/examples/react-table/README.md new file mode 100644 index 00000000000..0d5c3ab00f9 --- /dev/null +++ b/examples/react-table/README.md @@ -0,0 +1,7 @@ +# React Table Plugin example + +Here we have simplest Lexical Table Plugin setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`) and accessibility (`@lexical/dragon`) features enabled. + +**Run it locally:** `npm i && npm run dev` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-table?file=src/main.tsx) diff --git a/examples/react-table/index.html b/examples/react-table/index.html new file mode 100644 index 00000000000..8243eabc6b9 --- /dev/null +++ b/examples/react-table/index.html @@ -0,0 +1,12 @@ + + + + + + Lexical React TablePlugin Example + + +
                    + + + diff --git a/examples/react-table/package-lock.json b/examples/react-table/package-lock.json new file mode 100644 index 00000000000..1bc6109b491 --- /dev/null +++ b/examples/react-table/package-lock.json @@ -0,0 +1,3235 @@ +{ + "name": "@lexical/react-table-example", + "version": "0.16.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@lexical/react-table-example", + "version": "0.16.1", + "dependencies": { + "@lexical/react": "0.16.1", + "lexical": "0.16.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.59", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.2.11" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz", + "integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lexical/clipboard": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.16.1.tgz", + "integrity": "sha512-0dWs/SwKS5KPpuf6fUVVt9vSCl6HAqcDGhSITw/okv0rrIlXTUT6WhVsMJtXfFxTyVvwMeOecJHvQH3i/jRQtA==", + "dependencies": { + "@lexical/html": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/code": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.16.1.tgz", + "integrity": "sha512-pOC28rRZ2XkmI2nIJm50DbKaCJtk5D0o7r6nORYp4i0z+lxt5Sf2m82DL9ksUHJRqKy87pwJDpoWvJ2SAI0ohw==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1", + "prismjs": "^1.27.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.16.1.tgz", + "integrity": "sha512-8CvGERGL7ySDVGLU+YPeq+JupIXsOFlXa3EuJ88koLKqXxYenwMleZgGqayFp6lCP78xqPKnATVeoOZUt/NabQ==", + "dependencies": { + "@lexical/html": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/mark": "0.16.1", + "@lexical/table": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.16.1.tgz", + "integrity": "sha512-Rvd60GIYN5kpjjBumS34EnNbBaNsoseI0AlzOdtIV302jiHPCLH0noe9kxzu9nZy+MZmjZy8Dx2zTbQT2mueRw==", + "dependencies": { + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.16.1.tgz", + "integrity": "sha512-G+YOxStAKs3q1utqm9KR4D4lCkwIH52Rctm4RgaVTI+4lvTaybeDRGFV75P/pI/qlF7/FvAYHTYEzCjtC3GNMQ==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/history": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.16.1.tgz", + "integrity": "sha512-WQhScx0TJeKSQAnEkRpIaWdUXqirrNrom2MxbBUc/32zEUMm9FzV7nRGknvUabEFUo7vZq6xTZpOExQJqHInQA==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/html": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.16.1.tgz", + "integrity": "sha512-vbtAdCvQ3PaAqa5mFmtmrvbiAvjCu1iXBAJ0bsHqFXCF2Sba5LwHVe8dUAOTpfEZEMbiHfjul6b5fj4vNPGF2A==", + "dependencies": { + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/link": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.16.1.tgz", + "integrity": "sha512-zG36gEnEqbIe6tK/MhXi7wn/XMY/zdivnPcOY5WyC3derkEezeLSSIFsC1u5UNeK5pbpNMSy4LDpLhi1Ww4Y5w==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/list": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.16.1.tgz", + "integrity": "sha512-i9YhLAh5N6YO9dP+R1SIL9WEdCKeTiQQYVUzj84vDvX5DIBxMPUjTmMn3LXu9T+QO3h1s2L/vJusZASrl45eAw==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/mark": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.16.1.tgz", + "integrity": "sha512-CZRGMLcxn5D+jzf1XnH+Z+uUugmpg1mBwTbGybCPm8UWpBrKDHkrscfMgWz62iRWz0cdVjM5+0zWpNElxFTRjQ==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.16.1.tgz", + "integrity": "sha512-0sBLttMvfQO/hVaIqpHdvDowpgV2CoRuWo2CNwvRLZPPWvPVjL4Nkb73wmi8zAZsAOTbX2aw+g4m/+k5oJqNig==", + "dependencies": { + "@lexical/code": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/rich-text": "0.16.1", + "@lexical/text": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/offset": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.16.1.tgz", + "integrity": "sha512-/i2J04lQmFeydUZIF8tKXLQTXiJDTQ6GRnkfv1OpxU4amc0rwGa7+qAz/PuF1n58rP6InpLmSHxgY5JztXa2jw==", + "dependencies": { + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.16.1.tgz", + "integrity": "sha512-xh5YpoxwA7K4wgMQF/Sjl8sdjaxqesLCtH5ZrcMsaPlmucDIEEs+i8xxk+kDUTEY7y+3FvRxs4lGNgX8RVWkvQ==", + "dependencies": { + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.16.1.tgz", + "integrity": "sha512-GjY4ylrBZIaAVIF8IFnmW0XGyHAuRmWA6gKB8iTTlsjgFrCHFIYC74EeJSp309O0Hflg9rRBnKoX1TYruFHVwA==", + "dependencies": { + "@lexical/clipboard": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/react": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.16.1.tgz", + "integrity": "sha512-SsGgLt9iKfrrMRy9lFb6ROVPUYOgv6b+mCn9Al+TLqs/gBReDBi3msA7m526nrtBUKYUnjHdQ1QXIJzuKgOxcg==", + "dependencies": { + "@lexical/clipboard": "0.16.1", + "@lexical/code": "0.16.1", + "@lexical/devtools-core": "0.16.1", + "@lexical/dragon": "0.16.1", + "@lexical/hashtag": "0.16.1", + "@lexical/history": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/mark": "0.16.1", + "@lexical/markdown": "0.16.1", + "@lexical/overflow": "0.16.1", + "@lexical/plain-text": "0.16.1", + "@lexical/rich-text": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/table": "0.16.1", + "@lexical/text": "0.16.1", + "@lexical/utils": "0.16.1", + "@lexical/yjs": "0.16.1", + "lexical": "0.16.1", + "react-error-boundary": "^3.1.4" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.16.1.tgz", + "integrity": "sha512-4uEVXJur7tdSbqbmsToCW4YVm0AMh4y9LK077Yq2O9hSuA5dqpI8UbTDnxZN2D7RfahNvwlqp8eZKFB1yeiJGQ==", + "dependencies": { + "@lexical/clipboard": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/selection": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.16.1.tgz", + "integrity": "sha512-+nK3RvXtyQvQDq7AZ46JpphmM33pwuulwiRfeXR5T9iFQTtgWOEjsAi/KKX7vGm70BxACfiSxy5QCOgBWFwVJg==", + "dependencies": { + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/table": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.16.1.tgz", + "integrity": "sha512-GWb0/MM1sVXpi1p2HWWOBldZXASMQ4c6WRNYnRmq7J/aB5N66HqQgJGKp3m66Kz4k1JjhmZfPs7F018qIBhnFQ==", + "dependencies": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.16.1.tgz", + "integrity": "sha512-Os/nKQegORTrKKN6vL3/FMVszyzyqaotlisPynvTaHTUC+yY4uyjM2hlF93i5a2ixxyiPLF9bDroxUP96TMPXg==", + "dependencies": { + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/utils": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.16.1.tgz", + "integrity": "sha512-BVyJxDQi/rIxFTDjf2zE7rMDKSuEaeJ4dybHRa/hRERt85gavGByQawSLeQlTjLaYLVsy+x7wCcqh2fNhlLf0g==", + "dependencies": { + "@lexical/list": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/table": "0.16.1", + "lexical": "0.16.1" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.16.1.tgz", + "integrity": "sha512-QHw1bmzB/IypIV1tRWMH4hhwE1xX7wV+HxbzBS8oJAkoU5AYXM/kyp/sQicgqiwVfpai1Px7zatOoUDFgbyzHQ==", + "dependencies": { + "@lexical/offset": "0.16.1", + "lexical": "0.16.1" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.61", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.61.tgz", + "integrity": "sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", + "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.690", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", + "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lexical": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.16.1.tgz", + "integrity": "sha512-+R05d3+N945OY8pTUjTqQrWoApjC+ctzvjnmNETtx9WmVAaiW0tQVG+AYLt5pDGY8dQXtd4RPorvnxBTECt9SA==" + }, + "node_modules/lib0": { + "version": "0.2.94", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.94.tgz", + "integrity": "sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yjs": { + "version": "13.6.18", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.18.tgz", + "integrity": "sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==", + "peer": true, + "dependencies": { + "lib0": "^0.2.86" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true + }, + "@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "requires": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dev": true, + "requires": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + } + }, + "@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "dev": true + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + } + }, + "@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "dev": true, + "optional": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.24.tgz", + "integrity": "sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@lexical/clipboard": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.16.1.tgz", + "integrity": "sha512-0dWs/SwKS5KPpuf6fUVVt9vSCl6HAqcDGhSITw/okv0rrIlXTUT6WhVsMJtXfFxTyVvwMeOecJHvQH3i/jRQtA==", + "requires": { + "@lexical/html": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/code": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.16.1.tgz", + "integrity": "sha512-pOC28rRZ2XkmI2nIJm50DbKaCJtk5D0o7r6nORYp4i0z+lxt5Sf2m82DL9ksUHJRqKy87pwJDpoWvJ2SAI0ohw==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1", + "prismjs": "^1.27.0" + } + }, + "@lexical/devtools-core": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.16.1.tgz", + "integrity": "sha512-8CvGERGL7ySDVGLU+YPeq+JupIXsOFlXa3EuJ88koLKqXxYenwMleZgGqayFp6lCP78xqPKnATVeoOZUt/NabQ==", + "requires": { + "@lexical/html": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/mark": "0.16.1", + "@lexical/table": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/dragon": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.16.1.tgz", + "integrity": "sha512-Rvd60GIYN5kpjjBumS34EnNbBaNsoseI0AlzOdtIV302jiHPCLH0noe9kxzu9nZy+MZmjZy8Dx2zTbQT2mueRw==", + "requires": { + "lexical": "0.16.1" + } + }, + "@lexical/hashtag": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.16.1.tgz", + "integrity": "sha512-G+YOxStAKs3q1utqm9KR4D4lCkwIH52Rctm4RgaVTI+4lvTaybeDRGFV75P/pI/qlF7/FvAYHTYEzCjtC3GNMQ==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/history": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.16.1.tgz", + "integrity": "sha512-WQhScx0TJeKSQAnEkRpIaWdUXqirrNrom2MxbBUc/32zEUMm9FzV7nRGknvUabEFUo7vZq6xTZpOExQJqHInQA==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/html": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.16.1.tgz", + "integrity": "sha512-vbtAdCvQ3PaAqa5mFmtmrvbiAvjCu1iXBAJ0bsHqFXCF2Sba5LwHVe8dUAOTpfEZEMbiHfjul6b5fj4vNPGF2A==", + "requires": { + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/link": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.16.1.tgz", + "integrity": "sha512-zG36gEnEqbIe6tK/MhXi7wn/XMY/zdivnPcOY5WyC3derkEezeLSSIFsC1u5UNeK5pbpNMSy4LDpLhi1Ww4Y5w==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/list": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.16.1.tgz", + "integrity": "sha512-i9YhLAh5N6YO9dP+R1SIL9WEdCKeTiQQYVUzj84vDvX5DIBxMPUjTmMn3LXu9T+QO3h1s2L/vJusZASrl45eAw==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/mark": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.16.1.tgz", + "integrity": "sha512-CZRGMLcxn5D+jzf1XnH+Z+uUugmpg1mBwTbGybCPm8UWpBrKDHkrscfMgWz62iRWz0cdVjM5+0zWpNElxFTRjQ==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/markdown": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.16.1.tgz", + "integrity": "sha512-0sBLttMvfQO/hVaIqpHdvDowpgV2CoRuWo2CNwvRLZPPWvPVjL4Nkb73wmi8zAZsAOTbX2aw+g4m/+k5oJqNig==", + "requires": { + "@lexical/code": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/rich-text": "0.16.1", + "@lexical/text": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/offset": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.16.1.tgz", + "integrity": "sha512-/i2J04lQmFeydUZIF8tKXLQTXiJDTQ6GRnkfv1OpxU4amc0rwGa7+qAz/PuF1n58rP6InpLmSHxgY5JztXa2jw==", + "requires": { + "lexical": "0.16.1" + } + }, + "@lexical/overflow": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.16.1.tgz", + "integrity": "sha512-xh5YpoxwA7K4wgMQF/Sjl8sdjaxqesLCtH5ZrcMsaPlmucDIEEs+i8xxk+kDUTEY7y+3FvRxs4lGNgX8RVWkvQ==", + "requires": { + "lexical": "0.16.1" + } + }, + "@lexical/plain-text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.16.1.tgz", + "integrity": "sha512-GjY4ylrBZIaAVIF8IFnmW0XGyHAuRmWA6gKB8iTTlsjgFrCHFIYC74EeJSp309O0Hflg9rRBnKoX1TYruFHVwA==", + "requires": { + "@lexical/clipboard": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/react": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.16.1.tgz", + "integrity": "sha512-SsGgLt9iKfrrMRy9lFb6ROVPUYOgv6b+mCn9Al+TLqs/gBReDBi3msA7m526nrtBUKYUnjHdQ1QXIJzuKgOxcg==", + "requires": { + "@lexical/clipboard": "0.16.1", + "@lexical/code": "0.16.1", + "@lexical/devtools-core": "0.16.1", + "@lexical/dragon": "0.16.1", + "@lexical/hashtag": "0.16.1", + "@lexical/history": "0.16.1", + "@lexical/link": "0.16.1", + "@lexical/list": "0.16.1", + "@lexical/mark": "0.16.1", + "@lexical/markdown": "0.16.1", + "@lexical/overflow": "0.16.1", + "@lexical/plain-text": "0.16.1", + "@lexical/rich-text": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/table": "0.16.1", + "@lexical/text": "0.16.1", + "@lexical/utils": "0.16.1", + "@lexical/yjs": "0.16.1", + "lexical": "0.16.1", + "react-error-boundary": "^3.1.4" + } + }, + "@lexical/rich-text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.16.1.tgz", + "integrity": "sha512-4uEVXJur7tdSbqbmsToCW4YVm0AMh4y9LK077Yq2O9hSuA5dqpI8UbTDnxZN2D7RfahNvwlqp8eZKFB1yeiJGQ==", + "requires": { + "@lexical/clipboard": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/selection": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.16.1.tgz", + "integrity": "sha512-+nK3RvXtyQvQDq7AZ46JpphmM33pwuulwiRfeXR5T9iFQTtgWOEjsAi/KKX7vGm70BxACfiSxy5QCOgBWFwVJg==", + "requires": { + "lexical": "0.16.1" + } + }, + "@lexical/table": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.16.1.tgz", + "integrity": "sha512-GWb0/MM1sVXpi1p2HWWOBldZXASMQ4c6WRNYnRmq7J/aB5N66HqQgJGKp3m66Kz4k1JjhmZfPs7F018qIBhnFQ==", + "requires": { + "@lexical/utils": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/text": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.16.1.tgz", + "integrity": "sha512-Os/nKQegORTrKKN6vL3/FMVszyzyqaotlisPynvTaHTUC+yY4uyjM2hlF93i5a2ixxyiPLF9bDroxUP96TMPXg==", + "requires": { + "lexical": "0.16.1" + } + }, + "@lexical/utils": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.16.1.tgz", + "integrity": "sha512-BVyJxDQi/rIxFTDjf2zE7rMDKSuEaeJ4dybHRa/hRERt85gavGByQawSLeQlTjLaYLVsy+x7wCcqh2fNhlLf0g==", + "requires": { + "@lexical/list": "0.16.1", + "@lexical/selection": "0.16.1", + "@lexical/table": "0.16.1", + "lexical": "0.16.1" + } + }, + "@lexical/yjs": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.16.1.tgz", + "integrity": "sha512-QHw1bmzB/IypIV1tRWMH4hhwE1xX7wV+HxbzBS8oJAkoU5AYXM/kyp/sQicgqiwVfpai1Px7zatOoUDFgbyzHQ==", + "requires": { + "@lexical/offset": "0.16.1", + "lexical": "0.16.1" + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "dev": true, + "optional": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "@types/react": { + "version": "18.2.61", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.61.tgz", + "integrity": "sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", + "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "requires": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "caniuse-lite": { + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "electron-to-chromium": { + "version": "1.4.690", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", + "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==", + "dev": true + }, + "esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "peer": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "lexical": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.16.1.tgz", + "integrity": "sha512-+R05d3+N945OY8pTUjTqQrWoApjC+ctzvjnmNETtx9WmVAaiW0tQVG+AYLt5pDGY8dQXtd4RPorvnxBTECt9SA==" + }, + "lib0": { + "version": "0.2.94", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.94.tgz", + "integrity": "sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==", + "peer": true, + "requires": { + "isomorphic.js": "^0.2.4" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + } + }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@types/estree": "1.0.5", + "fsevents": "~2.3.2" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "requires": { + "esbuild": "^0.20.1", + "fsevents": "~2.3.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yjs": { + "version": "13.6.18", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.18.tgz", + "integrity": "sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==", + "peer": true, + "requires": { + "lib0": "^0.2.86" + } + } + } +} diff --git a/examples/react-table/package.json b/examples/react-table/package.json new file mode 100644 index 00000000000..edc114ec39d --- /dev/null +++ b/examples/react-table/package.json @@ -0,0 +1,24 @@ +{ + "name": "@lexical/react-table-example", + "private": true, + "version": "0.16.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@lexical/react": "0.16.1", + "lexical": "0.16.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.59", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.2.11" + } +} diff --git a/examples/react-table/public/icons/LICENSE.md b/examples/react-table/public/icons/LICENSE.md new file mode 100644 index 00000000000..ce74f6abeed --- /dev/null +++ b/examples/react-table/public/icons/LICENSE.md @@ -0,0 +1,5 @@ +Bootstrap Icons +https://icons.getbootstrap.com + +Licensed under MIT license +https://github.com/twbs/icons/blob/main/LICENSE.md diff --git a/examples/react-table/public/icons/arrow-clockwise.svg b/examples/react-table/public/icons/arrow-clockwise.svg new file mode 100644 index 00000000000..b072eb097ab --- /dev/null +++ b/examples/react-table/public/icons/arrow-clockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/arrow-counterclockwise.svg b/examples/react-table/public/icons/arrow-counterclockwise.svg new file mode 100644 index 00000000000..b0b23b9bbc4 --- /dev/null +++ b/examples/react-table/public/icons/arrow-counterclockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/journal-text.svg b/examples/react-table/public/icons/journal-text.svg new file mode 100644 index 00000000000..9b66f43aab5 --- /dev/null +++ b/examples/react-table/public/icons/journal-text.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/justify.svg b/examples/react-table/public/icons/justify.svg new file mode 100644 index 00000000000..009bd7214d9 --- /dev/null +++ b/examples/react-table/public/icons/justify.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/text-center.svg b/examples/react-table/public/icons/text-center.svg new file mode 100644 index 00000000000..2887a99f267 --- /dev/null +++ b/examples/react-table/public/icons/text-center.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/text-left.svg b/examples/react-table/public/icons/text-left.svg new file mode 100644 index 00000000000..04526116489 --- /dev/null +++ b/examples/react-table/public/icons/text-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/text-paragraph.svg b/examples/react-table/public/icons/text-paragraph.svg new file mode 100644 index 00000000000..9779beabf1c --- /dev/null +++ b/examples/react-table/public/icons/text-paragraph.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/text-right.svg b/examples/react-table/public/icons/text-right.svg new file mode 100644 index 00000000000..34686b0f1ff --- /dev/null +++ b/examples/react-table/public/icons/text-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/type-bold.svg b/examples/react-table/public/icons/type-bold.svg new file mode 100644 index 00000000000..276d133c25c --- /dev/null +++ b/examples/react-table/public/icons/type-bold.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/type-italic.svg b/examples/react-table/public/icons/type-italic.svg new file mode 100644 index 00000000000..3ac6b09f02a --- /dev/null +++ b/examples/react-table/public/icons/type-italic.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/type-strikethrough.svg b/examples/react-table/public/icons/type-strikethrough.svg new file mode 100644 index 00000000000..1c940e42a87 --- /dev/null +++ b/examples/react-table/public/icons/type-strikethrough.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/public/icons/type-underline.svg b/examples/react-table/public/icons/type-underline.svg new file mode 100644 index 00000000000..c299b8bf2f0 --- /dev/null +++ b/examples/react-table/public/icons/type-underline.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/react-table/src/App.tsx b/examples/react-table/src/App.tsx new file mode 100644 index 00000000000..c42a31342f9 --- /dev/null +++ b/examples/react-table/src/App.tsx @@ -0,0 +1,95 @@ +/** + * 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 {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin'; +import {LexicalComposer} from '@lexical/react/LexicalComposer'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {ContentEditable} from '@lexical/react/LexicalContentEditable'; +import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary'; +import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; +import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; +import {TablePlugin} from '@lexical/react/LexicalTablePlugin'; +import { + INSERT_TABLE_COMMAND, + TableCellNode, + TableNode, + TableRowNode, +} from '@lexical/table'; +import {LexicalEditor} from 'lexical'; +import {useEffect, useState} from 'react'; + +import ExampleTheme from './ExampleTheme'; +import ToolbarPlugin from './plugins/ToolbarPlugin'; +import TreeViewPlugin from './plugins/TreeViewPlugin'; + +const editorConfig = { + namespace: 'React.js Demo', + nodes: [TableNode, TableCellNode, TableRowNode], + // Handling of errors during update + onError(error: Error) { + throw error; + }, + // The editor theme + theme: ExampleTheme, +}; + +const $updateEditorState = (editor: LexicalEditor) => { + editor.dispatchCommand(INSERT_TABLE_COMMAND, { + columns: String(3), + includeHeaders: true, + rows: String(3), + }); +}; + +function InsertTable({ + showTable, + setShowTable, +}: { + showTable: boolean; + setShowTable: React.Dispatch>; +}) { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + if (!showTable) { + setShowTable(true); + } + }, [showTable, setShowTable]); + + useEffect(() => { + if (showTable) { + $updateEditorState(editor); + } + }, [editor, showTable]); + return <>; +} + +function Placeholder() { + return
                    Enter some rich text...
                    ; +} + +export default function App() { + const [showTable, setShowTable] = useState(false); + return ( + +
                    + +
                    + } + placeholder={} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + +
                    +
                    +
                    + ); +} diff --git a/examples/react-table/src/ExampleTheme.ts b/examples/react-table/src/ExampleTheme.ts new file mode 100644 index 00000000000..ca6919c8757 --- /dev/null +++ b/examples/react-table/src/ExampleTheme.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. + * + */ +export default { + code: 'editor-code', + heading: { + h1: 'editor-heading-h1', + h2: 'editor-heading-h2', + h3: 'editor-heading-h3', + h4: 'editor-heading-h4', + h5: 'editor-heading-h5', + }, + image: 'editor-image', + link: 'editor-link', + list: { + listitem: 'editor-listitem', + nested: { + listitem: 'editor-nested-listitem', + }, + ol: 'editor-list-ol', + ul: 'editor-list-ul', + }, + ltr: 'ltr', + paragraph: 'editor-paragraph', + placeholder: 'editor-placeholder', + quote: 'editor-quote', + rtl: 'rtl', + table: 'ExampleEditorTheme__table', + tableCell: 'ExampleEditorTheme__tableCell', + 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: { + bold: 'editor-text-bold', + code: 'editor-text-code', + hashtag: 'editor-text-hashtag', + italic: 'editor-text-italic', + overflowed: 'editor-text-overflowed', + strikethrough: 'editor-text-strikethrough', + underline: 'editor-text-underline', + underlineStrikethrough: 'editor-text-underlineStrikethrough', + }, +}; diff --git a/examples/react-table/src/main.tsx b/examples/react-table/src/main.tsx new file mode 100644 index 00000000000..793ce25b6e6 --- /dev/null +++ b/examples/react-table/src/main.tsx @@ -0,0 +1,22 @@ +/** + * 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 './styles.css'; + +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App.tsx'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + +
                    +

                    Table Plugin Lexical Example

                    + +
                    +
                    , +); diff --git a/examples/react-table/src/plugins/ToolbarPlugin.tsx b/examples/react-table/src/plugins/ToolbarPlugin.tsx new file mode 100644 index 00000000000..c4357bd67a1 --- /dev/null +++ b/examples/react-table/src/plugins/ToolbarPlugin.tsx @@ -0,0 +1,172 @@ +/** + * 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 {mergeRegister} from '@lexical/utils'; +import { + $getSelection, + $isRangeSelection, + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + FORMAT_ELEMENT_COMMAND, + FORMAT_TEXT_COMMAND, + REDO_COMMAND, + SELECTION_CHANGE_COMMAND, + UNDO_COMMAND, +} from 'lexical'; +import {useCallback, useEffect, useRef, useState} from 'react'; + +const LowPriority = 1; + +function Divider() { + return
                    ; +} + +export default function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + + const $updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + // Update text format + setIsBold(selection.hasFormat('bold')); + setIsItalic(selection.hasFormat('italic')); + setIsUnderline(selection.hasFormat('underline')); + setIsStrikethrough(selection.hasFormat('strikethrough')); + } + }, []); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({editorState}) => { + editorState.read(() => { + $updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, _newEditor) => { + $updateToolbar(); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority, + ), + ); + }, [editor, $updateToolbar]); + + return ( +
                    + + + + + + + + + + + + {' '} +
                    + ); +} diff --git a/examples/react-table/src/plugins/TreeViewPlugin.tsx b/examples/react-table/src/plugins/TreeViewPlugin.tsx new file mode 100644 index 00000000000..3f8980b7e86 --- /dev/null +++ b/examples/react-table/src/plugins/TreeViewPlugin.tsx @@ -0,0 +1,25 @@ +/** + * 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 {TreeView} from '@lexical/react/LexicalTreeView'; + +export default function TreeViewPlugin(): JSX.Element { + const [editor] = useLexicalComposerContext(); + return ( + + ); +} diff --git a/examples/react-table/src/styles.css b/examples/react-table/src/styles.css new file mode 100644 index 00000000000..c6832a104f4 --- /dev/null +++ b/examples/react-table/src/styles.css @@ -0,0 +1,602 @@ +/** + * 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. + * + */ + +body { + margin: 0; + background: #eee; + font-family: system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular', + sans-serif; + font-weight: 500; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.other h2 { + font-size: 18px; + color: #444; + margin-bottom: 7px; +} + +.other a { + color: #777; + text-decoration: underline; + font-size: 14px; +} + +.other ul { + padding: 0; + margin: 0; + list-style-type: none; +} + +.App { + font-family: sans-serif; + text-align: center; +} + +h1 { + font-size: 24px; + color: #333; +} + +.ltr { + text-align: left; +} + +.rtl { + text-align: right; +} + +.editor-container { + margin: 20px auto 20px auto; + border-radius: 2px; + max-width: 600px; + color: #000; + position: relative; + line-height: 20px; + font-weight: 400; + text-align: left; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.editor-inner { + background: #fff; + position: relative; +} + +.editor-input { + min-height: 150px; + resize: none; + font-size: 15px; + caret-color: rgb(5, 5, 5); + position: relative; + tab-size: 1; + outline: 0; + padding: 15px 10px; + caret-color: #444; +} + +.editor-placeholder { + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 15px; + left: 10px; + font-size: 15px; + user-select: none; + display: inline-block; + pointer-events: none; +} + +.editor-text-bold { + font-weight: bold; +} + +.editor-text-italic { + font-style: italic; +} + +.editor-text-underline { + text-decoration: underline; +} + +.editor-text-strikethrough { + text-decoration: line-through; +} + +.editor-text-underlineStrikethrough { + text-decoration: underline line-through; +} + +.editor-text-code { + background-color: rgb(240, 242, 245); + padding: 1px 0.25rem; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 94%; +} + +.editor-link { + color: rgb(33, 111, 219); + text-decoration: none; +} + +.tree-view-output { + display: block; + background: #222; + color: #fff; + padding: 5px; + font-size: 12px; + white-space: pre-wrap; + margin: 1px auto 10px auto; + max-height: 250px; + position: relative; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + overflow: auto; + line-height: 14px; +} + +.editor-code { + background-color: rgb(240, 242, 245); + font-family: Menlo, Consolas, Monaco, monospace; + display: block; + padding: 8px 8px 8px 52px; + line-height: 1.53; + font-size: 13px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; + tab-size: 2; + /* white-space: pre; */ + overflow-x: auto; + position: relative; +} + +.editor-code:before { + content: attr(data-gutter); + position: absolute; + background-color: #eee; + left: 0; + top: 0; + border-right: 1px solid #ccc; + padding: 8px; + color: #777; + white-space: pre-wrap; + text-align: right; + min-width: 25px; +} +.editor-code:after { + content: attr(data-highlight-language); + top: 0; + right: 3px; + padding: 3px; + font-size: 10px; + text-transform: uppercase; + position: absolute; + color: rgba(0, 0, 0, 0.5); +} + +.editor-tokenComment { + color: slategray; +} + +.editor-tokenPunctuation { + color: #999; +} + +.editor-tokenProperty { + color: #905; +} + +.editor-tokenSelector { + color: #690; +} + +.editor-tokenOperator { + color: #9a6e3a; +} + +.editor-tokenAttr { + color: #07a; +} + +.editor-tokenVariable { + color: #e90; +} + +.editor-tokenFunction { + color: #dd4a68; +} + +.editor-paragraph { + margin: 0; + margin-bottom: 8px; + position: relative; +} + +.editor-paragraph:last-child { + margin-bottom: 0; +} + +.editor-heading-h1 { + font-size: 24px; + color: rgb(5, 5, 5); + font-weight: 400; + margin: 0; + margin-bottom: 12px; + padding: 0; +} + +.editor-heading-h2 { + font-size: 15px; + color: rgb(101, 103, 107); + font-weight: 700; + margin: 0; + margin-top: 10px; + padding: 0; + text-transform: uppercase; +} + +.editor-quote { + margin: 0; + margin-left: 20px; + font-size: 15px; + color: rgb(101, 103, 107); + border-left-color: rgb(206, 208, 212); + border-left-width: 4px; + border-left-style: solid; + padding-left: 16px; +} + +.editor-list-ol { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-list-ul { + padding: 0; + margin: 0; + margin-left: 16px; +} + +.editor-listitem { + margin: 8px 32px 8px 32px; +} + +.editor-nested-listitem { + list-style-type: none; +} + +pre::-webkit-scrollbar { + background: transparent; + width: 10px; +} + +pre::-webkit-scrollbar-thumb { + background: #999; +} + +.debug-timetravel-panel { + overflow: hidden; + padding: 0 0 10px 0; + margin: auto; + display: flex; +} + +.debug-timetravel-panel-slider { + padding: 0; + flex: 8; +} + +.debug-timetravel-panel-button { + padding: 0; + border: 0; + background: none; + flex: 1; + color: #fff; + font-size: 12px; +} + +.debug-timetravel-panel-button:hover { + text-decoration: underline; +} + +.debug-timetravel-button { + border: 0; + padding: 0; + font-size: 12px; + top: 10px; + right: 15px; + position: absolute; + background: none; + color: #fff; +} + +.debug-timetravel-button:hover { + text-decoration: underline; +} + +.toolbar { + display: flex; + margin-bottom: 1px; + background: #fff; + padding: 4px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + vertical-align: middle; +} + +.toolbar button.toolbar-item { + border: 0; + display: flex; + background: none; + border-radius: 10px; + padding: 8px; + cursor: pointer; + vertical-align: middle; +} + +.toolbar button.toolbar-item:disabled { + cursor: not-allowed; +} + +.toolbar button.toolbar-item.spaced { + margin-right: 2px; +} + +.toolbar button.toolbar-item i.format { + background-size: contain; + display: inline-block; + height: 18px; + width: 18px; + margin-top: 2px; + vertical-align: -0.25em; + display: flex; + opacity: 0.6; +} + +.toolbar button.toolbar-item:disabled i.format { + opacity: 0.2; +} + +.toolbar button.toolbar-item.active { + background-color: rgba(223, 232, 250, 0.3); +} + +.toolbar button.toolbar-item.active i { + opacity: 1; +} + +.toolbar .toolbar-item:hover:not([disabled]) { + background-color: #eee; +} + +.toolbar .divider { + width: 1px; + background-color: #eee; + margin: 0 4px; +} + +.toolbar .toolbar-item .text { + display: flex; + line-height: 20px; + width: 200px; + vertical-align: middle; + font-size: 14px; + color: #777; + text-overflow: ellipsis; + width: 70px; + overflow: hidden; + height: 20px; + text-align: left; +} + +.toolbar .toolbar-item .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 8px; + line-height: 16px; + background-size: contain; +} + +i.undo { + background-image: url(icons/arrow-counterclockwise.svg); +} + +i.redo { + background-image: url(icons/arrow-clockwise.svg); +} + +i.bold { + background-image: url(icons/type-bold.svg); +} + +i.italic { + background-image: url(icons/type-italic.svg); +} + +i.underline { + background-image: url(icons/type-underline.svg); +} + +i.strikethrough { + background-image: url(icons/type-strikethrough.svg); +} + +i.left-align { + background-image: url(icons/text-left.svg); +} + +i.center-align { + background-image: url(icons/text-center.svg); +} + +i.right-align { + background-image: url(icons/text-right.svg); +} + +i.justify-align { + background-image: url(icons/justify.svg); +} + +.ExampleEditorTheme__table { + border-collapse: collapse; + border-spacing: 0; + overflow-y: scroll; + overflow-x: scroll; + table-layout: fixed; + width: max-content; + margin: 0px 25px 30px 0px; +} +.ExampleEditorTheme__tableSelection *::selection { + background-color: transparent; +} +.ExampleEditorTheme__tableSelected { + outline: 2px solid rgb(60, 132, 244); +} +.ExampleEditorTheme__tableCell { + border: 1px solid #bbb; + width: 75px; + min-width: 75px; + vertical-align: top; + text-align: start; + padding: 6px 8px; + 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; + height: 100%; + width: 8px; + cursor: ew-resize; + z-index: 10; + top: 0; +} +.ExampleEditorTheme__tableCellHeader { + background-color: #f2f3f5; + text-align: start; +} +.ExampleEditorTheme__tableCellSelected { + background-color: #c9dbf0; +} +.ExampleEditorTheme__tableCellPrimarySelected { + border: 2px solid rgb(60, 132, 244); + display: block; + height: calc(100% - 2px); + 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; +} +.ExampleEditorTheme__tableAddColumns { + position: absolute; + background-color: #eee; + height: 100%; + animation: table-controls 0.2s ease; + border: 0; + cursor: pointer; +} +.ExampleEditorTheme__tableAddColumns:after { + background-image: url(../images/icons/plus.svg); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + display: block; + content: ' '; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.4; +} +.ExampleEditorTheme__tableAddColumns:hover, +.ExampleEditorTheme__tableAddRows:hover { + background-color: #c9dbf0; +} +.ExampleEditorTheme__tableAddRows { + position: absolute; + width: calc(100% - 25px); + background-color: #eee; + animation: table-controls 0.2s ease; + border: 0; + cursor: pointer; +} +.ExampleEditorTheme__tableAddRows:after { + background-image: url(../images/icons/plus.svg); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + display: block; + content: ' '; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.4; +} +@keyframes table-controls { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +.ExampleEditorTheme__tableCellResizeRuler { + display: block; + position: absolute; + width: 1px; + background-color: rgb(60, 132, 244); + height: 100%; + top: 0; +} +.ExampleEditorTheme__tableCellActionButtonContainer { + display: block; + right: 5px; + top: 6px; + position: absolute; + z-index: 4; + width: 20px; + height: 20px; +} +.ExampleEditorTheme__tableCellActionButton { + background-color: #eee; + display: block; + border: 0; + border-radius: 20px; + width: 20px; + height: 20px; + color: #222; + cursor: pointer; +} +.ExampleEditorTheme__tableCellActionButton:hover { + background-color: #ddd; +} diff --git a/examples/react-table/src/vite-env.d.ts b/examples/react-table/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/examples/react-table/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/react-table/tsconfig.json b/examples/react-table/tsconfig.json new file mode 100644 index 00000000000..49071185005 --- /dev/null +++ b/examples/react-table/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{"path": "./tsconfig.node.json"}] +} diff --git a/examples/react-table/tsconfig.node.json b/examples/react-table/tsconfig.node.json new file mode 100644 index 00000000000..97ede7ee6f2 --- /dev/null +++ b/examples/react-table/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react-table/vite.config.ts b/examples/react-table/vite.config.ts new file mode 100644 index 00000000000..2294526fc4f --- /dev/null +++ b/examples/react-table/vite.config.ts @@ -0,0 +1,14 @@ +/** + * 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 react from '@vitejs/plugin-react'; +import {defineConfig} from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); From 1b97d9fc3b969c8353de634b0e69876c6cce8176 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:12:17 +0800 Subject: [PATCH 041/103] [Lexical][CI] ignore running unit/integerity/e2e tests on examples folder code (#6446) --- .github/workflows/after-approval.yml | 2 +- .github/workflows/tests-extended.yml | 1 + .github/workflows/tests.yml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/after-approval.yml b/.github/workflows/after-approval.yml index 6fc743641d0..65fbe45e348 100644 --- a/.github/workflows/after-approval.yml +++ b/.github/workflows/after-approval.yml @@ -17,7 +17,7 @@ jobs: concurrent_skipping: 'same_content_newer' skip_after_successful_duplicate: 'true' do_not_skip: '["pull_request", "merge_group"]' - paths_ignore: '["packages/lexical-website/**", "packages/*/README.md", ".size-limit.js"]' + paths_ignore: '["packages/lexical-website/**", "packages/*/README.md", ".size-limit.js", "examples/**"]' e2e-tests: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' && (github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'extended-tests')) diff --git a/.github/workflows/tests-extended.yml b/.github/workflows/tests-extended.yml index fd1fad554ea..2a4fc3adc92 100644 --- a/.github/workflows/tests-extended.yml +++ b/.github/workflows/tests-extended.yml @@ -4,6 +4,7 @@ on: pull_request: types: [labeled, synchronize, reopened] paths-ignore: + - 'examples/**' - 'packages/lexical-website/**' - 'packages/*/README.md' - '.size-limit.js' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04abdc12933..5f5074869c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,10 +5,12 @@ on: branches: - main paths-ignore: + - 'examples/**' - 'packages/lexical-website/**' pull_request: types: [opened, synchronize, reopened] paths-ignore: + - 'examples/**' - 'packages/lexical-website/**' concurrency: From 91ba9d3753bf15410d3f5a03c5cec63a4e53ec37 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:05:13 +0800 Subject: [PATCH 042/103] [Lexical][Gallery] Add tableplugin example to gallery (#6447) --- .../lexical-website/src/components/Gallery/pluginList.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/lexical-website/src/components/Gallery/pluginList.tsx b/packages/lexical-website/src/components/Gallery/pluginList.tsx index 97deca02e34..0f1750f4145 100644 --- a/packages/lexical-website/src/components/Gallery/pluginList.tsx +++ b/packages/lexical-website/src/components/Gallery/pluginList.tsx @@ -32,4 +32,10 @@ export const plugins = (customFields: { title: 'Collab RichText', uri: 'https://stackblitz.com/github/facebook/lexical/tree/fix/collab_example/examples/react-rich-collab?ctl=0&file=src%2Fmain.tsx&terminalHeight=0&embed=1', }, + { + description: 'Learn how to create an editor with Tables', + tags: ['opensource', 'favorite'], + title: 'TablePlugin', + uri: `${customFields.STACKBLITZ_PREFIX}examples/react-table?embed=1&file=src%2Fmain.tsx&terminalHeight=0&ctl=0`, + }, ]; From 5a3ab1f28a247be633fff430b89ff441240dc53b Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Tue, 23 Jul 2024 09:06:15 +0100 Subject: [PATCH 043/103] [lexical] [lexical-selection] Preserve paragraph styles between lines (#6437) Co-authored-by: Ivaylo Pavlov --- .../src/generateContent.ts | 4 +- .../__tests__/e2e/Selection.spec.mjs | 45 +++++++++++++++++++ .../unit/LexicalTableSelection.test.tsx | 3 ++ packages/lexical/src/LexicalEvents.ts | 3 +- packages/lexical/src/LexicalReconciler.ts | 43 +++++++++++++++--- packages/lexical/src/LexicalUtils.ts | 2 + .../src/__tests__/unit/LexicalEditor.test.tsx | 11 ++++- .../__tests__/unit/LexicalEditorState.test.ts | 6 ++- .../unit/LexicalSerialization.test.ts | 4 +- .../lexical/src/nodes/LexicalElementNode.ts | 12 +++++ .../lexical/src/nodes/LexicalParagraphNode.ts | 17 +++++++ .../unit/LexicalParagraphNode.test.ts | 1 + 12 files changed, 138 insertions(+), 13 deletions(-) diff --git a/packages/lexical-devtools-core/src/generateContent.ts b/packages/lexical-devtools-core/src/generateContent.ts index 1c82869a4d6..fcb6bf253b5 100644 --- a/packages/lexical-devtools-core/src/generateContent.ts +++ b/packages/lexical-devtools-core/src/generateContent.ts @@ -284,7 +284,9 @@ function printNode( return `ids: [ ${node.getIDs().join(', ')} ]`; } else if ($isParagraphNode(node)) { const formatText = printTextFormatProperties(node); - return formatText !== '' ? `{ ${formatText} }` : ''; + let paragraphData = formatText !== '' ? `{ ${formatText} }` : ''; + paragraphData += node.__style ? `(${node.__style})` : ''; + return paragraphData; } else { return ''; } diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 428b177d53b..38a6d520dcf 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -773,6 +773,51 @@ test.describe.parallel('Selection', () => { ); }); + test('Can persist the text style (color) from the paragraph', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await click(page, '.color-picker'); + await click(page, '.color-picker-basic-color > button'); + await click(page, '.PlaygroundEditorTheme__paragraph'); + await page.keyboard.type('Line1'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Line2'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.type('Line3'); + await assertHTML( + page, + html` +

                    + + Line1 + +

                    +


                    +

                    + + Line3 + +

                    +

                    + + Line2 + +

                    + `, + ); + }); + test('shift+arrowdown into a table selects the whole table', async ({ page, isPlainText, diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx index a3dab04aef0..cd95aca65bc 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx @@ -134,6 +134,7 @@ describe('table selection', () => { __parent: null, __prev: null, __size: 1, + __style: '', __type: 'root', }); expect(parsedParagraph).toEqual({ @@ -147,7 +148,9 @@ describe('table selection', () => { __parent: 'root', __prev: null, __size: 1, + __style: '', __textFormat: 0, + __textStyle: '', __type: 'paragraph', }); expect(parsedText).toEqual({ diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index ed8e7db43e1..2836f924cf6 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -348,15 +348,16 @@ function onSelectionChange( selection.style = anchorNode.getStyle(); } else if (anchor.type === 'element' && !isRootTextContentEmpty) { const lastNode = anchor.getNode(); + selection.style = ''; if ( lastNode instanceof ParagraphNode && lastNode.getChildrenSize() === 0 ) { selection.format = lastNode.getTextFormat(); + selection.style = lastNode.getTextStyle(); } else { selection.format = 0; } - selection.style = ''; } } } else { diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 169d6fe3dff..462dd22863e 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -51,6 +51,7 @@ type IntentionallyMarkedAsDirtyElement = boolean; let subTreeTextContent = ''; let subTreeDirectionedTextContent = ''; let subTreeTextFormat: number | null = null; +let subTreeTextStyle: string = ''; let editorTextContent = ''; let activeEditorConfig: EditorConfig; let activeEditor: LexicalEditor; @@ -288,8 +289,13 @@ function $createChildren( for (; startIndex <= endIndex; ++startIndex) { $createNode(children[startIndex], dom, insertDOM); const node = activeNextNodeMap.get(children[startIndex]); - if (node !== null && subTreeTextFormat === null && $isTextNode(node)) { - subTreeTextFormat = node.getFormat(); + if (node !== null && $isTextNode(node)) { + if (subTreeTextFormat === null) { + subTreeTextFormat = node.getFormat(); + } + if (subTreeTextStyle === '') { + subTreeTextStyle = node.getStyle(); + } } } if ($textContentRequiresDoubleLinebreakAtEnd(element)) { @@ -356,6 +362,18 @@ function reconcileParagraphFormat(element: ElementNode): void { !activeEditorStateReadOnly ) { element.setTextFormat(subTreeTextFormat); + element.setTextStyle(subTreeTextStyle); + } +} + +function reconcileParagraphStyle(element: ElementNode): void { + if ( + $isParagraphNode(element) && + subTreeTextStyle !== '' && + subTreeTextStyle !== element.__textStyle && + !activeEditorStateReadOnly + ) { + element.setTextStyle(subTreeTextStyle); } } @@ -440,11 +458,12 @@ function $reconcileChildrenWithDirection( const previousSubTreeDirectionTextContent = subTreeDirectionedTextContent; subTreeDirectionedTextContent = ''; subTreeTextFormat = null; + subTreeTextStyle = ''; $reconcileChildren(prevElement, nextElement, dom); reconcileBlockDirection(nextElement, dom); reconcileParagraphFormat(nextElement); + reconcileParagraphStyle(nextElement); subTreeDirectionedTextContent = previousSubTreeDirectionTextContent; - subTreeTextFormat = null; } function createChildrenArray( @@ -486,8 +505,13 @@ function $reconcileChildren( destroyNode(prevFirstChildKey, null); } const nextChildNode = activeNextNodeMap.get(nextFrstChildKey); - if (subTreeTextFormat === null && $isTextNode(nextChildNode)) { - subTreeTextFormat = nextChildNode.getFormat(); + if ($isTextNode(nextChildNode)) { + if (subTreeTextFormat === null) { + subTreeTextFormat = nextChildNode.getFormat(); + } + if (subTreeTextStyle === '') { + subTreeTextStyle = nextChildNode.getStyle(); + } } } else { const prevChildren = createChildrenArray(prevElement, activePrevNodeMap); @@ -777,8 +801,13 @@ function $reconcileNodeChildren( } const node = activeNextNodeMap.get(nextKey); - if (node !== null && subTreeTextFormat === null && $isTextNode(node)) { - subTreeTextFormat = node.getFormat(); + if (node !== null && $isTextNode(node)) { + if (subTreeTextFormat === null) { + subTreeTextFormat = node.getFormat(); + } + if (subTreeTextStyle === '') { + subTreeTextStyle = node.getStyle(); + } } } diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index db227610851..61eb44ea77b 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1751,12 +1751,14 @@ export function $cloneWithProperties(latestNode: T): T { if ($isElementNode(latestNode) && $isElementNode(mutableNode)) { if ($isParagraphNode(latestNode) && $isParagraphNode(mutableNode)) { mutableNode.__textFormat = latestNode.__textFormat; + mutableNode.__textStyle = latestNode.__textStyle; } mutableNode.__first = latestNode.__first; mutableNode.__last = latestNode.__last; mutableNode.__size = latestNode.__size; mutableNode.__indent = latestNode.__indent; mutableNode.__format = latestNode.__format; + mutableNode.__style = latestNode.__style; mutableNode.__dir = latestNode.__dir; } else if ($isTextNode(latestNode) && $isTextNode(mutableNode)) { mutableNode.__format = latestNode.__format; diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index aa63aa14ac5..24a1901dd7a 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -1022,7 +1022,7 @@ describe('LexicalEditor tests', () => { editable ? 'editable' : 'non-editable' })`, async () => { const JSON_EDITOR_STATE = - '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; + '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"123","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; init(); const contentEditable = editor.getRootElement(); editor.setEditable(editable); @@ -1188,6 +1188,7 @@ describe('LexicalEditor tests', () => { __parent: null, __prev: null, __size: 1, + __style: '', __type: 'root', }); expect(paragraph).toEqual({ @@ -1201,7 +1202,9 @@ describe('LexicalEditor tests', () => { __parent: 'root', __prev: null, __size: 0, + __style: '', __textFormat: 0, + __textStyle: '', __type: 'paragraph', }); }); @@ -1272,6 +1275,7 @@ describe('LexicalEditor tests', () => { __parent: null, __prev: null, __size: 1, + __style: '', __type: 'root', }); expect(parsedParagraph).toEqual({ @@ -1285,7 +1289,9 @@ describe('LexicalEditor tests', () => { __parent: 'root', __prev: null, __size: 1, + __style: '', __textFormat: 0, + __textStyle: '', __type: 'paragraph', }); expect(parsedText).toEqual({ @@ -1351,6 +1357,7 @@ describe('LexicalEditor tests', () => { __parent: null, __prev: null, __size: 1, + __style: '', __type: 'root', }); expect(parsedParagraph).toEqual({ @@ -1364,7 +1371,9 @@ describe('LexicalEditor tests', () => { __parent: 'root', __prev: null, __size: 1, + __style: '', __textFormat: 0, + __textStyle: '', __type: 'paragraph', }); expect(parsedText).toEqual({ diff --git a/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts b/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts index 021a968b67d..09b49b738d7 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalEditorState.test.ts @@ -62,6 +62,7 @@ describe('LexicalEditorState tests', () => { __parent: null, __prev: null, __size: 1, + __style: '', __type: 'root', }); expect(paragraph).toEqual({ @@ -75,7 +76,9 @@ describe('LexicalEditorState tests', () => { __parent: 'root', __prev: null, __size: 1, + __style: '', __textFormat: 0, + __textStyle: '', __type: 'paragraph', }); expect(text).toEqual({ @@ -110,7 +113,7 @@ describe('LexicalEditorState tests', () => { }); expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual( - `{"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,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, + `{"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,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, ); }); @@ -145,6 +148,7 @@ describe('LexicalEditorState tests', () => { __parent: null, __prev: null, __size: 0, + __style: '', __type: 'root', }, ], diff --git a/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts b/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts index 21915802689..9237bc9d3dd 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts @@ -110,7 +110,7 @@ describe('LexicalSerialization tests', () => { }); const stringifiedEditorState = JSON.stringify(editor.getEditorState()); - const expectedStringifiedEditorState = `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Welcome to the playground","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"quote","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"The playground is a demo environment built with ","type":"text","version":1},{"detail":0,"format":16,"mode":"normal","style":"","text":"@lexical/react","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". Try typing in ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"some text","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" with ","type":"text","version":1},{"detail":0,"format":2,"mode":"normal","style":"","text":"different","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" formats.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"If you'd like to find out more about Lexical, you can:","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Visit the ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lexical website","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://lexical.dev/"},{"detail":0,"format":0,"mode":"normal","style":"","text":" for documentation and more information.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Check out the code on our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"GitHub repository","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":2},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Playground code can be found ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"here","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical/tree/main/packages/lexical-playground"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":3},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Join our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Discord Server","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://discord.com/invite/KmG4wQnnD9"},{"detail":0,"format":0,"mode":"normal","style":"","text":" and chat with the team.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":4}],"direction":"ltr","format":"","indent":0,"type":"list","version":1,"listType":"bullet","start":1,"tag":"ul"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"const lexical = \\"awesome\\"","type":"code-highlight","version":1}],"direction":"ltr","format":"","indent":0,"type":"code","version":1,"language":"javascript"},{"children":[{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1}],"direction":"ltr","format":"","indent":0,"type":"table","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`; + const expectedStringifiedEditorState = `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Welcome to the playground","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"quote","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"The playground is a demo environment built with ","type":"text","version":1},{"detail":0,"format":16,"mode":"normal","style":"","text":"@lexical/react","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". Try typing in ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"some text","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" with ","type":"text","version":1},{"detail":0,"format":2,"mode":"normal","style":"","text":"different","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" formats.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"If you'd like to find out more about Lexical, you can:","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Visit the ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lexical website","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://lexical.dev/"},{"detail":0,"format":0,"mode":"normal","style":"","text":" for documentation and more information.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Check out the code on our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"GitHub repository","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":2},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Playground code can be found ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"here","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical/tree/main/packages/lexical-playground"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":3},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Join our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Discord Server","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://discord.com/invite/KmG4wQnnD9"},{"detail":0,"format":0,"mode":"normal","style":"","text":" and chat with the team.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":4}],"direction":"ltr","format":"","indent":0,"type":"list","version":1,"listType":"bullet","start":1,"tag":"ul"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"const lexical = \\"awesome\\"","type":"code-highlight","version":1}],"direction":"ltr","format":"","indent":0,"type":"code","version":1,"language":"javascript"},{"children":[{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":"ltr","format":"","indent":0,"type":"tablerow","version":1}],"direction":"ltr","format":"","indent":0,"type":"table","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`; expect(stringifiedEditorState).toBe(expectedStringifiedEditorState); @@ -119,7 +119,7 @@ describe('LexicalSerialization tests', () => { const otherStringifiedEditorState = JSON.stringify(editorState); expect(otherStringifiedEditorState).toBe( - `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Welcome to the playground","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"quote","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"The playground is a demo environment built with ","type":"text","version":1},{"detail":0,"format":16,"mode":"normal","style":"","text":"@lexical/react","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". Try typing in ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"some text","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" with ","type":"text","version":1},{"detail":0,"format":2,"mode":"normal","style":"","text":"different","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" formats.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"If you'd like to find out more about Lexical, you can:","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Visit the ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lexical website","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://lexical.dev/"},{"detail":0,"format":0,"mode":"normal","style":"","text":" for documentation and more information.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Check out the code on our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"GitHub repository","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":2},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Playground code can be found ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"here","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical/tree/main/packages/lexical-playground"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":3},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Join our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Discord Server","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://discord.com/invite/KmG4wQnnD9"},{"detail":0,"format":0,"mode":"normal","style":"","text":" and chat with the team.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":4}],"direction":"ltr","format":"","indent":0,"type":"list","version":1,"listType":"bullet","start":1,"tag":"ul"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"const lexical = \\"awesome\\"","type":"code-highlight","version":1}],"direction":"ltr","format":"","indent":0,"type":"code","version":1,"language":"javascript"},{"children":[{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, + `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Welcome to the playground","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"quote","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"The playground is a demo environment built with ","type":"text","version":1},{"detail":0,"format":16,"mode":"normal","style":"","text":"@lexical/react","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". Try typing in ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"some text","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" with ","type":"text","version":1},{"detail":0,"format":2,"mode":"normal","style":"","text":"different","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" formats.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"If you'd like to find out more about Lexical, you can:","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Visit the ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lexical website","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://lexical.dev/"},{"detail":0,"format":0,"mode":"normal","style":"","text":" for documentation and more information.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Check out the code on our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"GitHub repository","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":2},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Playground code can be found ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"here","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://github.com/facebook/lexical/tree/main/packages/lexical-playground"},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":3},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Join our ","type":"text","version":1},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Discord Server","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"title":null,"url":"https://discord.com/invite/KmG4wQnnD9"},{"detail":0,"format":0,"mode":"normal","style":"","text":" and chat with the team.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"listitem","version":1,"value":4}],"direction":"ltr","format":"","indent":0,"type":"list","version":1,"listType":"bullet","start":1,"tag":"ul"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"const lexical = \\"awesome\\"","type":"code-highlight","version":1}],"direction":"ltr","format":"","indent":0,"type":"code","version":1,"language":"javascript"},{"children":[{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1},{"children":[{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":2,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1},{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":0,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, ); }); }); diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index 2707fb318de..c4510eff132 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -69,6 +69,8 @@ export class ElementNode extends LexicalNode { /** @internal */ __format: number; /** @internal */ + __style: string; + /** @internal */ __indent: number; /** @internal */ __dir: 'ltr' | 'rtl' | null; @@ -79,6 +81,7 @@ export class ElementNode extends LexicalNode { this.__last = null; this.__size = 0; this.__format = 0; + this.__style = ''; this.__indent = 0; this.__dir = null; } @@ -91,6 +94,10 @@ export class ElementNode extends LexicalNode { const format = this.getFormat(); return ELEMENT_FORMAT_TO_TYPE[format] || ''; } + getStyle(): string { + const self = this.getLatest(); + return self.__style; + } getIndent(): number { const self = this.getLatest(); return self.__indent; @@ -358,6 +365,11 @@ export class ElementNode extends LexicalNode { self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0; return this; } + setStyle(style: string): this { + const self = this.getWritable(); + self.__style = style || ''; + return this; + } setIndent(indentLevel: number): this { const self = this.getWritable(); self.__indent = indentLevel; diff --git a/packages/lexical/src/nodes/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index e13e6c41efb..56649528897 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -37,6 +37,7 @@ import {$isTextNode, TextFormatType} from './LexicalTextNode'; export type SerializedParagraphNode = Spread< { textFormat: number; + textStyle: string; }, SerializedElementNode >; @@ -46,10 +47,12 @@ export class ParagraphNode extends ElementNode { ['constructor']!: KlassConstructor; /** @internal */ __textFormat: number; + __textStyle: string; constructor(key?: NodeKey) { super(key); this.__textFormat = 0; + this.__textStyle = ''; } static getType(): string { @@ -72,6 +75,17 @@ export class ParagraphNode extends ElementNode { return (this.getTextFormat() & formatFlag) !== 0; } + getTextStyle(): string { + const self = this.getLatest(); + return self.__textStyle; + } + + setTextStyle(style: string): this { + const self = this.getWritable(); + self.__textStyle = style; + return self; + } + static clone(node: ParagraphNode): ParagraphNode { return new ParagraphNode(node.__key); } @@ -145,6 +159,7 @@ export class ParagraphNode extends ElementNode { return { ...super.exportJSON(), textFormat: this.getTextFormat(), + textStyle: this.getTextStyle(), type: 'paragraph', version: 1, }; @@ -158,9 +173,11 @@ export class ParagraphNode extends ElementNode { ): ParagraphNode { const newElement = $createParagraphNode(); newElement.setTextFormat(rangeSelection.format); + newElement.setTextStyle(rangeSelection.style); const direction = this.getDirection(); newElement.setDirection(direction); newElement.setFormat(this.getFormatType()); + newElement.setStyle(this.getTextStyle()); this.insertAfter(newElement, restoreSelection); return newElement; } diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalParagraphNode.test.ts b/packages/lexical/src/nodes/__tests__/unit/LexicalParagraphNode.test.ts index 483251398e8..1f7c4cfc3a7 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalParagraphNode.test.ts +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalParagraphNode.test.ts @@ -53,6 +53,7 @@ describe('LexicalParagraphNode tests', () => { format: '', indent: 0, textFormat: 0, + textStyle: '', type: 'paragraph', version: 1, }); From 14c63f65449db65e335d89dfb990875e2041980c Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 23 Jul 2024 02:22:54 -0700 Subject: [PATCH 044/103] [lexical] Bug Fix: getCachedTypeToNodeMap should handle a empty and writable EditorState (#6444) --- packages/lexical/src/LexicalEditor.ts | 2 +- packages/lexical/src/LexicalUtils.ts | 6 ++++++ .../lexical/src/__tests__/unit/LexicalEditor.test.tsx | 10 +++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 4ee42ca838d..e08665f1c02 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -903,7 +903,7 @@ export class LexicalEditor { klass: Klass, ): void { const prevEditorState = this._editorState; - const nodeMap = getCachedTypeToNodeMap(this._editorState).get( + const nodeMap = getCachedTypeToNodeMap(prevEditorState).get( klass.getType(), ); if (!nodeMap) { diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 61eb44ea77b..9a4880b1dc4 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1710,9 +1710,15 @@ export type TypeToNodeMap = Map; * Compute a cached Map of node type to nodes for a frozen EditorState */ const cachedNodeMaps = new WeakMap(); +const EMPTY_TYPE_TO_NODE_MAP: TypeToNodeMap = new Map(); export function getCachedTypeToNodeMap( editorState: EditorState, ): TypeToNodeMap { + // If this is a new Editor it may have a writable this._editorState + // with only a 'root' entry. + if (!editorState._readOnly && editorState.isEmpty()) { + return EMPTY_TYPE_TO_NODE_MAP; + } invariant( editorState._readOnly, 'getCachedTypeToNodeMap called with a writable EditorState', diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 24a1901dd7a..cc81f334678 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -35,6 +35,7 @@ import { COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_LOW, createCommand, + createEditor, EditorState, ElementNode, type Klass, @@ -1816,7 +1817,14 @@ describe('LexicalEditor tests', () => { expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('destroyed'); expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('destroyed'); }); - + it('mutation listener on newly initialized editor', async () => { + editor = createEditor(); + const textNodeMutations = jest.fn(); + editor.registerMutationListener(TextNode, textNodeMutations, { + skipInitialization: false, + }); + expect(textNodeMutations.mock.calls.length).toBe(0); + }); it('mutation listener with setEditorState', async () => { init(); From 24b58d8129a317f3467a6e81360fef0f042f04e4 Mon Sep 17 00:00:00 2001 From: Sherry Date: Tue, 23 Jul 2024 17:36:51 +0800 Subject: [PATCH 045/103] [lexical-react]: sync format in flow file (#6448) --- .../lexical-react/flow/LexicalDraggableBlockPlugin.js.flow | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow index e3eea057e2d..27cde072690 100644 --- a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow @@ -17,5 +17,7 @@ type Props = $ReadOnly<{ targetLineComponent: React.Node, isOnMenu: (element: HTMLElement) => boolean, }>; - -declare export function DraggableBlockPlugin_EXPERIMENTAL(props: Props): React$MixedElement; + +declare export function DraggableBlockPlugin_EXPERIMENTAL( + props: Props, +): React$MixedElement; From 5a7d9c71d8b877f77f7d618ff228159433f4d520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:11:31 -0300 Subject: [PATCH 046/103] [lexical-rich-text] Bug Fix: HeadingNode.insertNewAfter (#6435) --- .../__tests__/unit/LexicalHeadingNode.test.ts | 52 ++++++++++++++++++- packages/lexical-rich-text/src/index.ts | 8 ++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts index 39b10047c4a..057999ba031 100644 --- a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts +++ b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts @@ -84,7 +84,7 @@ describe('LexicalHeadingNode tests', () => { }); }); - test('HeadingNode.insertNewAfter()', async () => { + test('HeadingNode.insertNewAfter() empty', async () => { const {editor} = testEnv; let headingNode: HeadingNode; await editor.update(() => { @@ -106,6 +106,56 @@ describe('LexicalHeadingNode tests', () => { ); }); + test('HeadingNode.insertNewAfter() middle', async () => { + const {editor} = testEnv; + let headingNode: HeadingNode; + await editor.update(() => { + const root = $getRoot(); + headingNode = new HeadingNode('h1'); + const headingTextNode = $createTextNode('hello world'); + root.append(headingNode.append(headingTextNode)); + headingTextNode.select(5, 5); + }); + expect(testEnv.outerHTML).toBe( + '

                    hello world

                    ', + ); + await editor.update(() => { + const selection = $getSelection() as RangeSelection; + const result = headingNode.insertNewAfter(selection); + expect(result).toBeInstanceOf(HeadingNode); + expect(result.getDirection()).toEqual(headingNode.getDirection()); + }); + expect(testEnv.outerHTML).toBe( + '

                    hello world


                    ', + ); + }); + + test('HeadingNode.insertNewAfter() end', async () => { + const {editor} = testEnv; + let headingNode: HeadingNode; + await editor.update(() => { + const root = $getRoot(); + headingNode = new HeadingNode('h1'); + const headingTextNode1 = $createTextNode('hello'); + const headingTextNode2 = $createTextNode(' world'); + headingTextNode2.setFormat('bold'); + root.append(headingNode.append(headingTextNode1, headingTextNode2)); + headingTextNode2.selectEnd(); + }); + expect(testEnv.outerHTML).toBe( + '

                    hello world

                    ', + ); + await editor.update(() => { + const selection = $getSelection() as RangeSelection; + const result = headingNode.insertNewAfter(selection); + expect(result).toBeInstanceOf(ParagraphNode); + expect(result.getDirection()).toEqual(headingNode.getDirection()); + }); + expect(testEnv.outerHTML).toBe( + '

                    hello world


                    ', + ); + }); + test('$createHeadingNode()', async () => { const {editor} = testEnv; await editor.update(() => { diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index ba984f1d5c8..9bbbc18d46b 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -355,8 +355,14 @@ export class HeadingNode extends ElementNode { restoreSelection = true, ): ParagraphNode | HeadingNode { const anchorOffet = selection ? selection.anchor.offset : 0; + const lastDesc = this.getLastDescendant(); + const isAtEnd = + !lastDesc || + (selection && + selection.anchor.key === lastDesc.getKey() && + anchorOffet === lastDesc.getTextContentSize()); const newElement = - anchorOffet === this.getTextContentSize() || !selection + isAtEnd || !selection ? $createParagraphNode() : $createHeadingNode(this.getTag()); const direction = this.getDirection(); From eb364a1fc521a51d8c88898b30f0eab79bbd3639 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 25 Jul 2024 03:18:11 +0100 Subject: [PATCH 047/103] Flow add tags type to OnChange plugin (#6457) --- packages/lexical-react/flow/LexicalOnChangePlugin.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/flow/LexicalOnChangePlugin.js.flow b/packages/lexical-react/flow/LexicalOnChangePlugin.js.flow index 5c14f992d14..3b3ddbb6ce5 100644 --- a/packages/lexical-react/flow/LexicalOnChangePlugin.js.flow +++ b/packages/lexical-react/flow/LexicalOnChangePlugin.js.flow @@ -12,5 +12,5 @@ import type {EditorState, LexicalEditor} from 'lexical'; declare export function OnChangePlugin({ ignoreHistoryMergeTagChange?: boolean, ignoreSelectionChange?: boolean, - onChange: (editorState: EditorState, editor: LexicalEditor) => void, + onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set) => void, }): null; From 23865fc2020ea7574cb5ffe36416f3858e82e086 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 26 Jul 2024 12:53:28 +0800 Subject: [PATCH 048/103] CI: dont cancel other test runs if e2e flaky job fails (#6460) --- .github/workflows/call-e2e-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/call-e2e-test.yml b/.github/workflows/call-e2e-test.yml index cc2c89aeedb..fe7b194c973 100644 --- a/.github/workflows/call-e2e-test.yml +++ b/.github/workflows/call-e2e-test.yml @@ -15,6 +15,7 @@ on: jobs: e2e-test: runs-on: ${{ inputs.os }} + continue-on-error: ${{ inputs.flaky }} if: (inputs.browser != 'webkit' || inputs.os == 'macos-latest') && (inputs.editor-mode != 'rich-text-with-collab' || inputs.events-mode != 'legacy-events') env: CI: true From 02d1b5b78314b98d975929b109f17a69929fc53c Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 25 Jul 2024 21:55:17 -0700 Subject: [PATCH 049/103] [lexical] Bug Fix: Allow getTopLevelElement to return a DecoratorNode (#6458) --- packages/lexical/flow/Lexical.js.flow | 12 +++++-- packages/lexical/src/LexicalNode.ts | 9 ++--- .../src/__tests__/unit/LexicalNode.test.ts | 33 +++++++++++++++++++ .../lexical/src/nodes/LexicalDecoratorNode.ts | 8 +++++ .../lexical/src/nodes/LexicalElementNode.ts | 7 ++++ packages/lexical/src/nodes/LexicalTextNode.ts | 8 +++++ 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index e66e558a311..3a8937cedfa 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -386,8 +386,8 @@ declare export class LexicalNode { getIndexWithinParent(): number; getParent(): T | null; getParentOrThrow(): T; - getTopLevelElement(): ElementNode | null; - getTopLevelElementOrThrow(): ElementNode; + getTopLevelElement(): DecoratorNode | ElementNode | null; + getTopLevelElementOrThrow(): DecoratorNode | ElementNode; getParents(): Array; getParentKeys(): Array; getPreviousSibling(): T | null; @@ -589,6 +589,8 @@ declare export class TextNode extends LexicalNode { static getType(): string; static clone(node: $FlowFixMe): TextNode; constructor(text: string, key?: NodeKey): void; + getTopLevelElement(): ElementNode | null; + getTopLevelElementOrThrow(): ElementNode; getFormat(): number; getStyle(): string; isComposing(): boolean; @@ -724,6 +726,8 @@ declare export class ElementNode extends LexicalNode { __indent: number; __dir: 'ltr' | 'rtl' | null; constructor(key?: NodeKey): void; + getTopLevelElement(): ElementNode | null; + getTopLevelElementOrThrow(): ElementNode; getFormat(): number; getFormatType(): ElementFormatType; getIndent(): number; @@ -790,6 +794,10 @@ declare export function $isElementNode( declare export class DecoratorNode extends LexicalNode { constructor(key?: NodeKey): void; + // Not sure how to get flow to agree that the DecoratorNode is compatible with this, + // so we have a less precise type than in TS + // getTopLevelElement(): this | ElementNode | null; + // getTopLevelElementOrThrow(): this | ElementNode; decorate(editor: LexicalEditor, config: EditorConfig): X; isIsolated(): boolean; isInline(): boolean; diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 8bb4b512f50..75b102f4a9d 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -19,6 +19,7 @@ import { $isElementNode, $isRootNode, $isTextNode, + type DecoratorNode, ElementNode, } from '.'; import { @@ -376,14 +377,14 @@ export class LexicalNode { * non-root ancestor of this node, or null if none is found. See {@link lexical!$isRootOrShadowRoot} * for more information on which Elements comprise "roots". */ - getTopLevelElement(): ElementNode | null { + getTopLevelElement(): ElementNode | DecoratorNode | null { let node: ElementNode | this | null = this; while (node !== null) { const parent: ElementNode | null = node.getParent(); if ($isRootOrShadowRoot(parent)) { invariant( - $isElementNode(node), - 'Children of root nodes must be elements', + $isElementNode(node) || (node === this && $isDecoratorNode(node)), + 'Children of root nodes must be elements or decorators', ); return node; } @@ -397,7 +398,7 @@ export class LexicalNode { * non-root ancestor of this node, or throws if none is found. See {@link lexical!$isRootOrShadowRoot} * for more information on which Elements comprise "roots". */ - getTopLevelElementOrThrow(): ElementNode { + getTopLevelElementOrThrow(): ElementNode | DecoratorNode { const parent = this.getTopLevelElement(); if (parent === null) { invariant( diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index c34ad1a2643..c9a178d4e7c 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -9,8 +9,11 @@ import { $getRoot, $getSelection, + $isDecoratorNode, + $isElementNode, $isRangeSelection, DecoratorNode, + ElementNode, ParagraphNode, TextNode, } from 'lexical'; @@ -400,6 +403,30 @@ describe('LexicalNode tests', () => { expect(paragraphNode.getTopLevelElement()).toBe(paragraphNode); }); expect(() => textNode.getTopLevelElement()).toThrow(); + await editor.update(() => { + const node = new InlineDecoratorNode(); + expect(node.getTopLevelElement()).toBe(null); + $getRoot().append(node); + expect(node.getTopLevelElement()).toBe(node); + }); + editor.getEditorState().read(() => { + const elementNodes: ElementNode[] = []; + const decoratorNodes: DecoratorNode[] = []; + for (const child of $getRoot().getChildren()) { + expect(child.getTopLevelElement()).toBe(child); + if ($isElementNode(child)) { + elementNodes.push(child); + } else if ($isDecoratorNode(child)) { + decoratorNodes.push(child); + } else { + throw new Error( + 'Expecting all children to be ElementNode or DecoratorNode', + ); + } + } + expect(decoratorNodes).toHaveLength(1); + expect(elementNodes).toHaveLength(1); + }); }); test('LexicalNode.getTopLevelElementOrThrow()', async () => { @@ -415,6 +442,12 @@ describe('LexicalNode tests', () => { expect(paragraphNode.getTopLevelElementOrThrow()).toBe(paragraphNode); }); expect(() => textNode.getTopLevelElementOrThrow()).toThrow(); + await editor.update(() => { + const node = new InlineDecoratorNode(); + expect(() => node.getTopLevelElementOrThrow()).toThrow(); + $getRoot().append(node); + expect(node.getTopLevelElementOrThrow()).toBe(node); + }); }); test('LexicalNode.getParents()', async () => { diff --git a/packages/lexical/src/nodes/LexicalDecoratorNode.ts b/packages/lexical/src/nodes/LexicalDecoratorNode.ts index 16beeda931f..5c5c7cb11c4 100644 --- a/packages/lexical/src/nodes/LexicalDecoratorNode.ts +++ b/packages/lexical/src/nodes/LexicalDecoratorNode.ts @@ -8,13 +8,21 @@ import type {KlassConstructor, LexicalEditor} from '../LexicalEditor'; import type {NodeKey} from '../LexicalNode'; +import type {ElementNode} from './LexicalElementNode'; import {EditorConfig} from 'lexical'; import invariant from 'shared/invariant'; import {LexicalNode} from '../LexicalNode'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface DecoratorNode { + getTopLevelElement(): ElementNode | this | null; + getTopLevelElementOrThrow(): ElementNode | this; +} + /** @noInheritDoc */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class DecoratorNode extends LexicalNode { ['constructor']!: KlassConstructor>; constructor(key?: NodeKey) { diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index c4510eff132..bacb0113299 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -57,7 +57,14 @@ export type ElementFormatType = | 'justify' | ''; +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface ElementNode { + getTopLevelElement(): ElementNode | null; + getTopLevelElementOrThrow(): ElementNode; +} + /** @noInheritDoc */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class ElementNode extends LexicalNode { ['constructor']!: KlassConstructor; /** @internal */ diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 59bafff183d..c735a945099 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -21,6 +21,7 @@ import type { SerializedLexicalNode, } from '../LexicalNode'; import type {BaseSelection, RangeSelection} from '../LexicalSelection'; +import type {ElementNode} from './LexicalElementNode'; import {IS_FIREFOX} from 'shared/environment'; import invariant from 'shared/invariant'; @@ -274,7 +275,14 @@ function wrapElementWith( return el; } +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface TextNode { + getTopLevelElement(): ElementNode | null; + getTopLevelElementOrThrow(): ElementNode; +} + /** @noInheritDoc */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class TextNode extends LexicalNode { ['constructor']!: KlassConstructor; __text: string; From d7aadab4ba5b65b464d348caadaaf13dd83a3232 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 26 Jul 2024 20:38:37 +0800 Subject: [PATCH 050/103] CI: tag flaky tests (#6462) --- .../__tests__/e2e/List.spec.mjs | 119 ++++++------ .../__tests__/e2e/Selection.spec.mjs | 42 +++-- .../__tests__/e2e/Tables.spec.mjs | 172 +++++++++--------- 3 files changed, 172 insertions(+), 161 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index ef1b7018ff7..3f7abe34433 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -1304,68 +1304,71 @@ test.describe.parallel('Nested List', () => { ); }); - test('can navigate and check/uncheck with keyboard', async ({ - page, - isCollab, - }) => { - await focusEditor(page); - await toggleCheckList(page); - // - // [ ] a - // [ ] b - // [ ] c - // [ ] d - // [ ] e - // [ ] f - await page.keyboard.type('a'); - await page.keyboard.press('Enter'); - await page.keyboard.type('b'); - await page.keyboard.press('Enter'); - await click(page, '.toolbar-item.alignment'); - await click(page, 'button:has-text("Indent")'); - await page.keyboard.type('c'); - await page.keyboard.press('Enter'); - await click(page, '.toolbar-item.alignment'); - await click(page, 'button:has-text("Indent")'); - await page.keyboard.type('d'); - await page.keyboard.press('Enter'); - await page.keyboard.type('e'); - await page.keyboard.press('Enter'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.type('f'); - - const assertCheckCount = async (checkCount, uncheckCount) => { - const pageOrFrame = await (isCollab ? page.frame('left') : page); - await expect( - pageOrFrame.locator('li[role="checkbox"][aria-checked="true"]'), - ).toHaveCount(checkCount); - await expect( - pageOrFrame.locator('li[role="checkbox"][aria-checked="false"]'), - ).toHaveCount(uncheckCount); - }; - - await assertCheckCount(0, 6); - - // Go back to select checkbox - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('Space'); - - await repeat(5, async () => { - await page.keyboard.press('ArrowUp', {delay: 50}); + test( + 'can navigate and check/uncheck with keyboard', + { + tag: '@flaky', + }, + async ({page, isCollab}) => { + await focusEditor(page); + await toggleCheckList(page); + // + // [ ] a + // [ ] b + // [ ] c + // [ ] d + // [ ] e + // [ ] f + await page.keyboard.type('a'); + await page.keyboard.press('Enter'); + await page.keyboard.type('b'); + await page.keyboard.press('Enter'); + await click(page, '.toolbar-item.alignment'); + await click(page, 'button:has-text("Indent")'); + await page.keyboard.type('c'); + await page.keyboard.press('Enter'); + await click(page, '.toolbar-item.alignment'); + await click(page, 'button:has-text("Indent")'); + await page.keyboard.type('d'); + await page.keyboard.press('Enter'); + await page.keyboard.type('e'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('f'); + + const assertCheckCount = async (checkCount, uncheckCount) => { + const pageOrFrame = await (isCollab ? page.frame('left') : page); + await expect( + pageOrFrame.locator('li[role="checkbox"][aria-checked="true"]'), + ).toHaveCount(checkCount); + await expect( + pageOrFrame.locator('li[role="checkbox"][aria-checked="false"]'), + ).toHaveCount(uncheckCount); + }; + + await assertCheckCount(0, 6); + + // Go back to select checkbox + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); await page.keyboard.press('Space'); - }); - await assertCheckCount(6, 0); + await repeat(5, async () => { + await page.keyboard.press('ArrowUp', {delay: 50}); + await page.keyboard.press('Space'); + }); - await repeat(3, async () => { - await page.keyboard.press('ArrowDown', {delay: 50}); - await page.keyboard.press('Space'); - }); + await assertCheckCount(6, 0); - await assertCheckCount(3, 3); - }); + await repeat(3, async () => { + await page.keyboard.press('ArrowDown', {delay: 50}); + await page.keyboard.press('Space'); + }); + + await assertCheckCount(3, 3); + }, + ); test('replaces existing element node', async ({page}) => { // Create two quote blocks, select it and format to a list diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 38a6d520dcf..aa674960347 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -480,25 +480,31 @@ test.describe.parallel('Selection', () => { ); }); - test('Can delete sibling elements forward', async ({page, isPlainText}) => { - test.skip(isPlainText); + test( + 'Can delete sibling elements forward', + { + tag: '@flaky', + }, + async ({page, isPlainText}) => { + test.skip(isPlainText); - await focusEditor(page); - await page.keyboard.press('Enter'); - await page.keyboard.type('# Title'); - await page.keyboard.press('ArrowUp'); - await deleteForward(page); - await assertHTML( - page, - html` -

                    - Title -

                    - `, - ); - }); + await focusEditor(page); + await page.keyboard.press('Enter'); + await page.keyboard.type('# Title'); + await page.keyboard.press('ArrowUp'); + await deleteForward(page); + await assertHTML( + page, + html` +

                    + Title +

                    + `, + ); + }, + ); test('Can adjust tripple click selection', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 424205955fe..e4dde016730 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -875,96 +875,98 @@ test.describe.parallel('Tables', () => { ); }); - test(`Can style text using Table selection`, async ({ - page, - isPlainText, - isCollab, - }) => { - await initialize({isCollab, page}); - test.skip(isPlainText); + test( + `Can style text using Table selection`, + { + tag: '@flaky', + }, + async ({page, isPlainText, isCollab}) => { + await initialize({isCollab, page}); + test.skip(isPlainText); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await clickSelectors(page, ['.bold', '.italic', '.underline']); + await clickSelectors(page, ['.bold', '.italic', '.underline']); - await selectFromAdditionalStylesDropdown(page, '.strikethrough'); + await selectFromAdditionalStylesDropdown(page, '.strikethrough'); - // Check that the character styles are applied. - await assertHTML( - page, - html` -


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

                    a

                    -
                    -

                    bb

                    -
                    -

                    cc

                    -
                    -

                    d

                    -
                    -

                    e

                    -
                    -

                    f

                    -
                    -


                    - `, - html` -


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

                    a

                    -
                    -

                    bb

                    -
                    -

                    cc

                    -
                    -

                    d

                    -
                    -

                    e

                    -
                    -

                    f

                    -
                    -


                    - `, - {ignoreClasses: true}, - ); - }); + // Check that the character styles are applied. + await assertHTML( + page, + html` +


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

                    a

                    +
                    +

                    bb

                    +
                    +

                    cc

                    +
                    +

                    d

                    +
                    +

                    e

                    +
                    +

                    f

                    +
                    +


                    + `, + html` +


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

                    a

                    +
                    +

                    bb

                    +
                    +

                    cc

                    +
                    +

                    d

                    +
                    +

                    e

                    +
                    +

                    f

                    +
                    +


                    + `, + {ignoreClasses: true}, + ); + }, + ); test( `Can copy + paste (internal) using Table selection`, From 36fe1fd4cf237aa008fd3488ccbcc9686e8e0db7 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Sat, 27 Jul 2024 14:58:34 +0800 Subject: [PATCH 051/103] [Lexical][CI] Update canary e2e test os (#6465) --- .github/workflows/call-e2e-canary-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/call-e2e-canary-tests.yml b/.github/workflows/call-e2e-canary-tests.yml index 148679673d5..2c0f50b732d 100644 --- a/.github/workflows/call-e2e-canary-tests.yml +++ b/.github/workflows/call-e2e-canary-tests.yml @@ -7,7 +7,7 @@ jobs: canary: strategy: matrix: - os: ['macos-latest'] + os: ['ubuntu-latest'] node-version: [18.18.0] browser: ['chromium'] editor-mode: ['rich-text'] From 01d7d0ab13a4467b6aa982239694eaaf88d73eba Mon Sep 17 00:00:00 2001 From: placeba Date: Mon, 29 Jul 2024 00:35:17 +0200 Subject: [PATCH 052/103] [lexical-table] Bug Fix: cannot delete content when a table inside selection (#6412) Co-authored-by: Ivaylo Pavlov --- .../__tests__/e2e/Tables.spec.mjs | 24 +++++++++++++++++++ .../src/LexicalTableSelectionHelpers.ts | 8 +------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index e4dde016730..694f9c86e4b 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1247,6 +1247,30 @@ test.describe.parallel('Tables', () => { ); }); + test('Can delete all with node selection', async ({ + page, + isCollab, + isPlainText, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type('Text before'); + await page.keyboard.press('Enter'); + await insertSampleImage(page); + await page.keyboard.press('Enter'); + await page.keyboard.type('Text after'); + await insertTable(page, 2, 3); + await selectAll(page); + await page.keyboard.press('Backspace'); + await assertHTML( + page, + html` +


                    + `, + ); + }); + test(`Horizontal rule inside cell`, async ({page, isPlainText, isCollab}) => { await initialize({isCollab, page}); test.skip(isPlainText); diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index eb75797d994..604a6bf787d 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -329,16 +329,10 @@ export function applyTableHandlers( if (!parentNode) { return false; } - const nextNode = table.getNextSibling() || table.getPreviousSibling(); table.remove(); - if (nextNode) { - nextNode.selectStart(); - } else { - parentNode.selectStart(); - } } } - return true; + return false; } if ($isTableSelection(selection)) { From f92696dd3b67d203d7d4827523aa70147eb53305 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:50:11 +0800 Subject: [PATCH 053/103] [Lexica][CI] run extended tests for safari in mac-os and chrome/firefox in linux/windows (#6466) --- .github/workflows/call-e2e-all-tests.yml | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/call-e2e-all-tests.yml b/.github/workflows/call-e2e-all-tests.yml index 27dfb597a80..3bb173e122d 100644 --- a/.github/workflows/call-e2e-all-tests.yml +++ b/.github/workflows/call-e2e-all-tests.yml @@ -4,12 +4,27 @@ on: workflow_call: jobs: - mac: + mac-rich: strategy: matrix: node-version: [18.18.0] - browser: ['chromium', 'firefox', 'webkit'] - editor-mode: ['rich-text', 'plain-text'] + browser: ['webkit', 'chromium', 'firefox'] + editor-mode: ['rich-text'] + events-mode: ['modern-events'] + uses: ./.github/workflows/call-e2e-test.yml + with: + os: 'macos-latest' + node-version: ${{ matrix.node-version }} + browser: ${{ matrix.browser }} + editor-mode: ${{ matrix.editor-mode }} + events-mode: ${{ matrix.events-mode }} + + mac-plain: + strategy: + matrix: + node-version: [18.18.0] + browser: ['webkit'] + editor-mode: ['plain-text'] events-mode: ['modern-events'] uses: ./.github/workflows/call-e2e-test.yml with: @@ -56,7 +71,7 @@ jobs: strategy: matrix: node-version: [18.18.0] - browser: ['chromium', 'firefox', 'webkit'] + browser: ['webkit'] uses: ./.github/workflows/call-e2e-test.yml with: os: 'macos-latest' @@ -94,7 +109,7 @@ jobs: prod: strategy: matrix: - os: ['macos-latest'] + os: ['ubuntu-latest'] node-version: [18.18.0] browser: ['chromium'] editor-mode: ['rich-text'] @@ -111,7 +126,7 @@ jobs: collab-prod: strategy: matrix: - os: ['macos-latest'] + os: ['ubuntu-latest'] node-version: [18.18.0] browser: ['chromium'] editor-mode: ['rich-text-with-collab'] @@ -134,7 +149,7 @@ jobs: prod: [false] uses: ./.github/workflows/call-e2e-test.yml with: - os: 'macos-latest' + os: 'ubuntu-latest' browser: 'chromium' node-version: 18.18.0 events-mode: 'modern-events' @@ -151,7 +166,7 @@ jobs: events-mode: ['modern-events'] uses: ./.github/workflows/call-e2e-test.yml with: - os: 'macos-latest' + os: 'ubuntu-latest' flaky: true node-version: ${{ matrix.node-version }} browser: ${{ matrix.browser }} From 472cf68cf25f6299cdd07cb2cbaa13954cefcb16 Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 31 Jul 2024 11:48:23 +0800 Subject: [PATCH 054/103] be in sync with www (#6478) --- packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow index 27cde072690..c04a612054f 100644 --- a/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalDraggableBlockPlugin.js.flow @@ -10,7 +10,7 @@ import * as React from 'react'; type Props = $ReadOnly<{ - anchorElem?: HTMLElement, + anchorElem?: ?HTMLElement, menuRef: React.RefObject, targetLineRef: React.RefObject, menuComponent: React.Node, From bed804e8c653c0ff4cfeb35218e42853fda62e03 Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 2 Aug 2024 00:19:52 +0800 Subject: [PATCH 055/103] v0.17.0 (#6487) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 60 +++ 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 | 434 ++++++++++---------- 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 | 6 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 6 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- scripts/error-codes/codes.json | 8 +- 39 files changed, 432 insertions(+), 366 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7078f65936..975c9b20eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,63 @@ +## v0.17.0 (2024-07-31) + +- LexicaCI run extended tests for safari in mac-os and chromefirefox in linuxwindows (#6466) Sahejkm +- lexical-table Bug Fix cannot delete content when a table inside selection (#6412) placeba +- LexicalCI Update canary e2e test os (#6465) Sahejkm +- CI tag flaky tests (#6462) Sherry +- lexical Bug Fix Allow getTopLevelElement to return a DecoratorNode (#6458) Bob Ippolito +- CI dont cancel other test runs if e2e flaky job fails (#6460) Sherry +- Flow add tags type to OnChange plugin (#6457) Gerard Rovira +- lexical-rich-text Bug Fix HeadingNode.insertNewAfter (#6435) Germn Jabloski +- lexical-react sync format in flow file (#6448) Sherry +- lexical Bug Fix getCachedTypeToNodeMap should handle a empty and writable EditorState (#6444) Bob Ippolito +- lexical lexical-selection Preserve paragraph styles between lines (#6437) Ivaylo Pavlov +- LexicalGallery Add tableplugin example to gallery (#6447) Sahejkm +- LexicalCI ignore running unitintegeritye2e tests on examples folder code (#6446) Sahejkm +- LexicalGallery Create Simple Tableplugin example (#6445) Sahejkm +- lexicalauto-link Fix auto link crash editor (#6433) Maksym Plavinskyi +- lexicallexical-selection Bug Fix Respect mode when patching text style (#6428) Adrian Busse +- lexical-historylexical-selectionlexical-react Fix #6409 TextNode change detection (#6420) Bob Ippolito +- lexical-playground Refactor run prettier to fix CI (#6436) Germn Jabloski +- fix(LexicalNode) fix inline decorator isSelected (#5948) Xuan +- lexical-playgroundTableCellResizer Bug Fix Register event handlers on root element (#6416) JBWereRuss +- lexical-react update flow typing for draggable block plugin (#6426) Sherry +- docs fix typo in editor.registerCommand() usage (#6429) Yangshun Tay +- fix(docs) correct typo in Lexical collaboration guide (#6421) Francois Polo +- Fix discrete nested updates (#6419) Gerard Rovira +- CI fix build failure on astro integration tests (#6414) wnhlee +- CI run flaky tests on firefox browsers (#6411) Sherry +- CI tag flaky tests (#6405) Sherry +- lexical-reactlexical-playground sync draggable block plugin to www (#6397) Sherry +- Fix transpile nodesOfType (#6408) Gerard Rovira +- Restore registerRootListener null call (#6403) Gerard Rovira +- Add ref to contenteditable (#6381) Gerard Rovira +- lexical Feature registerMutationListener should initialize its existing nodes (#6357) Bob Ippolito +- lexical Feature Implement Editor.read and EditorState.read with editor argument (#6347) Bob Ippolito +- lexical Bug Fix more accurate line break pasting (#6395) Sherry +- lexical-html Feature support pasting empty block nodes (#6392) Sherry +- lexical-playgroundlexical-table Bug Fix Fix Shift Down Arrow regression for table sequence. (#6393) Serey Roth +- Gallery Add option to filter plugins based on tags (#6391) Sahejkm +- Fix clear rootElement on React (#6389) Gerard Rovira +- CI tag flaky tests (#6388) Sherry +- Prettier sort test attributes (#6384) Gerard Rovira +- Fix integrity test (#6385) Gerard Rovira +- LexicalGallery Convert files to follow typescript (#6383) Sahejkm +- lexicallexical-playground Bug Fix Create line break on paste of content type texthtml (#6376) Janna Wieneke +- examples Chore Use named export of LexicalErrorBoundary in the examples (#6378) Bob Ippolito +- LexicalGallery Add option to search examples in the gallery (#6379) Sahejkm +- lexical-playground Fix Table Hover Actions Noclick Bug (#6375) Ivaylo Pavlov +- Lexical Fix flow errors on syncing build to meta intern (#6373) Sahejkm +- rexical-react Bug Fix Headings inside collapsible sections are lost when Table of Contents is re-initialized (#6371) Katsia +- LexicalGallery Add description in the card, option to render preview card at run time if no image (#6372) Sahejkm +- Lexical Create initial Gallery View with Emoji Plugin Example (#6369) Sahejkm +- CI run e2e flaky tests in a separate job (#6365) Sherry +- Make placeholder accessible (#6171) Gerard Rovira +- lexical-playground Table Hover Action Buttons (#6355) Ivaylo Pavlov +- lexicallexical-table Chore Replace references to old GridSelection with TableSelection (#6366) Bob Ippolito +- lexical-markdown Feature Change Dont trim whitespaces on convertFromMarkdownString (#6360) Sherry +- v0.16.1 (#6363) Ivaylo Pavlov +- v0.16.1 Lexical GitHub Actions Bot + ## v0.16.1 (2024-07-01) - lexical-playgroundlexical-poll Bug Fix Fixes undefined context inside Poll add option (#6361) Roman Lyubimov diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index 0a4ca6ae021..fc31de898a8 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.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.16.1", - "lexical": "0.16.1", + "@lexical/react": "0.17.0", + "lexical": "0.17.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 9d828758c6f..b9a8b8cde8f 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.16.1", + "version": "0.17.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.16.1", - "@lexical/yjs": "0.16.1", - "lexical": "0.16.1", + "@lexical/react": "0.17.0", + "@lexical/yjs": "0.17.0", + "lexical": "0.17.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 768c8220e4e..5dbda0fd5db 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.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.16.1", - "lexical": "0.16.1", + "@lexical/react": "0.17.0", + "lexical": "0.17.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 edc114ec39d..20deb65f206 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.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.16.1", - "lexical": "0.16.1", + "@lexical/react": "0.17.0", + "lexical": "0.17.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 d1a2b25c728..370684b5f36 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.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.16.1", - "@lexical/history": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/utils": "0.16.1", + "@lexical/dragon": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/utils": "0.17.0", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.16.1" + "lexical": "0.17.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index e6a15cc5e1d..871e5fd7aa8 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.16.1", + "version": "0.17.0", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.16.1", - "@lexical/history": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/dragon": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index 03a6d93a2ce..4f248e3ac2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.16.1", + "version": "0.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "workspaces": [ "packages/*" @@ -36267,28 +36267,28 @@ } }, "packages/lexical": { - "version": "0.16.1", + "version": "0.17.0", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0", "prismjs": "^1.27.0" }, "devDependencies": { @@ -36297,7 +36297,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.16.1", + "version": "0.17.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -36314,12 +36314,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.16.1", + "@lexical/devtools-core": "0.17.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.16.1", + "lexical": "0.17.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -36327,15 +36327,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "peerDependencies": { "react": ">=17.x", @@ -36344,15 +36344,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -36363,136 +36363,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/code": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/code": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-playground": { - "version": "0.16.1", + "version": "0.17.0", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/file": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/react": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/file": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/react": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.0", "katex": "^0.16.10", - "lexical": "0.16.1", + "lexical": "0.17.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -36515,28 +36515,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/devtools-core": "0.16.1", - "@lexical/dragon": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/history": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/markdown": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "@lexical/yjs": "0.16.1", - "lexical": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/devtools-core": "0.17.0", + "@lexical/dragon": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/markdown": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "@lexical/yjs": "0.17.0", + "lexical": "0.17.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -36546,54 +36546,54 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "lexical": "0.16.1" + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "lexical": "0.17.0" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.16.1", + "version": "0.17.0", "dependencies": { "@docusaurus/core": "^3.3.2", "@docusaurus/preset-classic": "^3.3.2", @@ -36622,11 +36622,11 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "@lexical/offset": "0.16.1", - "lexical": "0.16.1" + "@lexical/offset": "0.17.0", + "lexical": "0.17.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -36659,10 +36659,10 @@ } }, "packages/shared": { - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } }, @@ -40987,19 +40987,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.16.1", + "@lexical/utils": "0.17.0", "@types/prismjs": "^1.26.0", - "lexical": "0.16.1", + "lexical": "0.17.0", "prismjs": "^1.27.0" } }, @@ -41011,7 +41011,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.16.1", + "@lexical/devtools-core": "0.17.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -41020,7 +41020,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.16.1", + "lexical": "0.17.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -41032,18 +41032,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/eslint-plugin": { @@ -41055,151 +41055,151 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/code": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/devtools-core": "0.16.1", - "@lexical/dragon": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/history": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/markdown": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "@lexical/yjs": "0.16.1", - "lexical": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/devtools-core": "0.17.0", + "@lexical/dragon": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/markdown": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "@lexical/yjs": "0.17.0", + "lexical": "0.17.0", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "lexical": "0.16.1" + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "lexical": "0.17.0" } }, "@lexical/website": { @@ -41231,8 +41231,8 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.16.1", - "lexical": "0.16.1" + "@lexical/offset": "0.17.0", + "lexical": "0.17.0" } }, "@mdx-js/mdx": { @@ -52952,26 +52952,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/file": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/react": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/file": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/react": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.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.16.1", + "lexical": "0.17.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -58853,7 +58853,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.16.1" + "lexical": "0.17.0" } }, "shebang-command": { diff --git a/package.json b/package.json index 064d5d288d6..0a60a5f1659 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.16.1", + "version": "0.17.0", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index 9acd6f83dc8..6a79bbcfdf6 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index f46d9103de8..8e27fd6547f 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index de4f9ddada5..a0eb2c427db 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/html": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index d7b03ecc1a8..b2bb5da5af0 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.16.1", + "version": "0.17.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.16.1", + "@lexical/devtools-core": "0.17.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.16.1", + "lexical": "0.17.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 e14971d7469..e9e276358b1 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index 43b5b7a4421..265dabcafe3 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.16.1", + "version": "0.17.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 5c1a005785b..1cbc2b4d8d8 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 6889fb2a4dd..784622d7830 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index 33f8d3a145b..4b651029d17 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 880e13410b8..acb4e60bed2 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index a5b415592a8..14c5da6f845 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index a39f3055338..34730f514a9 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index 3f7cfd2f323..b2b085f279b 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 95e5c2d938d..f3705f4274f 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index 2fbdf7c53d0..b866034b957 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/code": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index 7e757fc2e6f..d7ef4445bb0 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index c3702bf3497..d908bead1f2 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index c828fb5e8e7..554107f5bd5 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.16.1", + "version": "0.17.0", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 9efce56f468..066a66037f5 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.16.1", + "version": "0.17.0", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/file": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/react": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/utils": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/file": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/react": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/utils": "0.17.0", "katex": "^0.16.10", - "lexical": "0.16.1", + "lexical": "0.17.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 a95ba4ed46a..7afe294bcac 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/code": "0.16.1", - "@lexical/devtools-core": "0.16.1", - "@lexical/dragon": "0.16.1", - "@lexical/hashtag": "0.16.1", - "@lexical/history": "0.16.1", - "@lexical/link": "0.16.1", - "@lexical/list": "0.16.1", - "@lexical/mark": "0.16.1", - "@lexical/markdown": "0.16.1", - "@lexical/overflow": "0.16.1", - "@lexical/plain-text": "0.16.1", - "@lexical/rich-text": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "@lexical/text": "0.16.1", - "@lexical/utils": "0.16.1", - "@lexical/yjs": "0.16.1", - "lexical": "0.16.1", + "@lexical/clipboard": "0.17.0", + "@lexical/code": "0.17.0", + "@lexical/devtools-core": "0.17.0", + "@lexical/dragon": "0.17.0", + "@lexical/hashtag": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/link": "0.17.0", + "@lexical/list": "0.17.0", + "@lexical/mark": "0.17.0", + "@lexical/markdown": "0.17.0", + "@lexical/overflow": "0.17.0", + "@lexical/plain-text": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "@lexical/text": "0.17.0", + "@lexical/utils": "0.17.0", + "@lexical/yjs": "0.17.0", + "lexical": "0.17.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 974ac9ac271..64efaa0958b 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.16.1", + "version": "0.17.0", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/clipboard": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 9bb166ca69f..6a797221998 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index 15130de8f14..fdef1dd4887 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,12 +8,12 @@ "table" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.16.1", - "lexical": "0.16.1" + "@lexical/utils": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 4af8eda68f0..09aa1528aa9 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 7e9be44f97f..4f3f899ef2f 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.16.1", - "@lexical/selection": "0.16.1", - "@lexical/table": "0.16.1", - "lexical": "0.16.1" + "@lexical/list": "0.17.0", + "@lexical/selection": "0.17.0", + "@lexical/table": "0.17.0", + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index faa240ad640..568e8e44931 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.16.1", + "version": "0.17.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index b331a442e2c..2df54014c08 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,12 +11,12 @@ "crdt" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.16.1", - "lexical": "0.16.1" + "@lexical/offset": "0.17.0", + "lexical": "0.17.0" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index c6381e0cead..f3ca77bb732 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 0e83cbd6d06..e1acff658d3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.16.1", + "version": "0.17.0", "dependencies": { - "lexical": "0.16.1" + "lexical": "0.17.0" }, "repository": { "type": "git", diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 9663b42e464..74daa5f1314 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -187,5 +187,11 @@ "185": "%s doesn't extend the %s", "186": "Lexical node with constructor %s attempted to re-use key from node in active editor state with constructor %s. Keys must not be re-used when the type is changed.", "187": "Lexical node with constructor %s attempted to re-use key from node in active editor state with different constructor with the same name (possibly due to invalid Hot Module Replacement). Keys must not be re-used when the type is changed.", - "188": "Expected a RangeSelection or TableSelection" + "188": "Expected a RangeSelection or TableSelection", + "189": "Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update(), editor.read(), or editorState.read().", + "190": "Unable to find an active editor. This method can only be used synchronously during the callback of editor.update() or editor.read().", + "191": "Unexpected empty pending editor state on discrete nested update", + "192": "getCachedTypeToNodeMap called with a writable EditorState", + "193": "$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor", + "194": "Children of root nodes must be elements or decorators" } From f373759a7849f473d34960a6bf4e34b2a011e762 Mon Sep 17 00:00:00 2001 From: Evgeny Vorobyev Date: Thu, 1 Aug 2024 18:20:07 -0500 Subject: [PATCH 056/103] [lexical-table] Bug Fix: Enable observer updates on table elements attributes change (#6479) --- packages/lexical-table/src/LexicalTableObserver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 33a0a6f427e..4b917e090a0 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -151,6 +151,7 @@ export class TableObserver { this.table = getTable(tableElement); observer.observe(tableElement, { + attributes: true, childList: true, subtree: true, }); From 3c53e648e2f4eb5baadb49c8f3b353fb1f6397a4 Mon Sep 17 00:00:00 2001 From: wnhlee <40269597+2wheeh@users.noreply.github.com> Date: Sat, 3 Aug 2024 04:06:45 +0900 Subject: [PATCH 057/103] [lexical] Bug Fix: Merge pasted paragraph into empty quote (#6367) Co-authored-by: Bob Ippolito --- .../lexical-list/src/LexicalListItemNode.ts | 4 +++ .../lexical/CopyAndPaste.spec.mjs | 33 +++++++++++++++++++ packages/lexical-rich-text/src/index.ts | 4 +++ packages/lexical/src/LexicalSelection.ts | 6 ++-- .../lexical/src/nodes/LexicalElementNode.ts | 17 ++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index d07e6ac2f7f..72b9ac1b612 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -404,6 +404,10 @@ export class ListItemNode extends ElementNode { createParentElementNode(): ElementNode { return $createListNode('bullet'); } + + canMergeWhenEmpty(): true { + return true; + } } function $setListItemThemeClassNames( diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs index aa05e3a0a18..161d5792eb2 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs @@ -914,4 +914,37 @@ test.describe('CopyAndPaste', () => { `, ); }); + + test('Copy and paste paragraph into quote', async ({page, isPlainText}) => { + test.skip(isPlainText); + await focusEditor(page); + + await page.keyboard.type('Hello world'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Some text'); + + await selectAll(page); + + const clipboard = await copyToClipboard(page); + + await page.keyboard.type('> '); + + await pasteFromClipboard(page, clipboard); + + await assertHTML( + page, + html` +
                    + Hello world +
                    +

                    + Some text +

                    + `, + ); + }); }); diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 9bbbc18d46b..fbf9f53b0ec 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -202,6 +202,10 @@ export class QuoteNode extends ElementNode { this.replace(paragraph); return true; } + + canMergeWhenEmpty(): true { + return true; + } } export function $createQuoteNode(): QuoteNode { diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 64fe30607f4..8ffe35923c3 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -1255,14 +1255,12 @@ export class RangeSelection implements BaseSelection { const blocksParent = $wrapInlineNodes(nodes); const nodeToSelect = blocksParent.getLastDescendant()!; const blocks = blocksParent.getChildren(); - const isLI = (node: LexicalNode) => - '__value' in node && '__checked' in node; const isMergeable = (node: LexicalNode): node is ElementNode => $isElementNode(node) && INTERNAL_$isBlock(node) && !node.isEmpty() && $isElementNode(firstBlock) && - (!firstBlock.isEmpty() || isLI(firstBlock)); + (!firstBlock.isEmpty() || firstBlock.canMergeWhenEmpty()); const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty(); const insertedParagraph = shouldInsert ? this.insertParagraph() : null; @@ -1284,7 +1282,7 @@ export class RangeSelection implements BaseSelection { if ( insertedParagraph && $isElementNode(lastInsertedBlock) && - (isLI(insertedParagraph) || INTERNAL_$isBlock(lastToInsert)) + (insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert)) ) { lastInsertedBlock.append(...insertedParagraph.getChildren()); insertedParagraph.remove(); diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index bacb0113299..83c33c9776c 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -582,6 +582,23 @@ export class ElementNode extends LexicalNode { ): boolean { return false; } + + /** + * Determines whether this node, when empty, can merge with a first block + * of nodes being inserted. + * + * This method is specifically called in {@link RangeSelection.insertNodes} + * to determine merging behavior during nodes insertion. + * + * @example + * // In a ListItemNode or QuoteNode implementation: + * canMergeWhenEmpty(): true { + * return true; + * } + */ + canMergeWhenEmpty(): boolean { + return false; + } } export function $isElementNode( From 65bdd2c71f04353faea26d52fc4833b71d149216 Mon Sep 17 00:00:00 2001 From: keiseiTi Date: Sat, 3 Aug 2024 23:04:56 +0800 Subject: [PATCH 058/103] [@lexical/playground] fix: block cursor show horizontal (#6486) --- packages/lexical-playground/src/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index a6c9d65c267..d21a384bcf1 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -1753,8 +1753,8 @@ button.item.dropdown-item-active i { display: block; position: absolute; top: -2px; - width: 20px; - border-top: 1px solid black; + height: 18px; + border-left: 1px solid black; animation: CursorBlink 1.1s steps(2, start) infinite; } From f834a048a0e6db2159c82796eabd36cecec99b97 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sun, 4 Aug 2024 08:57:46 +0530 Subject: [PATCH 059/103] When creating a new check list, set the `checked` value of the list item to `false` instead of `undefined` (#5978) --- packages/lexical-list/src/LexicalListItemNode.ts | 11 +++++++++-- packages/lexical-list/src/formatList.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index 72b9ac1b612..d2091e2c10a 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -6,7 +6,7 @@ * */ -import type {ListNode} from './'; +import type {ListNode, ListType} from './'; import type { BaseSelection, DOMConversionMap, @@ -320,7 +320,14 @@ export class ListItemNode extends ElementNode { getChecked(): boolean | undefined { const self = this.getLatest(); - return self.__checked; + let listType: ListType | undefined; + + const parent = this.getParent(); + if ($isListNode(parent)) { + listType = parent.getListType(); + } + + return listType === 'check' ? Boolean(self.__checked) : undefined; } setChecked(checked?: boolean): void { diff --git a/packages/lexical-list/src/formatList.ts b/packages/lexical-list/src/formatList.ts index 565887c57d5..6d4a5cb41b5 100644 --- a/packages/lexical-list/src/formatList.ts +++ b/packages/lexical-list/src/formatList.ts @@ -297,7 +297,7 @@ export function updateChildrenListItemValue(list: ListNode): void { if (child.getValue() !== value) { child.setValue(value); } - if (isNotChecklist && child.getChecked() != null) { + if (isNotChecklist && child.getLatest().__checked != null) { child.setChecked(undefined); } if (!$isListNode(child.getFirstChild())) { From 80c400c27065b1183449f825529fe76a1b570497 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 4 Aug 2024 10:45:06 -0700 Subject: [PATCH 060/103] Revert "[@lexical/playground] fix: block cursor show horizontal" (#6490) --- packages/lexical-playground/src/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index d21a384bcf1..a6c9d65c267 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -1753,8 +1753,8 @@ button.item.dropdown-item-active i { display: block; position: absolute; top: -2px; - height: 18px; - border-left: 1px solid black; + width: 20px; + border-top: 1px solid black; animation: CursorBlink 1.1s steps(2, start) infinite; } From e1881a6e409e1541c10dd0b5378f3a38c9dc8c9e Mon Sep 17 00:00:00 2001 From: Devy <168729019+devy-bee@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:45:26 +0900 Subject: [PATCH 061/103] docs: prevent automatic

                    tag wrapping (#6491) --- packages/lexical-website/docs/react/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-website/docs/react/index.md b/packages/lexical-website/docs/react/index.md index 2462763c9a1..b0150b10116 100644 --- a/packages/lexical-website/docs/react/index.md +++ b/packages/lexical-website/docs/react/index.md @@ -13,7 +13,7 @@ To make it easier for React users to implement rich-text editors, Lexical expose - Getting Started Guide + {`Getting Started Guide`} ## Supported Versions From 20e4ea1687bd846653a0cc2df8cbb610d8308307 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 6 Aug 2024 11:19:28 -0700 Subject: [PATCH 062/103] [lexical] Feature: Add version identifier to LexicalEditor constructor (#6488) --- .../src/generateContent.ts | 4 +- .../src/utils/isLexicalNode.ts | 4 +- packages/lexical-playground/vite.config.ts | 4 ++ .../lexical-playground/vite.prod.config.ts | 4 ++ .../src/LexicalCheckListPlugin.tsx | 31 +++------- .../docs/concepts/listeners.md | 2 +- packages/lexical/src/LexicalEditor.ts | 5 ++ packages/lexical/src/LexicalEvents.ts | 12 +++- packages/lexical/src/LexicalUpdates.ts | 60 +++++++++++++++---- packages/lexical/src/LexicalUtils.ts | 22 +++++-- packages/lexical/src/index.ts | 2 + .../lexical-esm-astro-react/package.json | 13 ++-- .../fixtures/lexical-esm-nextjs/package.json | 8 +-- .../package.json | 59 +++++++++--------- .../tests/test.ts | 11 ++++ scripts/__tests__/integration/setup.js | 3 + scripts/build.js | 17 +++++- scripts/updateVersion.js | 5 +- 18 files changed, 177 insertions(+), 89 deletions(-) diff --git a/packages/lexical-devtools-core/src/generateContent.ts b/packages/lexical-devtools-core/src/generateContent.ts index fcb6bf253b5..b08eb2ec777 100644 --- a/packages/lexical-devtools-core/src/generateContent.ts +++ b/packages/lexical-devtools-core/src/generateContent.ts @@ -161,8 +161,8 @@ export function generateContent( } else { res += '\n └ None dispatched.'; } - - res += '\n\n editor:'; + const {version} = editor.constructor; + res += `\n\n editor${version ? ` (v${version})` : ''}:`; res += `\n └ namespace ${editorConfig.namespace}`; if (compositionKey !== null) { res += `\n └ compositionKey ${compositionKey}`; diff --git a/packages/lexical-devtools/src/utils/isLexicalNode.ts b/packages/lexical-devtools/src/utils/isLexicalNode.ts index f3c3b52f6c9..f4969532012 100644 --- a/packages/lexical-devtools/src/utils/isLexicalNode.ts +++ b/packages/lexical-devtools/src/utils/isLexicalNode.ts @@ -6,10 +6,12 @@ * */ +import {getEditorPropertyFromDOMNode} from 'lexical'; + import {LexicalHTMLElement} from '../types'; export function isLexicalNode( node: LexicalHTMLElement | Element, ): node is LexicalHTMLElement { - return (node as LexicalHTMLElement).__lexicalEditor !== undefined; + return getEditorPropertyFromDOMNode(node) !== undefined; } diff --git a/packages/lexical-playground/vite.config.ts b/packages/lexical-playground/vite.config.ts index d7d0d3b5c48..12490da5cf2 100644 --- a/packages/lexical-playground/vite.config.ts +++ b/packages/lexical-playground/vite.config.ts @@ -52,6 +52,10 @@ export default defineConfig(({command}) => { from: /__DEV__/g, to: 'true', }, + { + from: 'process.env.LEXICAL_VERSION', + to: JSON.stringify(`${process.env.npm_package_version}+git`), + }, ], }), babel({ diff --git a/packages/lexical-playground/vite.prod.config.ts b/packages/lexical-playground/vite.prod.config.ts index 083f4697032..97fc5a760cd 100644 --- a/packages/lexical-playground/vite.prod.config.ts +++ b/packages/lexical-playground/vite.prod.config.ts @@ -53,6 +53,10 @@ export default defineConfig({ from: /__DEV__/g, to: 'false', }, + { + from: 'process.env.LEXICAL_VERSION', + to: JSON.stringify(`${process.env.npm_package_version}+git`), + }, ], }), babel({ diff --git a/packages/lexical-react/src/LexicalCheckListPlugin.tsx b/packages/lexical-react/src/LexicalCheckListPlugin.tsx index 50bffdbc033..6125a7b2077 100644 --- a/packages/lexical-react/src/LexicalCheckListPlugin.tsx +++ b/packages/lexical-react/src/LexicalCheckListPlugin.tsx @@ -28,6 +28,7 @@ import { $isElementNode, $isRangeSelection, COMMAND_PRIORITY_LOW, + getNearestEditorFromDOMNode, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, @@ -199,20 +200,20 @@ function handleCheckItemEvent(event: PointerEvent, callback: () => void) { function handleClick(event: Event) { handleCheckItemEvent(event as PointerEvent, () => { - const domNode = event.target as HTMLElement; - const editor = findEditor(domNode); + if (event.target instanceof HTMLElement) { + const domNode = event.target; + const editor = getNearestEditorFromDOMNode(domNode); - if (editor != null && editor.isEditable()) { - editor.update(() => { - if (event.target) { + if (editor != null && editor.isEditable()) { + editor.update(() => { const node = $getNearestNodeFromDOMNode(domNode); if ($isListItemNode(node)) { domNode.focus(); node.toggleChecked(); } - } - }); + }); + } } }); } @@ -224,22 +225,6 @@ function handlePointerDown(event: PointerEvent) { }); } -function findEditor(target: Node) { - let node: ParentNode | Node | null = target; - - while (node) { - // @ts-ignore internal field - if (node.__lexicalEditor) { - // @ts-ignore internal field - return node.__lexicalEditor; - } - - node = node.parentNode; - } - - return null; -} - function getActiveCheckListItem(): HTMLElement | null { const activeElement = document.activeElement as HTMLElement; diff --git a/packages/lexical-website/docs/concepts/listeners.md b/packages/lexical-website/docs/concepts/listeners.md index fb5036223b0..834db37c0bf 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.16.x but will change to false in 0.17.0). +(default is currently true for backwards compatibility in 0.17.x but will change to false in 0.18.0). ```js const removeMutationListener = editor.registerMutationListener( diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index e08665f1c02..44ae24f4b6b 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -562,6 +562,9 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { export class LexicalEditor { ['constructor']!: KlassConstructor; + /** The version with build identifiers for this editor (since 0.17.1) */ + static version: string | undefined; + /** @internal */ _headless: boolean; /** @internal */ @@ -1284,3 +1287,5 @@ export class LexicalEditor { }; } } + +LexicalEditor.version = process.env.LEXICAL_VERSION; diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 2836f924cf6..4a177d19498 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -94,6 +94,7 @@ import { getAnchorTextFromDOM, getDOMSelection, getDOMTextNode, + getEditorPropertyFromDOMNode, getEditorsToPropagate, getNearestEditorFromDOMNode, getWindow, @@ -111,6 +112,7 @@ import { isEscape, isFirefoxClipboardEvents, isItalic, + isLexicalEditor, isLineBreak, isModifier, isMoveBackward, @@ -1329,13 +1331,17 @@ export function removeRootElementEvents(rootElement: HTMLElement): void { doc.removeEventListener('selectionchange', onDocumentSelectionChange); } - // @ts-expect-error: internal field - const editor: LexicalEditor | null | undefined = rootElement.__lexicalEditor; + const editor = getEditorPropertyFromDOMNode(rootElement); - if (editor !== null && editor !== undefined) { + if (isLexicalEditor(editor)) { cleanActiveNestedEditorsMap(editor); // @ts-expect-error: internal field rootElement.__lexicalEditor = null; + } else if (editor) { + invariant( + false, + 'Attempted to remove event handlers from a node that does not belong to this build of Lexical', + ); } const removeHandles = getRootElementRemoveHandles(rootElement); diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 45f01b7815a..38e83cc56e8 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -6,7 +6,14 @@ * */ -import type { +import type {SerializedEditorState} from './LexicalEditorState'; +import type {LexicalNode, SerializedLexicalNode} from './LexicalNode'; + +import invariant from 'shared/invariant'; + +import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.'; +import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants'; +import { CommandPayloadType, EditorUpdateOptions, LexicalCommand, @@ -14,16 +21,9 @@ import type { Listener, MutatedNodes, RegisteredNodes, + resetEditor, Transform, } from './LexicalEditor'; -import type {SerializedEditorState} from './LexicalEditorState'; -import type {LexicalNode, SerializedLexicalNode} from './LexicalNode'; - -import invariant from 'shared/invariant'; - -import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.'; -import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants'; -import {resetEditor} from './LexicalEditor'; import { cloneEditorState, createEmptyEditorState, @@ -47,9 +47,11 @@ import { import { $getCompositionKey, getDOMSelection, + getEditorPropertyFromDOMNode, getEditorStateTextContent, getEditorsToPropagate, getRegisteredNodeOrThrow, + isLexicalEditor, removeDOMBlockCursorElement, scheduleMicroTask, updateDOMBlockCursorElement, @@ -96,7 +98,8 @@ export function getActiveEditorState(): EditorState { 'Unable to find an active editor state. ' + 'State helpers or node methods can only be used ' + 'synchronously during the callback of ' + - 'editor.update(), editor.read(), or editorState.read().', + 'editor.update(), editor.read(), or editorState.read().%s', + collectBuildInformation(), ); } @@ -110,13 +113,46 @@ export function getActiveEditor(): LexicalEditor { 'Unable to find an active editor. ' + 'This method can only be used ' + 'synchronously during the callback of ' + - 'editor.update() or editor.read().', + 'editor.update() or editor.read().%s', + collectBuildInformation(), ); } - return activeEditor; } +function collectBuildInformation(): string { + let compatibleEditors = 0; + const incompatibleEditors = new Set(); + const thisVersion = LexicalEditor.version; + if (typeof window !== 'undefined') { + for (const node of document.querySelectorAll('[contenteditable]')) { + const editor = getEditorPropertyFromDOMNode(node); + if (isLexicalEditor(editor)) { + compatibleEditors++; + } else if (editor) { + let version = String( + ( + editor.constructor as typeof editor['constructor'] & + Record + ).version || '<0.17.1', + ); + if (version === thisVersion) { + version += + ' (separately built, likely a bundler configuration issue)'; + } + incompatibleEditors.add(version); + } + } + } + let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`; + if (incompatibleEditors.size) { + output += ` and incompatible editors with versions ${Array.from( + incompatibleEditors, + ).join(', ')}`; + } + return output; +} + export function internalGetActiveEditor(): LexicalEditor | null { return activeEditor; } diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 9a4880b1dc4..f735aba2306 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -123,8 +123,7 @@ export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean { (nodeName === 'INPUT' || nodeName === 'TEXTAREA' || (activeElement.contentEditable === 'true' && - // @ts-ignore internal field - activeElement.__lexicalEditor == null)) + getEditorPropertyFromDOMNode(activeElement) == null)) ); } @@ -149,14 +148,21 @@ export function isSelectionWithinEditor( } } +/** + * @returns true if the given argument is a LexicalEditor instance from this build of Lexical + */ +export function isLexicalEditor(editor: unknown): editor is LexicalEditor { + // Check instanceof to prevent issues with multiple embedded Lexical installations + return editor instanceof LexicalEditor; +} + export function getNearestEditorFromDOMNode( node: Node | null, ): LexicalEditor | null { let currentNode = node; while (currentNode != null) { - // @ts-expect-error: internal field - const editor: LexicalEditor = currentNode.__lexicalEditor; - if (editor != null) { + const editor = getEditorPropertyFromDOMNode(currentNode); + if (isLexicalEditor(editor)) { return editor; } currentNode = getParentElement(currentNode); @@ -164,6 +170,12 @@ export function getNearestEditorFromDOMNode( return null; } +/** @internal */ +export function getEditorPropertyFromDOMNode(node: Node | null): unknown { + // @ts-expect-error: internal field + return node ? node.__lexicalEditor : null; +} + export function getTextDirection(text: string): 'ltr' | 'rtl' | null { if (RTL_REGEX.test(text)) { return 'rtl'; diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index de3a78ff645..5ef926b5afc 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -176,11 +176,13 @@ export { $setCompositionKey, $setSelection, $splitNode, + getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, isBlockDomNode, isHTMLAnchorElement, isHTMLElement, isInlineDomNode, + isLexicalEditor, isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, resetRandomKey, 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 7fdab6d467c..0f2d6f14d19 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,29 +1,30 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.0.1", + "version": "0.17.0", "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro check && astro build", "preview": "astro preview", - "astro": "astro", + "astro": "astro", "test": "playwright test" }, "dependencies": { "@astrojs/check": "^0.5.9", "@astrojs/react": "^3.1.0", - "@lexical/react": "^0.14.3", - "@lexical/utils": "^0.14.3", + "@lexical/react": "0.17.0", + "@lexical/utils": "0.17.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "^0.14.3", + "lexical": "0.17.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" }, "devDependencies": { "@playwright/test": "^1.43.1" - } + }, + "sideEffects": false } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index efd4626d5aa..4c3d8394e12 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.1.0", + "version": "0.17.0", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "^0.14.5", - "@lexical/react": "^0.14.5", - "lexical": "^0.14.5", + "@lexical/plain-text": "0.17.0", + "@lexical/react": "0.17.0", + "lexical": "0.17.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 b9fa88eb595..249b430f3b2 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,32 +1,31 @@ { - "name": "lexical-sveltekit-vanilla-js", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "test": "playwright test" - }, - "devDependencies": { - "@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.14.5", - "@lexical/dragon": "^0.14.5", - "@lexical/history": "^0.14.5", - "@lexical/rich-text": "^0.14.5", - "@lexical/utils": "^0.14.5", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "svelte": "^4.2.7", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.1.7" - }, - "type": "module", - "dependencies": {} + "name": "lexical-sveltekit-vanilla-js", + "version": "0.17.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "test": "playwright test" + }, + "devDependencies": { + "@lexical/dragon": "0.17.0", + "@lexical/history": "0.17.0", + "@lexical/rich-text": "0.17.0", + "@lexical/utils": "0.17.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.17.0", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "svelte": "^4.2.7", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.1.7" + }, + "type": "module" } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/tests/test.ts b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/tests/test.ts index a2a1b26aa8a..f265d9e85b3 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/tests/test.ts +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/tests/test.ts @@ -17,3 +17,14 @@ test('index page has expected h1 and lexical state', async ({ page }) => { /"text": "Welcome to the Vanilla JS Lexical Demo!"/ ); }); + +test('lexical editor has an accessible numeric version', async ({ page }) => { + await page.goto('/'); + await expect( + page.getByRole('heading', { name: 'SvelteKit Lexical Basic - Vanilla JS' }) + ).toBeVisible(); + expect(await page.evaluate(() => + // @ts-expect-error + document.querySelector('[contenteditable]')!.__lexicalEditor.constructor.version + )).toMatch(/^\d+\.\d+\.\d+/); +}); diff --git a/scripts/__tests__/integration/setup.js b/scripts/__tests__/integration/setup.js index 2213a78ccb8..a921a8e31f2 100644 --- a/scripts/__tests__/integration/setup.js +++ b/scripts/__tests__/integration/setup.js @@ -28,6 +28,9 @@ module.exports = async function (globalConfig, projectConfig) { ), ); if (!needsBuild) { + console.log( + '\nWARNING: Running integration tests with cached build artifacts from a previous `npm run prepare-release`.', + ); return; } await exec('npm run prepare-release'); diff --git a/scripts/build.js b/scripts/build.js index d8b0897dc96..14bc48af584 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -125,9 +125,18 @@ function getExtension(format) { * @param {string} outputFile * @param {boolean} isProd * @param {'cjs'|'esm'} format + * @param {string} version * @returns {Promise>} the exports of the built module */ -async function build(name, inputFile, outputPath, outputFile, isProd, format) { +async function build( + name, + inputFile, + outputPath, + outputFile, + isProd, + format, + version, +) { const extensions = ['.js', '.jsx', '.ts', '.tsx']; const inputOptions = { external(modulePath, src) { @@ -214,6 +223,9 @@ async function build(name, inputFile, outputPath, outputFile, isProd, format) { __DEV__: isProd ? 'false' : 'true', delimiters: ['', ''], preventAssignment: true, + 'process.env.LEXICAL_VERSION': JSON.stringify( + `${version}+${isProd ? 'prod' : 'dev'}.${format}`, + ), }, isWWW && strictWWWMappings, ), @@ -393,6 +405,7 @@ async function buildAll() { for (const pkg of packagesManager.getPublicPackages()) { const {name, sourcePath, outputPath, packageName, modules} = pkg.getPackageBuildDefinition(); + const {version} = pkg.packageJson; for (const module of modules) { for (const format of formats) { const {sourceFileName, outputFileName} = module; @@ -408,6 +421,7 @@ async function buildAll() { ), isProduction, format, + version, ); if (isRelease) { @@ -421,6 +435,7 @@ async function buildAll() { ), false, format, + version, ); buildForkModules(outputPath, outputFileName, format, exports); } diff --git a/scripts/updateVersion.js b/scripts/updateVersion.js index ffcdb5e529d..5ec3cd8840b 100644 --- a/scripts/updateVersion.js +++ b/scripts/updateVersion.js @@ -47,7 +47,10 @@ function updatePackage(pkg) { function updateVersion() { packagesManager.getPackages().forEach(updatePackage); glob - .sync('./examples/*/package.json') + .sync([ + './examples/*/package.json', + './scripts/__tests__/integration/fixtures/*/package.json', + ]) .forEach((packageJsonPath) => updatePackage(new PackageMetadata(packageJsonPath)), ); From 9b45ce9b6709dbcb476f814de146af7ef0e79ef1 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 6 Aug 2024 13:15:29 -0700 Subject: [PATCH 063/103] [lexical-playground] Bug Fix: Update tooltip for redo button with correct macOS shortcut (#6497) --- packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index 333a1f02bbd..b32d841c7ec 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -879,7 +879,7 @@ export default function ToolbarPlugin({ onClick={() => { activeEditor.dispatchCommand(REDO_COMMAND, undefined); }} - title={IS_APPLE ? 'Redo (⌘Y)' : 'Redo (Ctrl+Y)'} + title={IS_APPLE ? 'Redo (⇧⌘Z)' : 'Redo (Ctrl+Y)'} type="button" className="toolbar-item" aria-label="Redo"> From 031891eda7f37563666f3fcf0d8f18d1fa8904a7 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 7 Aug 2024 20:27:03 +0100 Subject: [PATCH 064/103] Flow: add more HTMLDivElementDOMProps (#6500) --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 99004d8b137..917abd8de49 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -22,6 +22,9 @@ type InlineStyle = { type HTMLDivElementDOMProps = $ReadOnly<{ 'aria-label'?: void | string, 'aria-labeledby'?: void | string, + 'aria-activedescendant'?: void | string, + 'aria-autocomplete'?: void | string, + 'aria-owns'?: void | string, 'title'?: void | string, onClick?: void | (e: SyntheticEvent) => mixed, autoCapitalize?: void | boolean, From 9cb10ea3624b747ea68323908e76631bf46b31c4 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 8 Aug 2024 13:13:43 +0100 Subject: [PATCH 065/103] Fix splitText when detached (#6501) --- packages/lexical/src/nodes/LexicalTextNode.ts | 36 ++++++++++--------- .../__tests__/unit/LexicalTextNode.test.tsx | 12 +++++++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index c735a945099..03568abe7e5 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -935,7 +935,7 @@ export class TextNode extends LexicalNode { return [self]; } const firstPart = parts[0]; - const parent = self.getParentOrThrow(); + const parent = self.getParent(); let writableNode; const format = self.getFormat(); const style = self.getStyle(); @@ -1005,23 +1005,25 @@ export class TextNode extends LexicalNode { } // Insert the nodes into the parent's children - internalMarkSiblingsAsDirty(this); - const writableParent = parent.getWritable(); - const insertionIndex = this.getIndexWithinParent(); - if (hasReplacedSelf) { - writableParent.splice(insertionIndex, 0, splitNodes); - this.remove(); - } else { - writableParent.splice(insertionIndex, 1, splitNodes); - } + if (parent !== null) { + internalMarkSiblingsAsDirty(this); + const writableParent = parent.getWritable(); + const insertionIndex = this.getIndexWithinParent(); + if (hasReplacedSelf) { + writableParent.splice(insertionIndex, 0, splitNodes); + this.remove(); + } else { + writableParent.splice(insertionIndex, 1, splitNodes); + } - if ($isRangeSelection(selection)) { - $updateElementSelectionOnCreateDeleteNode( - selection, - parent, - insertionIndex, - partsLength - 1, - ); + if ($isRangeSelection(selection)) { + $updateElementSelectionOnCreateDeleteNode( + selection, + parent, + insertionIndex, + partsLength - 1, + ); + } } return splitNodes; diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index b034c96814a..37191abc831 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -582,6 +582,18 @@ describe('LexicalTextNode tests', () => { }); }, ); + + test('with detached parent', async () => { + await update(() => { + const textNode = $createTextNode('foo'); + const splits = textNode.splitText(1, 2); + expect(splits.map((split) => split.getTextContent())).toEqual([ + 'f', + 'o', + 'o', + ]); + }); + }); }); describe('createDOM()', () => { From 3f79ca010ffb657a5180396da0e4d38292f1f258 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sun, 11 Aug 2024 06:32:02 -0700 Subject: [PATCH 066/103] [lexical] Bug Fix: Fix decorator selection regression with short-circuiting (#6508) --- packages/lexical/src/LexicalNode.ts | 31 +-- .../src/__tests__/unit/LexicalNode.test.ts | 203 ++++++++++++++---- 2 files changed, 169 insertions(+), 65 deletions(-) diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 75b102f4a9d..591282da87d 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -295,27 +295,16 @@ export class LexicalNode { const parentNode = this.getParent(); if ($isDecoratorNode(this) && this.isInline() && parentNode) { - const {anchor, focus} = targetSelection; - - if (anchor.isBefore(focus)) { - const anchorNode = anchor.getNode() as ElementNode; - const isAnchorPointToLast = - anchor.offset === anchorNode.getChildrenSize(); - const isAnchorNodeIsParent = anchorNode.is(parentNode); - const isLastChild = anchorNode.getLastChildOrThrow().is(this); - - if (isAnchorPointToLast && isAnchorNodeIsParent && isLastChild) { - return false; - } - } else { - const focusNode = focus.getNode() as ElementNode; - const isFocusPointToLast = - focus.offset === focusNode.getChildrenSize(); - const isFocusNodeIsParent = focusNode.is(parentNode); - const isLastChild = focusNode.getLastChildOrThrow().is(this); - if (isFocusPointToLast && isFocusNodeIsParent && isLastChild) { - return false; - } + const firstPoint = targetSelection.isBackward() + ? targetSelection.focus + : targetSelection.anchor; + const firstElement = firstPoint.getNode() as ElementNode; + if ( + firstPoint.offset === firstElement.getChildrenSize() && + firstElement.is(parentNode) && + firstElement.getLastChildOrThrow().is(this) + ) { + return false; } } } diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index c9a178d4e7c..1879de07d49 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -12,9 +12,13 @@ import { $isDecoratorNode, $isElementNode, $isRangeSelection, + $setSelection, DecoratorNode, ElementNode, + LexicalEditor, + NodeKey, ParagraphNode, + RangeSelection, TextNode, } from 'lexical'; @@ -304,54 +308,165 @@ describe('LexicalNode tests', () => { await Promise.resolve().then(); }); - test('LexicalNode.isSelected(): with inline decorator node', async () => { - const {editor} = testEnv; + describe('LexicalNode.isSelected(): with inline decorator node', () => { + let editor: LexicalEditor; let paragraphNode1: ParagraphNode; let paragraphNode2: ParagraphNode; + let paragraphNode3: ParagraphNode; let inlineDecoratorNode: InlineDecoratorNode; - - editor.update(() => { - paragraphNode1 = $createParagraphNode(); - paragraphNode2 = $createParagraphNode(); - inlineDecoratorNode = new InlineDecoratorNode(); - paragraphNode1.append(inlineDecoratorNode); - $getRoot().append(paragraphNode1, paragraphNode2); - paragraphNode1.selectEnd(); - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - expect(selection.anchor.getNode().is(paragraphNode1)).toBe(true); - } - }); - - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - expect(selection.anchor.key).toBe(paragraphNode1.getKey()); - - selection.focus.set(paragraphNode2.getKey(), 1, 'element'); - } - }); - - await Promise.resolve().then(); - - editor.getEditorState().read(() => { - expect(inlineDecoratorNode.isSelected()).toBe(false); - }); - - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - selection.anchor.set(paragraphNode2.getKey(), 0, 'element'); - selection.focus.set(paragraphNode1.getKey(), 1, 'element'); - } - }); - - await Promise.resolve().then(); - - editor.getEditorState().read(() => { - expect(inlineDecoratorNode.isSelected()).toBe(false); + let names: Record; + beforeEach(() => { + editor = testEnv.editor; + editor.update(() => { + inlineDecoratorNode = new InlineDecoratorNode(); + paragraphNode1 = $createParagraphNode(); + paragraphNode2 = $createParagraphNode().append(inlineDecoratorNode); + paragraphNode3 = $createParagraphNode(); + names = { + [inlineDecoratorNode.getKey()]: 'd', + [paragraphNode1.getKey()]: 'p1', + [paragraphNode2.getKey()]: 'p2', + [paragraphNode3.getKey()]: 'p3', + }; + $getRoot() + .clear() + .append(paragraphNode1, paragraphNode2, paragraphNode3); + }); }); + const cases: { + label: string; + isSelected: boolean; + update: () => void; + }[] = [ + { + isSelected: true, + label: 'whole editor', + update() { + $getRoot().select(0); + }, + }, + { + isSelected: true, + label: 'containing paragraph', + update() { + paragraphNode2.select(0); + }, + }, + { + isSelected: true, + label: 'before and containing', + update() { + paragraphNode2 + .select(0) + .anchor.set(paragraphNode1.getKey(), 0, 'element'); + }, + }, + { + isSelected: true, + label: 'containing and after', + update() { + paragraphNode2 + .select(0) + .focus.set(paragraphNode3.getKey(), 0, 'element'); + }, + }, + { + isSelected: true, + label: 'before and after', + update() { + paragraphNode1 + .select(0) + .focus.set(paragraphNode3.getKey(), 0, 'element'); + }, + }, + { + isSelected: false, + label: 'collapsed before', + update() { + paragraphNode2.select(0, 0); + }, + }, + { + isSelected: false, + label: 'in another element', + update() { + paragraphNode1.select(0); + }, + }, + { + isSelected: false, + label: 'before', + update() { + paragraphNode1 + .select(0) + .focus.set(paragraphNode2.getKey(), 0, 'element'); + }, + }, + { + isSelected: false, + label: 'collapsed after', + update() { + paragraphNode2.selectEnd(); + }, + }, + { + isSelected: false, + label: 'after', + update() { + paragraphNode3 + .select(0) + .anchor.set( + paragraphNode2.getKey(), + paragraphNode2.getChildrenSize(), + 'element', + ); + }, + }, + ]; + for (const {label, isSelected, update} of cases) { + test(`${isSelected ? 'is' : "isn't"} selected ${label}`, () => { + editor.update(update); + const $verify = () => { + const selection = $getSelection() as RangeSelection; + expect($isRangeSelection(selection)).toBe(true); + const dbg = [selection.anchor, selection.focus] + .map( + (point) => + `(${names[point.key] || point.key}:${point.offset})`, + ) + .join(' '); + const nodes = `[${selection + .getNodes() + .map((k) => names[k.__key] || k.__key) + .join(',')}]`; + expect([dbg, nodes, inlineDecoratorNode.isSelected()]).toEqual([ + dbg, + nodes, + isSelected, + ]); + }; + editor.read($verify); + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const backwards = $createRangeSelection(); + backwards.anchor.set( + selection.focus.key, + selection.focus.offset, + selection.focus.type, + ); + backwards.focus.set( + selection.anchor.key, + selection.anchor.offset, + selection.anchor.type, + ); + $setSelection(backwards); + } + expect($isRangeSelection(selection)).toBe(true); + }); + editor.read($verify); + }); + } }); test('LexicalNode.getKey()', async () => { From db4c743b79e9c237634b262866e45b6350bcc978 Mon Sep 17 00:00:00 2001 From: Sherry Date: Mon, 12 Aug 2024 22:40:22 +0800 Subject: [PATCH 067/103] [lexical] surface more error details in reconciler (#6511) --- packages/lexical/src/LexicalReconciler.ts | 30 +++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 462dd22863e..0ad9cf2c911 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -340,7 +340,18 @@ function reconcileElementTerminatingLineBreak( const element = dom.__lexicalLineBreak; if (element != null) { - dom.removeChild(element); + 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 @@ -501,7 +512,22 @@ function $reconcileChildren( } else { const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey); const replacementDOM = $createNode(nextFrstChildKey, null, null); - dom.replaceChild(replacementDOM, lastDOM); + try { + dom.replaceChild(replacementDOM, lastDOM); + } catch (error) { + if (typeof error === 'object' && error != null) { + const msg = `${error.toString()} Parent: ${ + dom.tagName + }, new child: {tag: ${ + replacementDOM.tagName + } key: ${nextFrstChildKey}}, old child: {tag: ${ + lastDOM.tagName + }, key: ${prevFirstChildKey}}.`; + throw new Error(msg); + } else { + throw error; + } + } destroyNode(prevFirstChildKey, null); } const nextChildNode = activeNextNodeMap.get(nextFrstChildKey); From a1b5fd2851fed9ad2b4a24bf061ddde7027411a1 Mon Sep 17 00:00:00 2001 From: Divyansh Kumar Date: Mon, 12 Aug 2024 21:06:17 +0530 Subject: [PATCH 068/103] [@lexical/selection] Feature: yield target to style patch fn (#6472) Co-authored-by: Bob Ippolito --- .../__tests__/unit/LexicalSelection.test.tsx | 109 ++++++++++++++++++ .../lexical-selection/src/lexical-node.ts | 15 ++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index 73cfd62c0d1..121d13fe3ec 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -18,6 +18,7 @@ import {$createHeadingNode} from '@lexical/rich-text'; import { $addNodeStyle, $getSelectionStyleValueForProperty, + $patchStyleText, $setBlocksType, } from '@lexical/selection'; import {$createTableNodeWithDimensions} from '@lexical/table'; @@ -30,6 +31,7 @@ import { $getSelection, $isElementNode, $isRangeSelection, + $isTextNode, $setSelection, DecoratorNode, ElementNode, @@ -2500,6 +2502,113 @@ describe('LexicalSelection tests', () => { }); }); + describe('$patchStyle', () => { + it('should patch the style with the new style object', async () => { + await ReactTestUtils.act(async () => { + await editor!.update(() => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const textNode = $createTextNode('Hello, World!'); + textNode.setStyle('font-family: serif; color: red;'); + $addNodeStyle(textNode); + paragraph.append(textNode); + root.append(paragraph); + + const selection = $createRangeSelection(); + $setSelection(selection); + selection.insertParagraph(); + $setAnchorPoint({ + key: textNode.getKey(), + offset: 0, + type: 'text', + }); + + $setFocusPoint({ + key: textNode.getKey(), + offset: 10, + type: 'text', + }); + + const newStyle = { + color: 'blue', + 'font-family': 'Arial', + }; + + $patchStyleText(selection, newStyle); + + const cssFontFamilyValue = $getSelectionStyleValueForProperty( + selection, + 'font-family', + '', + ); + expect(cssFontFamilyValue).toBe('Arial'); + + const cssColorValue = $getSelectionStyleValueForProperty( + selection, + 'color', + '', + ); + expect(cssColorValue).toBe('blue'); + }); + }); + }); + + it('should patch the style with property function', async () => { + await ReactTestUtils.act(async () => { + await editor!.update(() => { + const currentColor = 'red'; + const nextColor = 'blue'; + + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const textNode = $createTextNode('Hello, World!'); + textNode.setStyle(`color: ${currentColor};`); + $addNodeStyle(textNode); + paragraph.append(textNode); + root.append(paragraph); + + const selection = $createRangeSelection(); + $setSelection(selection); + selection.insertParagraph(); + $setAnchorPoint({ + key: textNode.getKey(), + offset: 0, + type: 'text', + }); + + $setFocusPoint({ + key: textNode.getKey(), + offset: 10, + type: 'text', + }); + + const newStyle = { + color: jest.fn( + (current: string | null, target: LexicalNode | RangeSelection) => + nextColor, + ), + }; + + $patchStyleText(selection, newStyle); + + const cssColorValue = $getSelectionStyleValueForProperty( + selection, + 'color', + '', + ); + + expect(cssColorValue).toBe(nextColor); + expect(newStyle.color).toHaveBeenCalledTimes(1); + + const lastCall = newStyle.color.mock.lastCall!; + expect(lastCall[0]).toBe(currentColor); + // @ts-ignore - It expected to be a LexicalNode + expect($isTextNode(lastCall[1])).toBeTruthy(); + }); + }); + }); + }); + describe('$setBlocksType', () => { test('Collapsed selection in text', async () => { const testEditor = createTestEditor(); diff --git a/packages/lexical-selection/src/lexical-node.ts b/packages/lexical-selection/src/lexical-node.ts index b7b5a753ef0..32b7b0b802e 100644 --- a/packages/lexical-selection/src/lexical-node.ts +++ b/packages/lexical-selection/src/lexical-node.ts @@ -243,7 +243,9 @@ function $patchStyle( target: TextNode | RangeSelection, patch: Record< string, - string | null | ((currentStyleValue: string | null) => string) + | string + | null + | ((currentStyleValue: string | null, _target: typeof target) => string) >, ): void { const prevStyles = getStyleObjectFromCSS( @@ -251,8 +253,8 @@ function $patchStyle( ); const newStyles = Object.entries(patch).reduce>( (styles, [key, value]) => { - if (value instanceof Function) { - styles[key] = value(prevStyles[key]); + if (typeof value === 'function') { + styles[key] = value(prevStyles[key], target); } else if (value === null) { delete styles[key]; } else { @@ -278,7 +280,12 @@ export function $patchStyleText( selection: BaseSelection, patch: Record< string, - string | null | ((currentStyleValue: string | null) => string) + | string + | null + | (( + currentStyleValue: string | null, + target: TextNode | RangeSelection, + ) => string) >, ): void { const selectedNodes = selection.getNodes(); From 43caf29a733f533e3af27715436824ebf6121832 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 12 Aug 2024 09:51:59 -0700 Subject: [PATCH 069/103] [lexical] Refactor: [RFC] LexicalNode.afterCloneFrom to simplify clone implementation (#6505) --- .../lexical-website/docs/concepts/nodes.md | 2 + .../docs/concepts/serialization.md | 5 +- packages/lexical/src/LexicalNode.ts | 61 +++++++++++++++- packages/lexical/src/LexicalUtils.ts | 40 ++++------ .../src/__tests__/unit/LexicalNode.test.ts | 73 ++++++++++++++++++- .../lexical/src/nodes/LexicalElementNode.ts | 11 +++ .../lexical/src/nodes/LexicalParagraphNode.ts | 6 ++ packages/lexical/src/nodes/LexicalTabNode.ts | 11 +-- packages/lexical/src/nodes/LexicalTextNode.ts | 8 ++ 9 files changed, 181 insertions(+), 36 deletions(-) diff --git a/packages/lexical-website/docs/concepts/nodes.md b/packages/lexical-website/docs/concepts/nodes.md index 5ee80516040..aedb7bb43cb 100644 --- a/packages/lexical-website/docs/concepts/nodes.md +++ b/packages/lexical-website/docs/concepts/nodes.md @@ -111,6 +111,8 @@ class MyCustomNode extends SomeOtherNode { } static clone(node: MyCustomNode): MyCustomNode { + // If any state needs to be set after construction, it should be + // done by overriding the `afterCloneFrom` instance method. return new MyCustomNode(node.__foo, node.__key); } diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 84ee58e5202..64e7fe14193 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -365,10 +365,7 @@ export class ExtendedTextNode extends TextNode { } isSimpleText() { - return ( - (this.__type === 'text' || this.__type === 'extended-text') && - this.__mode === 0 - ); + return this.__type === 'extended-text' && this.__mode === 0; } exportJSON(): SerializedTextNode { diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 591282da87d..9d24f72867b 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -205,6 +205,62 @@ export class LexicalNode { ); } + /** + * Perform any state updates on the clone of prevNode that are not already + * handled by the constructor call in the static clone method. If you have + * state to update in your clone that is not handled directly by the + * constructor, it is advisable to override this method but it is required + * to include a call to `super.afterCloneFrom(prevNode)` in your + * implementation. This is only intended to be called by + * {@link $cloneWithProperties} function or via a super call. + * + * @example + * ```ts + * class ClassesTextNode extends TextNode { + * // Not shown: static getType, static importJSON, exportJSON, createDOM, updateDOM + * __classes = new Set(); + * static clone(node: ClassesTextNode): ClassesTextNode { + * // The inherited TextNode constructor is used here, so + * // classes is not set by this method. + * return new ClassesTextNode(node.__text, node.__key); + * } + * afterCloneFrom(node: this): void { + * // This calls TextNode.afterCloneFrom and LexicalNode.afterCloneFrom + * // for necessary state updates + * super.afterCloneFrom(node); + * this.__addClasses(node.__classes); + * } + * // This method is a private implementation detail, it is not + * // suitable for the public API because it does not call getWritable + * __addClasses(classNames: Iterable): this { + * for (const className of classNames) { + * this.__classes.add(className); + * } + * return this; + * } + * addClass(...classNames: string[]): this { + * return this.getWritable().__addClasses(classNames); + * } + * removeClass(...classNames: string[]): this { + * const node = this.getWritable(); + * for (const className of classNames) { + * this.__classes.delete(className); + * } + * return this; + * } + * getClasses(): Set { + * return this.getLatest().__classes; + * } + * } + * ``` + * + */ + afterCloneFrom(prevNode: this) { + this.__parent = prevNode.__parent; + this.__next = prevNode.__next; + this.__prev = prevNode.__prev; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any static importDOM?: () => DOMConversionMap | null; @@ -692,8 +748,9 @@ export class LexicalNode { } /** - * Returns a mutable version of the node. Will throw an error if - * called outside of a Lexical Editor {@link LexicalEditor.update} callback. + * Returns a mutable version of the node using {@link $cloneWithProperties} + * if necessary. Will throw an error if called outside of a Lexical Editor + * {@link LexicalEditor.update} callback. * */ getWritable(): this { diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index f735aba2306..229fb3e3f13 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -41,7 +41,6 @@ import { $isDecoratorNode, $isElementNode, $isLineBreakNode, - $isParagraphNode, $isRangeSelection, $isRootNode, $isTextNode, @@ -1753,8 +1752,13 @@ export function getCachedTypeToNodeMap( } /** - * Returns a clone of a node with the same key and parent/next/prev pointers and other - * properties that are not set by the KlassConstructor.clone (format, style, etc.). + * Returns a clone of a node using `node.constructor.clone()` followed by + * `clone.afterCloneFrom(node)`. The resulting clone must have the same key, + * parent/next/prev pointers, and other properties that are not set by + * `node.constructor.clone` (format, style, etc.). This is primarily used by + * {@link LexicalNode.getWritable} to create a writable version of an + * existing node. The clone is the same logical node as the original node, + * do not try and use this function to duplicate or copy an existing node. * * Does not mutate the EditorState. * @param node - The node to be cloned. @@ -1763,27 +1767,7 @@ export function getCachedTypeToNodeMap( export function $cloneWithProperties(latestNode: T): T { const constructor = latestNode.constructor; const mutableNode = constructor.clone(latestNode) as T; - mutableNode.__parent = latestNode.__parent; - mutableNode.__next = latestNode.__next; - mutableNode.__prev = latestNode.__prev; - if ($isElementNode(latestNode) && $isElementNode(mutableNode)) { - if ($isParagraphNode(latestNode) && $isParagraphNode(mutableNode)) { - mutableNode.__textFormat = latestNode.__textFormat; - mutableNode.__textStyle = latestNode.__textStyle; - } - mutableNode.__first = latestNode.__first; - mutableNode.__last = latestNode.__last; - mutableNode.__size = latestNode.__size; - mutableNode.__indent = latestNode.__indent; - mutableNode.__format = latestNode.__format; - mutableNode.__style = latestNode.__style; - mutableNode.__dir = latestNode.__dir; - } else if ($isTextNode(latestNode) && $isTextNode(mutableNode)) { - mutableNode.__format = latestNode.__format; - mutableNode.__style = latestNode.__style; - mutableNode.__mode = latestNode.__mode; - mutableNode.__detail = latestNode.__detail; - } + mutableNode.afterCloneFrom(latestNode); if (__DEV__) { invariant( mutableNode.__key === latestNode.__key, @@ -1791,6 +1775,14 @@ export function $cloneWithProperties(latestNode: T): T { constructor.name, constructor.getType(), ); + invariant( + mutableNode.__parent === latestNode.__parent && + mutableNode.__next === latestNode.__next && + mutableNode.__prev === latestNode.__prev, + "$cloneWithProperties: %s.clone(node) (with type '%s') overrided afterCloneFrom but did not call super.afterCloneFrom(prevNode)", + constructor.name, + constructor.getType(), + ); } return mutableNode; } diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index 1879de07d49..7373f898d80 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -7,22 +7,24 @@ */ import { + $createRangeSelection, $getRoot, $getSelection, $isDecoratorNode, $isElementNode, $isRangeSelection, $setSelection, + createEditor, DecoratorNode, ElementNode, LexicalEditor, NodeKey, ParagraphNode, RangeSelection, + SerializedTextNode, TextNode, } from 'lexical'; -import {$createRangeSelection} from '../..'; import {LexicalNode} from '../../LexicalNode'; import {$createParagraphNode} from '../../nodes/LexicalParagraphNode'; import {$createTextNode} from '../../nodes/LexicalTextNode'; @@ -151,6 +153,75 @@ describe('LexicalNode tests', () => { expect(() => LexicalNode.clone(node)).toThrow(); }); }); + test('LexicalNode.afterCloneFrom()', () => { + class VersionedTextNode extends TextNode { + // ['constructor']!: KlassConstructor; + __version = 0; + static getType(): 'vtext' { + return 'vtext'; + } + static clone(node: VersionedTextNode): VersionedTextNode { + return new VersionedTextNode(node.__text, node.__key); + } + static importJSON(node: SerializedTextNode): VersionedTextNode { + throw new Error('Not implemented'); + } + exportJSON(): SerializedTextNode { + throw new Error('Not implemented'); + } + afterCloneFrom(node: this): void { + super.afterCloneFrom(node); + this.__version = node.__version + 1; + } + } + const editor = createEditor({ + nodes: [VersionedTextNode], + onError(err) { + throw err; + }, + }); + let versionedTextNode: VersionedTextNode; + + editor.update( + () => { + versionedTextNode = new VersionedTextNode('test'); + $getRoot().append($createParagraphNode().append(versionedTextNode)); + expect(versionedTextNode.__version).toEqual(0); + }, + {discrete: true}, + ); + editor.update( + () => { + expect(versionedTextNode.getLatest().__version).toEqual(0); + expect( + versionedTextNode.setTextContent('update').setMode('token') + .__version, + ).toEqual(1); + }, + {discrete: true}, + ); + editor.update( + () => { + let latest = versionedTextNode.getLatest(); + expect(versionedTextNode.__version).toEqual(0); + expect(versionedTextNode.__mode).toEqual(0); + expect(versionedTextNode.getMode()).toEqual('token'); + expect(latest.__version).toEqual(1); + expect(latest.__mode).toEqual(1); + latest = latest.setTextContent('another update'); + expect(latest.__version).toEqual(2); + expect(latest.getWritable().__version).toEqual(2); + expect( + versionedTextNode.getLatest().getWritable().__version, + ).toEqual(2); + expect(versionedTextNode.getLatest().__version).toEqual(2); + expect(versionedTextNode.__mode).toEqual(0); + expect(versionedTextNode.getLatest().__mode).toEqual(1); + expect(versionedTextNode.getMode()).toEqual('token'); + }, + {discrete: true}, + ); + }); test('LexicalNode.getType()', async () => { const {editor} = testEnv; diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index 83c33c9776c..474d0b405ba 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -93,6 +93,17 @@ export class ElementNode extends LexicalNode { this.__dir = null; } + afterCloneFrom(prevNode: this) { + super.afterCloneFrom(prevNode); + this.__first = prevNode.__first; + this.__last = prevNode.__last; + this.__size = prevNode.__size; + this.__indent = prevNode.__indent; + this.__format = prevNode.__format; + this.__style = prevNode.__style; + this.__dir = prevNode.__dir; + } + getFormat(): number { const self = this.getLatest(); return self.__format; diff --git a/packages/lexical/src/nodes/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index 56649528897..deab3a2cc13 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -90,6 +90,12 @@ export class ParagraphNode extends ElementNode { return new ParagraphNode(node.__key); } + afterCloneFrom(prevNode: this) { + super.afterCloneFrom(prevNode); + this.__textFormat = prevNode.__textFormat; + this.__textStyle = prevNode.__textStyle; + } + // View createDOM(config: EditorConfig): HTMLElement { diff --git a/packages/lexical/src/nodes/LexicalTabNode.ts b/packages/lexical/src/nodes/LexicalTabNode.ts index 4a919b43968..d3182e40df0 100644 --- a/packages/lexical/src/nodes/LexicalTabNode.ts +++ b/packages/lexical/src/nodes/LexicalTabNode.ts @@ -29,12 +29,13 @@ export class TabNode extends TextNode { } static clone(node: TabNode): TabNode { - const newNode = new TabNode(node.__key); + return new TabNode(node.__key); + } + + afterCloneFrom(prevNode: this): void { + super.afterCloneFrom(prevNode); // TabNode __text can be either '\t' or ''. insertText will remove the empty Node - newNode.__text = node.__text; - newNode.__format = node.__format; - newNode.__style = node.__style; - return newNode; + this.__text = prevNode.__text; } constructor(key?: NodeKey) { diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 03568abe7e5..fad639a1c72 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -303,6 +303,14 @@ export class TextNode extends LexicalNode { return new TextNode(node.__text, node.__key); } + afterCloneFrom(prevNode: this): void { + super.afterCloneFrom(prevNode); + this.__format = prevNode.__format; + this.__style = prevNode.__style; + this.__mode = prevNode.__mode; + this.__detail = prevNode.__detail; + } + constructor(text: string, key?: NodeKey) { super(key); this.__text = text; From 21e50adcc74d7a4cc2f31ba4ac7cafda9c73d783 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 13 Aug 2024 07:14:35 -0700 Subject: [PATCH 070/103] [lexical-react] remove editor__DEPRECATED that has been deprecated for two years (#6494) --- .eslintignore | 1 + .prettierignore | 1 + .../flow/LexicalComposer.js.flow | 1 - .../flow/LexicalContentEditable.js.flow | 1 - .../lexical-react/src/LexicalComposer.tsx | 26 +++++++------------ .../src/LexicalContentEditable.tsx | 11 +++----- 6 files changed, 15 insertions(+), 26 deletions(-) diff --git a/.eslintignore b/.eslintignore index 0b4c244ec62..b97d5766408 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,3 +14,4 @@ .ts-temp **/.docusaurus /playwright-report +test-results diff --git a/.prettierignore b/.prettierignore index 45b5e4704eb..547468182f0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,3 +23,4 @@ flow-typed .prettierignore **/.docusaurus /playwright-report +test-results diff --git a/packages/lexical-react/flow/LexicalComposer.js.flow b/packages/lexical-react/flow/LexicalComposer.js.flow index b4cb35ce9b4..ae02446078c 100644 --- a/packages/lexical-react/flow/LexicalComposer.js.flow +++ b/packages/lexical-react/flow/LexicalComposer.js.flow @@ -23,7 +23,6 @@ export type InitialEditorStateType = | ((editor: LexicalEditor) => void); export type InitialConfigType = $ReadOnly<{ - editor__DEPRECATED?: LexicalEditor | null, editable?: boolean, namespace: string, nodes?: $ReadOnlyArray | LexicalNodeReplacement>, diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 917abd8de49..c5a7feb55fc 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -56,7 +56,6 @@ export type PlaceholderProps = export type Props = $ReadOnly<{ ...HTMLDivElementDOMProps, - editor__DEPRECATED?: LexicalEditor; ariaActiveDescendant?: string, ariaAutoComplete?: string, ariaControls?: string, diff --git a/packages/lexical-react/src/LexicalComposer.tsx b/packages/lexical-react/src/LexicalComposer.tsx index c40568b7d6d..f5a58ca0a69 100644 --- a/packages/lexical-react/src/LexicalComposer.tsx +++ b/packages/lexical-react/src/LexicalComposer.tsx @@ -39,7 +39,6 @@ export type InitialEditorStateType = | ((editor: LexicalEditor) => void); export type InitialConfigType = Readonly<{ - editor__DEPRECATED?: LexicalEditor | null; namespace: string; nodes?: ReadonlyArray | LexicalNodeReplacement>; onError: (error: Error, editor: LexicalEditor) => void; @@ -59,7 +58,6 @@ export function LexicalComposer({initialConfig, children}: Props): JSX.Element { const { theme, namespace, - editor__DEPRECATED: initialEditor, nodes, onError, editorState: initialEditorState, @@ -71,21 +69,15 @@ export function LexicalComposer({initialConfig, children}: Props): JSX.Element { theme, ); - let editor = initialEditor || null; - - if (editor === null) { - const newEditor = createEditor({ - editable: initialConfig.editable, - html, - namespace, - nodes, - onError: (error) => onError(error, newEditor), - theme, - }); - initializeEditor(newEditor, initialEditorState); - - editor = newEditor; - } + const editor = createEditor({ + editable: initialConfig.editable, + html, + namespace, + nodes, + onError: (error) => onError(error, editor), + theme, + }); + initializeEditor(editor, initialEditorState); return [editor, context]; }, diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index f94a5207395..657ba4fed3a 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -15,9 +15,8 @@ import {forwardRef, Ref, useLayoutEffect, useState} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; -export type Props = Omit & { - editor__DEPRECATED?: LexicalEditor; -} & ( +export type Props = Omit & + ( | { 'aria-placeholder'?: void; placeholder?: null; @@ -36,10 +35,8 @@ function ContentEditableImpl( props: Props, ref: Ref, ): JSX.Element { - const {placeholder, editor__DEPRECATED, ...rest} = props; - // editor__DEPRECATED will always be defined for non MLC surfaces - // eslint-disable-next-line react-hooks/rules-of-hooks - const editor = editor__DEPRECATED || useLexicalComposerContext()[0]; + const {placeholder, ...rest} = props; + const [editor] = useLexicalComposerContext(); return ( <> From cb0b7312a4a7ff69cce12719409a8b685daa963e Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 15 Aug 2024 03:17:03 +0100 Subject: [PATCH 071/103] Fix OverflowNode configuration (#6027) Co-authored-by: Bob Ippolito --- packages/lexical-overflow/src/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/lexical-overflow/src/index.ts b/packages/lexical-overflow/src/index.ts index 8cdc0b0e8cc..b19e89f15ed 100644 --- a/packages/lexical-overflow/src/index.ts +++ b/packages/lexical-overflow/src/index.ts @@ -69,7 +69,15 @@ export class OverflowNode extends ElementNode { return parent.insertNewAfter(selection, restoreSelection); } - excludeFromCopy(): boolean { + canBeEmpty(): false { + return false; + } + + isInline(): true { + return true; + } + + excludeFromCopy(): true { return true; } } From 510720e727a6bdd86a10ebc2da03fa916746bbd4 Mon Sep 17 00:00:00 2001 From: Turner Date: Thu, 15 Aug 2024 10:17:35 +0800 Subject: [PATCH 072/103] [lexical-react] Fix: Fix React.startTransition on Webpack + React 17 (#6517) Co-authored-by: guohao --- packages/lexical-devtools/tsconfig.json | 1 + .../src/LexicalNodeMenuPlugin.tsx | 9 +------- .../src/LexicalTypeaheadMenuPlugin.tsx | 9 +------- packages/shared/src/reactPatches.ts | 22 +++++++++++++++++++ tsconfig.build.json | 1 + tsconfig.json | 1 + 6 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 packages/shared/src/reactPatches.ts diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json index a6fd399e833..74c92e8040d 100644 --- a/packages/lexical-devtools/tsconfig.json +++ b/packages/lexical-devtools/tsconfig.json @@ -165,6 +165,7 @@ "shared/invariant": ["../shared/src/invariant.ts"], "shared/normalizeClassNames": ["../shared/src/normalizeClassNames.ts"], "shared/react-test-utils": ["../shared/src/react-test-utils.ts"], + "shared/reactPatches": ["../shared/src/reactPatches.ts"], "shared/simpleDiffWithCursor": ["../shared/src/simpleDiffWithCursor.ts"], "shared/useLayoutEffect": ["../shared/src/useLayoutEffect.ts"], "shared/warnOnlyOnce": ["../shared/src/warnOnlyOnce.ts"] diff --git a/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx b/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx index 9b52a3791ad..3589ff55f62 100644 --- a/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx @@ -18,17 +18,10 @@ import { } from 'lexical'; import {useCallback, useEffect, useState} from 'react'; import * as React from 'react'; +import {startTransition} from 'shared/reactPatches'; import {LexicalMenu, MenuOption, useMenuAnchorRef} from './shared/LexicalMenu'; -function startTransition(callback: () => void) { - if (React.startTransition) { - React.startTransition(callback); - } else { - callback(); - } -} - export type NodeMenuPluginProps = { onSelectOption: ( option: TOption, diff --git a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx index af59b530f59..d6bbd6014d4 100644 --- a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx @@ -28,6 +28,7 @@ import { } from 'lexical'; import {useCallback, useEffect, useState} from 'react'; import * as React from 'react'; +import {startTransition} from 'shared/reactPatches'; import {LexicalMenu, MenuOption, useMenuAnchorRef} from './shared/LexicalMenu'; @@ -105,14 +106,6 @@ function isSelectionOnEntityBoundary( }); } -function startTransition(callback: () => void) { - if (React.startTransition) { - React.startTransition(callback); - } else { - callback(); - } -} - // Got from https://stackoverflow.com/a/42543908/2013580 export function getScrollParent( element: HTMLElement, diff --git a/packages/shared/src/reactPatches.ts b/packages/shared/src/reactPatches.ts new file mode 100644 index 00000000000..9685cd89e95 --- /dev/null +++ b/packages/shared/src/reactPatches.ts @@ -0,0 +1,22 @@ +/** + * 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 React from 'react'; + +// Webpack + React 17 fails to compile on the usage of `React.startTransition` or +// `React["startTransition"]` even if it's behind a feature detection of +// `"startTransition" in React`. Moving this to a constant avoids the issue :/ +const START_TRANSITION = 'startTransition'; + +export function startTransition(callback: () => void) { + if (START_TRANSITION in React) { + React[START_TRANSITION](callback); + } else { + callback(); + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json index febc18a834a..227dca5252a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -168,6 +168,7 @@ "./packages/shared/src/normalizeClassNames.ts" ], "shared/react-test-utils": ["./packages/shared/src/react-test-utils.ts"], + "shared/reactPatches": ["./packages/shared/src/reactPatches.ts"], "shared/simpleDiffWithCursor": [ "./packages/shared/src/simpleDiffWithCursor.ts" ], diff --git a/tsconfig.json b/tsconfig.json index a090e0449b2..50f67a71a8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -176,6 +176,7 @@ "./packages/shared/src/normalizeClassNames.ts" ], "shared/react-test-utils": ["./packages/shared/src/react-test-utils.ts"], + "shared/reactPatches": ["./packages/shared/src/reactPatches.ts"], "shared/simpleDiffWithCursor": [ "./packages/shared/src/simpleDiffWithCursor.ts" ], From 7cf2e247a5a8a60ae70ff14940e8ae652ddb102a Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 20 Aug 2024 22:26:02 -0700 Subject: [PATCH 073/103] Revert "Fix OverflowNode configuration" (#6535) --- packages/lexical-overflow/src/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/lexical-overflow/src/index.ts b/packages/lexical-overflow/src/index.ts index b19e89f15ed..8cdc0b0e8cc 100644 --- a/packages/lexical-overflow/src/index.ts +++ b/packages/lexical-overflow/src/index.ts @@ -69,15 +69,7 @@ export class OverflowNode extends ElementNode { return parent.insertNewAfter(selection, restoreSelection); } - canBeEmpty(): false { - return false; - } - - isInline(): true { - return true; - } - - excludeFromCopy(): true { + excludeFromCopy(): boolean { return true; } } From 0373248ac830777434e7599172323925e9c83c72 Mon Sep 17 00:00:00 2001 From: Botho <1258870+elbotho@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:06:13 +0200 Subject: [PATCH 074/103] [lexical-table] Bug Fix: Selection in tables with merged cells (#6529) Co-authored-by: Ivaylo Pavlov --- packages/lexical-table/src/LexicalTableNode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 95d03895bae..3e695eaa475 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -162,7 +162,9 @@ export class TableNode extends ElementNode { return null; } - const cell = row[x]; + const index = x < row.length ? x : row.length - 1; + + const cell = row[index]; if (cell == null) { return null; From 24cf9e35580be3b4bf9732de236793705d0d28b5 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 21 Aug 2024 16:40:21 -0700 Subject: [PATCH 075/103] [*] Chore: Mark additional tests as flaky from #6535 test runs (#6536) --- .../lexical/ListsCopyAndPaste.spec.mjs | 351 +++++++++--------- .../__tests__/e2e/HorizontalRule.spec.mjs | 205 +++++----- .../__tests__/e2e/Links.spec.mjs | 143 +++---- .../__tests__/e2e/Selection.spec.mjs | 106 +++--- .../__tests__/e2e/Tab.spec.mjs | 130 +++---- .../__tests__/e2e/TextFormatting.spec.mjs | 146 ++++---- .../regression/429-swapping-emoji.spec.mjs | 135 +++---- 7 files changed, 609 insertions(+), 607 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs index 1d0da269d17..17d628ebff5 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs @@ -108,183 +108,182 @@ test.describe('Lists CopyAndPaste', () => { }); }); - test('Copy and paste of partial list items into the list', async ({ - page, - isPlainText, - isCollab, - browserName, - }) => { - test.skip(isPlainText); - - await focusEditor(page); - - // Add three list items - await page.keyboard.type('- one'); - await page.keyboard.press('Enter'); - await page.keyboard.type('two'); - await page.keyboard.press('Enter'); - await page.keyboard.type('three'); - - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); - - // Add a paragraph - await page.keyboard.type('Some text.'); - - await assertHTML( - page, - html` -

                      -
                    • - one -
                    • -
                    • - two -
                    • -
                    • - three -
                    • -
                    -

                    - Some text. -

                    - `, - ); - await assertSelection(page, { - anchorOffset: 10, - anchorPath: [1, 0, 0], - focusOffset: 10, - focusPath: [1, 0, 0], - }); - - await page.keyboard.down('Shift'); - await moveToLineBeginning(page); - await moveLeft(page, 3); - await page.keyboard.up('Shift'); - - await assertSelection(page, { - anchorOffset: 10, - anchorPath: [1, 0, 0], - focusOffset: 3, - focusPath: [0, 2, 0, 0], - }); - - // Copy the partial list item and paragraph - const clipboard = await copyToClipboard(page); - - // Select all and remove content - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('ArrowUp'); - if (!IS_WINDOWS && browserName === 'firefox') { + test( + 'Copy and paste of partial list items into the list', + {tag: '@flaky'}, + async ({page, isPlainText, isCollab, browserName}) => { + test.skip(isPlainText); + + await focusEditor(page); + + // Add three list items + await page.keyboard.type('- one'); + await page.keyboard.press('Enter'); + await page.keyboard.type('two'); + await page.keyboard.press('Enter'); + await page.keyboard.type('three'); + + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + + // Add a paragraph + await page.keyboard.type('Some text.'); + + await assertHTML( + page, + html` +
                      +
                    • + one +
                    • +
                    • + two +
                    • +
                    • + three +
                    • +
                    +

                    + Some text. +

                    + `, + ); + await assertSelection(page, { + anchorOffset: 10, + anchorPath: [1, 0, 0], + focusOffset: 10, + focusPath: [1, 0, 0], + }); + + await page.keyboard.down('Shift'); + await moveToLineBeginning(page); + await moveLeft(page, 3); + await page.keyboard.up('Shift'); + + await assertSelection(page, { + anchorOffset: 10, + anchorPath: [1, 0, 0], + focusOffset: 3, + focusPath: [0, 2, 0, 0], + }); + + // Copy the partial list item and paragraph + const clipboard = await copyToClipboard(page); + + // Select all and remove content await page.keyboard.press('ArrowUp'); - } - await moveToLineEnd(page); - - await page.keyboard.down('Enter'); - - await assertHTML( - page, - html` -
                      -
                    • - one -
                    • -
                    • -
                      -
                    • -
                    • - two -
                    • -
                    • - three -
                    • -
                    -

                    - Some text. -

                    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0, 1], - focusOffset: 0, - focusPath: [0, 1], - }); - - await pasteFromClipboard(page, clipboard); - - await assertHTML( - page, - html` -
                      -
                    • - one -
                    • -
                    • - ee -
                    • -
                    -

                    - Some text. -

                    -
                      -
                    • - two -
                    • -
                    • - three -
                    • -
                    -

                    - Some text. -

                    - `, - ); - await assertSelection(page, { - anchorOffset: 10, - anchorPath: [1, 0, 0], - focusOffset: 10, - focusPath: [1, 0, 0], - }); - }); + await page.keyboard.press('ArrowUp'); + if (!IS_WINDOWS && browserName === 'firefox') { + await page.keyboard.press('ArrowUp'); + } + await moveToLineEnd(page); + + await page.keyboard.down('Enter'); + + await assertHTML( + page, + html` +
                      +
                    • + one +
                    • +
                    • +
                      +
                    • +
                    • + two +
                    • +
                    • + three +
                    • +
                    +

                    + Some text. +

                    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0, 1], + focusOffset: 0, + focusPath: [0, 1], + }); + + await pasteFromClipboard(page, clipboard); + + await assertHTML( + page, + html` +
                      +
                    • + one +
                    • +
                    • + ee +
                    • +
                    +

                    + Some text. +

                    +
                      +
                    • + two +
                    • +
                    • + three +
                    • +
                    +

                    + Some text. +

                    + `, + ); + await assertSelection(page, { + anchorOffset: 10, + anchorPath: [1, 0, 0], + focusOffset: 10, + focusPath: [1, 0, 0], + }); + }, + ); test('Copy list items and paste back into list', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs b/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs index 47a68316854..37ea391f7f9 100644 --- a/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs @@ -27,131 +27,130 @@ import { test.describe('HorizontalRule', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test('Can create a horizontal rule and move selection around it', async ({ - page, - isCollab, - isPlainText, - browserName, - }) => { - test.skip(isPlainText); - await focusEditor(page); + test( + 'Can create a horizontal rule and move selection around it', + {tag: '@flaky'}, + async ({page, isCollab, isPlainText, browserName}) => { + test.skip(isPlainText); + await focusEditor(page); - await selectFromInsertDropdown(page, '.horizontal-rule'); + await selectFromInsertDropdown(page, '.horizontal-rule'); - await waitForSelector(page, 'hr'); + await waitForSelector(page, 'hr'); - await assertHTML( - page, - html` -


                    -
                    -


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


                    +
                    +


                    + `, + ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowUp'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0], - focusOffset: 0, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 0, + focusPath: [0], + }); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0], - focusOffset: 0, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 0, + focusPath: [0], + }); - await page.keyboard.type('Some text'); + await page.keyboard.type('Some text'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.type('Some more text'); + await page.keyboard.type('Some more text'); - await assertHTML( - page, - html` -

                    - Some text -

                    -
                    -

                    - Some more text -

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

                    + Some text +

                    +
                    +

                    + Some more text +

                    + `, + ); - await moveToLineBeginning(page); + await moveToLineBeginning(page); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await assertSelection(page, { - anchorOffset: 1, - anchorPath: [0], - focusOffset: 1, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [0], + focusOffset: 1, + focusPath: [0], + }); - await pressBackspace(page, 10); + await pressBackspace(page, 10); - // Collab doesn't process the cursor correctly - if (!isCollab) { - await assertHTML( - page, - '

                    Some more text

                    ', - ); - } + // Collab doesn't process the cursor correctly + if (!isCollab) { + await assertHTML( + page, + '

                    Some more text

                    ', + ); + } - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [], - focusOffset: 0, - focusPath: [], - }); - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [], + focusOffset: 0, + focusPath: [], + }); + }, + ); test('Will add a horizontal rule at the end of a current TextNode and move selection to the new ParagraphNode.', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index 289ccf34996..0523dd0323c 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -25,7 +25,6 @@ import { focusEditor, html, initialize, - IS_LINUX, keyDownCtrlOrMeta, keyUpCtrlOrMeta, pasteFromClipboard, @@ -1923,82 +1922,86 @@ test.describe.parallel('Links', () => { ); }); - test('Can handle pressing Enter inside a Link', async ({page}) => { - await focusEditor(page); - await page.keyboard.type('Hello awesome'); - await selectAll(page); - await click(page, '.link'); - await click(page, '.link-confirm'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.type('world'); + test( + 'Can handle pressing Enter inside a Link', + {tag: '@flaky'}, + async ({page}) => { + await focusEditor(page); + await page.keyboard.type('Hello awesome'); + await selectAll(page); + await click(page, '.link'); + await click(page, '.link-confirm'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.type('world'); - await moveToLineBeginning(page); - await moveRight(page, 6); + await moveToLineBeginning(page); + await moveRight(page, 6); - await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

                    - - Hello - -

                    -

                    - - awesome - - world -

                    - `, - undefined, - {ignoreClasses: true}, - ); - }); + await assertHTML( + page, + html` +

                    + + Hello + +

                    +

                    + + awesome + + world +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }, + ); - test('Can handle pressing Enter inside a Link containing multiple TextNodes', async ({ - page, - isCollab, - }) => { - test.fixme(isCollab && IS_LINUX, 'Flaky on Linux + Collab'); - await focusEditor(page); - await page.keyboard.type('Hello '); - await toggleBold(page); - await page.keyboard.type('awe'); - await toggleBold(page); - await page.keyboard.type('some'); - await selectAll(page); - await click(page, '.link'); - await click(page, '.link-confirm'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.type(' world'); + test( + 'Can handle pressing Enter inside a Link containing multiple TextNodes', + {tag: '@flaky'}, + async ({page, isCollab}) => { + await focusEditor(page); + await page.keyboard.type('Hello '); + await toggleBold(page); + await page.keyboard.type('awe'); + await toggleBold(page); + await page.keyboard.type('some'); + await selectAll(page); + await click(page, '.link'); + await click(page, '.link-confirm'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.type(' world'); - await moveToLineBeginning(page); - await moveRight(page, 6); + await moveToLineBeginning(page); + await moveRight(page, 6); - await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

                    - - Hello - -

                    -

                    - - awe - some - - world -

                    - `, - undefined, - {ignoreClasses: true}, - ); - }); + await assertHTML( + page, + html` +

                    + + Hello + +

                    +

                    + + awe + some + + world +

                    + `, + undefined, + {ignoreClasses: true}, + ); + }, + ); test( 'Can handle pressing Enter at the beginning of a Link', diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index aa674960347..651e8707cdb 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -874,30 +874,28 @@ test.describe.parallel('Selection', () => { }); }); - test('shift+arrowdown into a table, when the table is the last node, selects the whole table', async ({ - page, - isPlainText, - isCollab, - browserName, - legacyEvents, - }) => { - test.skip(isPlainText); - test.fixme(browserName === 'chromium' && legacyEvents); - await focusEditor(page); - await insertTable(page, 2, 2); - await moveToEditorEnd(page); - await deleteBackward(page); - 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, 1, 1], - }); - }); + test( + 'shift+arrowdown into a table, when the table is the last node, selects the whole table', + {tag: '@flaky'}, + async ({page, isPlainText, isCollab, browserName, legacyEvents}) => { + test.skip(isPlainText); + test.fixme(browserName === 'chromium' && legacyEvents); + await focusEditor(page); + await insertTable(page, 2, 2); + await moveToEditorEnd(page); + await deleteBackward(page); + 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, 1, 1], + }); + }, + ); test('shift+arrowup into a table, when the table is the first node, selects the whole table', async ({ page, @@ -924,37 +922,35 @@ test.describe.parallel('Selection', () => { }); }); - test('shift+arrowdown into a table, when the table is the only node, selects the whole table', async ({ - page, - isPlainText, - isCollab, - legacyEvents, - browserName, - }) => { - test.skip(isPlainText); - test.fixme(browserName === 'chromium' && legacyEvents); - await focusEditor(page); - await insertTable(page, 2, 2); - await moveToEditorBeginning(page); - await deleteBackward(page); - await moveToEditorEnd(page); - await deleteBackward(page); - await moveToEditorBeginning(page); - await moveUp(page, 1); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [], - focusOffset: 0, - focusPath: [], - }); - await page.keyboard.down('Shift'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.up('Shift'); - await assertTableSelectionCoordinates(page, { - anchor: {x: 0, y: 0}, - focus: {x: 1, y: 1}, - }); - }); + test( + 'shift+arrowdown into a table, when the table is the only node, selects the whole table', + {tag: '@flaky'}, + async ({page, isPlainText, isCollab, legacyEvents, browserName}) => { + test.skip(isPlainText); + test.fixme(browserName === 'chromium' && legacyEvents); + await focusEditor(page); + await insertTable(page, 2, 2); + await moveToEditorBeginning(page); + await deleteBackward(page); + await moveToEditorEnd(page); + await deleteBackward(page); + await moveToEditorBeginning(page); + await moveUp(page, 1); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [], + focusOffset: 0, + focusPath: [], + }); + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.up('Shift'); + await assertTableSelectionCoordinates(page, { + anchor: {x: 0, y: 0}, + focus: {x: 1, y: 1}, + }); + }, + ); test('shift+arrowup into a table, when the table is the only node, selects the whole table', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs index 0019f2fb25c..abbfd5c316c 100644 --- a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs @@ -17,71 +17,75 @@ import { /* eslint-disable sort-keys-fix/sort-keys-fix */ test.describe('Tab', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test(`can tab + IME`, async ({page, isPlainText, browserName}) => { - // CDP session is only available in Chromium - test.skip( - isPlainText || browserName === 'firefox' || browserName === 'webkit', - ); + test( + `can tab + IME`, + {tag: '@flaky'}, + async ({page, isPlainText, browserName}) => { + // CDP session is only available in Chromium + test.skip( + isPlainText || browserName === 'firefox' || browserName === 'webkit', + ); - const client = await page.context().newCDPSession(page); - async function imeType() { - // await page.keyboard.imeSetComposition('s', 1, 1); - await client.send('Input.imeSetComposition', { - selectionStart: 1, - selectionEnd: 1, - text: 's', - }); - // await page.keyboard.imeSetComposition('す', 1, 1); - await client.send('Input.imeSetComposition', { - selectionStart: 1, - selectionEnd: 1, - text: 'す', - }); - // await page.keyboard.imeSetComposition('すs', 2, 2); - await client.send('Input.imeSetComposition', { - selectionStart: 2, - selectionEnd: 2, - text: 'すs', - }); - // await page.keyboard.imeSetComposition('すsh', 3, 3); - await client.send('Input.imeSetComposition', { - selectionStart: 3, - selectionEnd: 3, - text: 'すsh', - }); - // await page.keyboard.imeSetComposition('すし', 2, 2); - await client.send('Input.imeSetComposition', { - selectionStart: 2, - selectionEnd: 2, - text: 'すし', - }); - // await page.keyboard.insertText('すし'); - await client.send('Input.insertText', { - text: 'すし', - }); - await page.keyboard.type(' '); - } - await focusEditor(page); - // Indent - await page.keyboard.press('Tab'); - await imeType(); - await page.keyboard.press('Tab'); - await imeType(); + const client = await page.context().newCDPSession(page); + async function imeType() { + // await page.keyboard.imeSetComposition('s', 1, 1); + await client.send('Input.imeSetComposition', { + selectionStart: 1, + selectionEnd: 1, + text: 's', + }); + // await page.keyboard.imeSetComposition('す', 1, 1); + await client.send('Input.imeSetComposition', { + selectionStart: 1, + selectionEnd: 1, + text: 'す', + }); + // await page.keyboard.imeSetComposition('すs', 2, 2); + await client.send('Input.imeSetComposition', { + selectionStart: 2, + selectionEnd: 2, + text: 'すs', + }); + // await page.keyboard.imeSetComposition('すsh', 3, 3); + await client.send('Input.imeSetComposition', { + selectionStart: 3, + selectionEnd: 3, + text: 'すsh', + }); + // await page.keyboard.imeSetComposition('すし', 2, 2); + await client.send('Input.imeSetComposition', { + selectionStart: 2, + selectionEnd: 2, + text: 'すし', + }); + // await page.keyboard.insertText('すし'); + await client.send('Input.insertText', { + text: 'すし', + }); + await page.keyboard.type(' '); + } + await focusEditor(page); + // Indent + await page.keyboard.press('Tab'); + await imeType(); + await page.keyboard.press('Tab'); + await imeType(); - await assertHTML( - page, - html` -

                    - すし - - すし -

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

                    + すし + + すし +

                    + `, + ); + }, + ); test('can tab inside code block #4399', async ({page, isPlainText}) => { test.skip(isPlainText); diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index c4e715a8624..528cec14d12 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -1111,23 +1111,77 @@ test.describe.parallel('TextFormatting', () => { expect(isButtonActiveStatusDisplayedCorrectly).toBe(true); }); - test('Regression #2523: can toggle format when selecting a TextNode edge followed by a non TextNode; ', async ({ - page, - isCollab, - isPlainText, - }) => { - test.skip(isPlainText); - await focusEditor(page); - - await page.keyboard.type('A'); - await insertSampleImage(page); - await page.keyboard.type('BC'); - - await moveLeft(page, 1); - await selectCharacters(page, 'left', 2); - - if (!isCollab) { - await waitForSelector(page, '.editor-image img'); + test( + 'Regression #2523: can toggle format when selecting a TextNode edge followed by a non TextNode; ', + {tag: '@flaky'}, + async ({page, isCollab, isPlainText}) => { + test.skip(isPlainText); + await focusEditor(page); + + await page.keyboard.type('A'); + await insertSampleImage(page); + await page.keyboard.type('BC'); + + await moveLeft(page, 1); + await selectCharacters(page, 'left', 2); + + if (!isCollab) { + await waitForSelector(page, '.editor-image img'); + await assertHTML( + page, + html` +

                    + A + +

                    + Yellow flower in tilt shift lens +
                    + + BC +

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

                    + A + +

                    + Yellow flower in tilt shift lens +
                    + + + B + + C +

                    + `, + ); + await toggleBold(page); await assertHTML( page, html` @@ -1141,7 +1195,6 @@ test.describe.parallel('TextFormatting', () => { data-lexical-decorator="true">
                    Yellow flower in tilt shift lens {

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

                    - A - -

                    - Yellow flower in tilt shift lens -
                    - - - B - - C -

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

                    - A - -

                    - Yellow flower in tilt shift lens -
                    - - BC -

                    - `, - ); - }); + }, + ); test('Multiline selection format ignores new lines', async ({ page, diff --git a/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs b/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs index 4fca4299fba..2f6511f8594 100644 --- a/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs @@ -18,42 +18,15 @@ import { test.describe('Regression test #429', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test(`Can add new lines before the line with emoji`, async ({ - isRichText, - page, - }) => { - await focusEditor(page); - await page.keyboard.type(':) or :('); - await assertHTML( - page, - html` -

                    - - 🙂 - - or - - 🙁 - -

                    - `, - ); - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0, 2, 0, 0], - focusOffset: 2, - focusPath: [0, 2, 0, 0], - }); - - await moveLeft(page, 6); - await page.keyboard.press('Enter'); - if (isRichText) { + test( + `Can add new lines before the line with emoji`, + {tag: '@flaky'}, + async ({isRichText, page}) => { + await focusEditor(page); + await page.keyboard.type(':) or :('); await assertHTML( page, html` -


                    @@ -68,19 +41,71 @@ test.describe('Regression test #429', () => { `, ); await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1, 0, 0, 0], - focusOffset: 0, - focusPath: [1, 0, 0, 0], + anchorOffset: 2, + anchorPath: [0, 2, 0, 0], + focusOffset: 2, + focusPath: [0, 2, 0, 0], }); - } else { + + await moveLeft(page, 6); + await page.keyboard.press('Enter'); + if (isRichText) { + await assertHTML( + page, + html` +


                    +

                    + + 🙂 + + or + + 🙁 + +

                    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [1, 0, 0, 0], + focusOffset: 0, + focusPath: [1, 0, 0, 0], + }); + } else { + await assertHTML( + page, + html` +

                    +
                    + + 🙂 + + or + + 🙁 + +

                    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0, 1, 0, 0], + focusOffset: 0, + focusPath: [0, 1, 0, 0], + }); + } + + await page.keyboard.press('Backspace'); await assertHTML( page, html`

                    -
                    🙂 @@ -93,34 +118,10 @@ test.describe('Regression test #429', () => { ); await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 1, 0, 0], + anchorPath: [0, 0, 0, 0], focusOffset: 0, - focusPath: [0, 1, 0, 0], + focusPath: [0, 0, 0, 0], }); - } - - await page.keyboard.press('Backspace'); - await assertHTML( - page, - html` -

                    - - 🙂 - - or - - 🙁 - -

                    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0, 0, 0, 0], - focusOffset: 0, - focusPath: [0, 0, 0, 0], - }); - }); + }, + ); }); From aab6bf623f2b9b6dce71137a4ed3d3ea2a6ed928 Mon Sep 17 00:00:00 2001 From: jrfitzsimmons Date: Fri, 23 Aug 2024 01:26:43 +1000 Subject: [PATCH 076/103] [lexical-list] Bug Fix: handle non-integer numbers in setIndent (#6522) Co-authored-by: Bob Ippolito --- packages/lexical-list/src/LexicalListItemNode.ts | 7 +++---- .../src/__tests__/unit/LexicalListItemNode.test.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index d2091e2c10a..f4fafcba71a 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -357,10 +357,9 @@ export class ListItemNode extends ElementNode { } setIndent(indent: number): this { - invariant( - typeof indent === 'number' && indent > -1, - 'Invalid indent value.', - ); + invariant(typeof indent === 'number', 'Invalid indent value.'); + indent = Math.floor(indent); + invariant(indent >= 0, 'Indent value must be non-negative.'); let currentIndent = this.getIndent(); while (currentIndent !== indent) { if (currentIndent < indent) { diff --git a/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts b/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts index 75bb253fc53..d36b8f1cbd5 100644 --- a/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts +++ b/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts @@ -1348,6 +1348,18 @@ describe('LexicalListItemNode tests', () => { `, ); }); + + it('handles fractional indent values', async () => { + const {editor} = testEnv; + + await editor.update(() => { + listItemNode1.setIndent(0.5); + }); + + await editor.update(() => { + expect(listItemNode1.getIndent()).toBe(0); + }); + }); }); }); }); From fef40152b7160ef4ccc399dfa1e3ac97b65d78cf Mon Sep 17 00:00:00 2001 From: Mingxuan Wang Date: Thu, 22 Aug 2024 23:27:17 +0800 Subject: [PATCH 077/103] Chore: change className props in TreeView component to optional (#6531) Co-authored-by: Bob Ippolito --- packages/lexical-devtools-core/src/TreeView.tsx | 12 ++++++------ packages/lexical-react/src/LexicalTreeView.tsx | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/lexical-devtools-core/src/TreeView.tsx b/packages/lexical-devtools-core/src/TreeView.tsx index 0ed8aea6ec4..65bc7e51e16 100644 --- a/packages/lexical-devtools-core/src/TreeView.tsx +++ b/packages/lexical-devtools-core/src/TreeView.tsx @@ -17,12 +17,12 @@ export const TreeView = forwardRef< HTMLPreElement, { editorState: EditorState; - treeTypeButtonClassName: string; - timeTravelButtonClassName: string; - timeTravelPanelButtonClassName: string; - timeTravelPanelClassName: string; - timeTravelPanelSliderClassName: string; - viewClassName: string; + treeTypeButtonClassName?: string; + timeTravelButtonClassName?: string; + timeTravelPanelButtonClassName?: string; + timeTravelPanelClassName?: string; + timeTravelPanelSliderClassName?: string; + viewClassName?: string; generateContent: (exportDOM: boolean) => Promise; setEditorState: (state: EditorState, options?: EditorSetOptions) => void; setEditorReadOnly: (isReadonly: boolean) => void; diff --git a/packages/lexical-react/src/LexicalTreeView.tsx b/packages/lexical-react/src/LexicalTreeView.tsx index c3a282b4760..db536de589b 100644 --- a/packages/lexical-react/src/LexicalTreeView.tsx +++ b/packages/lexical-react/src/LexicalTreeView.tsx @@ -29,12 +29,12 @@ export function TreeView({ customPrintNode, }: { editor: LexicalEditor; - treeTypeButtonClassName: string; - timeTravelButtonClassName: string; - timeTravelPanelButtonClassName: string; - timeTravelPanelClassName: string; - timeTravelPanelSliderClassName: string; - viewClassName: string; + treeTypeButtonClassName?: string; + timeTravelButtonClassName?: string; + timeTravelPanelButtonClassName?: string; + timeTravelPanelClassName?: string; + timeTravelPanelSliderClassName?: string; + viewClassName?: string; customPrintNode?: CustomPrintNodeFn; }): JSX.Element { const treeElementRef = React.createRef(); From 3f1f76511dec2463b2468953811caa17f79118b7 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Thu, 22 Aug 2024 19:48:41 +0300 Subject: [PATCH 078/103] [lexical-table] Fix a number of table Cut command scenarios (#6528) Co-authored-by: Ivaylo Pavlov --- .../src/LexicalTableSelectionHelpers.ts | 47 ++- .../__tests__/unit/LexicalTableNode.test.ts | 104 ------ .../__tests__/unit/LexicalTableNode.test.tsx | 301 ++++++++++++++++++ .../lexical/src/__tests__/utils/index.tsx | 10 + 4 files changed, 354 insertions(+), 108 deletions(-) delete mode 100644 packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.ts create mode 100644 packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 604a6bf787d..0edd2747b30 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -24,7 +24,8 @@ import type { TextFormatType, } from 'lexical'; -import {$findMatchingParent} from '@lexical/utils'; +import {copyToClipboard} from '@lexical/clipboard'; +import {$findMatchingParent, objectKlassEquals} from '@lexical/utils'; import { $createParagraphNode, $createRangeSelectionFromDom, @@ -34,6 +35,7 @@ import { $getSelection, $isDecoratorNode, $isElementNode, + $isNodeSelection, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, @@ -41,6 +43,7 @@ import { COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, CONTROLLED_TEXT_INSERTION_COMMAND, + CUT_COMMAND, DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, @@ -314,7 +317,9 @@ export function applyTableHandlers( }, ); - const $deleteCellHandler = (event: KeyboardEvent): boolean => { + const $deleteCellHandler = ( + event: KeyboardEvent | ClipboardEvent | null, + ): boolean => { const selection = $getSelection(); if (!$isSelectionInTable(selection, tableNode)) { @@ -336,8 +341,10 @@ export function applyTableHandlers( } if ($isTableSelection(selection)) { - event.preventDefault(); - event.stopPropagation(); + if (event) { + event.preventDefault(); + event.stopPropagation(); + } tableObserver.clearText(); return true; @@ -371,6 +378,38 @@ export function applyTableHandlers( ), ); + tableObserver.listenersToRemove.add( + editor.registerCommand( + CUT_COMMAND, + (event) => { + const selection = $getSelection(); + if (selection) { + if ($isNodeSelection(selection)) { + return false; + } + + copyToClipboard( + editor, + objectKlassEquals(event, ClipboardEvent) + ? (event as ClipboardEvent) + : null, + ); + + if ($isTableSelection(selection)) { + $deleteCellHandler(event); + return true; + } else if ($isRangeSelection(selection)) { + $deleteCellHandler(event); + selection.removeText(); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_CRITICAL, + ), + ); + tableObserver.listenersToRemove.add( editor.registerCommand( FORMAT_TEXT_COMMAND, diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.ts b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.ts deleted file mode 100644 index 4eb836f0d22..00000000000 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.ts +++ /dev/null @@ -1,104 +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 {$insertDataTransferForRichText} from '@lexical/clipboard'; -import {$createTableNode} from '@lexical/table'; -import { - $createParagraphNode, - $getRoot, - $getSelection, - $isRangeSelection, -} from 'lexical'; -import { - DataTransferMock, - initializeUnitTest, - invariant, -} from 'lexical/src/__tests__/utils'; - -const editorConfig = Object.freeze({ - namespace: '', - theme: { - table: 'test-table-class', - }, -}); - -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 = '


                    '; - expect(testEnv.innerHTML).toBe( - `${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); - }); - expect(testEnv.innerHTML).toBe( - `

                    Surface

                    MWP_WORK_LS_COMPOSER

                    77349

                    Lexical

                    XDS_RICH_TEXT_AREA

                    sdvd sdfvsfs

                    `, - ); - }); - }); -}); diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx new file mode 100644 index 00000000000..a560884f8d6 --- /dev/null +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -0,0 +1,301 @@ +/** + * 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 {$insertDataTransferForRichText} from '@lexical/clipboard'; +import {TablePlugin} from '@lexical/react/LexicalTablePlugin'; +import { + $createTableNode, + $createTableNodeWithDimensions, + $createTableSelection, +} from '@lexical/table'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $getSelection, + $isRangeSelection, + $selectAll, + $setSelection, + CUT_COMMAND, + ParagraphNode, +} from 'lexical'; +import { + DataTransferMock, + initializeUnitTest, + invariant, +} from 'lexical/src/__tests__/utils'; + +import {$getElementForTableNode, TableNode} from '../../LexicalTableNode'; + +export class ClipboardDataMock { + getData: jest.Mock; + setData: jest.Mock; + + constructor() { + this.getData = jest.fn(); + this.setData = jest.fn(); + } +} + +export class ClipboardEventMock extends Event { + clipboardData: ClipboardDataMock; + + constructor(type: string, options?: EventInit) { + super(type, options); + this.clipboardData = new ClipboardDataMock(); + } +} + +global.document.execCommand = function execCommandMock( + commandId: string, + showUI?: boolean, + value?: string, +): boolean { + return true; +}; +Object.defineProperty(window, 'ClipboardEvent', { + value: new ClipboardEventMock('cut'), +}); + +const editorConfig = Object.freeze({ + namespace: '', + theme: { + table: 'test-table-class', + }, +}); + +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 = '


                    '; + expect(testEnv.innerHTML).toBe( + `${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); + }); + expect(testEnv.innerHTML).toBe( + `

                    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); + }); + + 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 || '', + ); + $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')); + 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); + } + } + }); + + expect(testEnv.innerHTML).toBe( + `


















                    `, + ); + }); + }, + undefined, + , + ); +}); diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index 739fc760b54..fac3879975b 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -559,6 +559,16 @@ export function invariant(cond?: boolean, message?: string): asserts cond { throw new Error(`Invariant: ${message}`); } +export class ClipboardDataMock { + getData: jest.Mock; + setData: jest.Mock; + + constructor() { + this.getData = jest.fn(); + this.setData = jest.fn(); + } +} + export class DataTransferMock implements DataTransfer { _data: Map = new Map(); get dropEffect(): DataTransfer['dropEffect'] { From fb82331db801b7ddc35d0e7857530d77307a1d1f Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Thu, 22 Aug 2024 20:01:39 +0300 Subject: [PATCH 079/103] [lexical-table] Stop selecting the whole table after pasting cells (#6539) --- .../src/LexicalTableSelectionHelpers.ts | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 0edd2747b30..53ee28115b8 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -69,10 +69,7 @@ import {$isTableCellNode} from './LexicalTableCellNode'; import {$isTableNode} from './LexicalTableNode'; import {TableDOMTable, TableObserver} from './LexicalTableObserver'; import {$isTableRowNode} from './LexicalTableRowNode'; -import { - $createTableSelection, - $isTableSelection, -} from './LexicalTableSelection'; +import {$isTableSelection} from './LexicalTableSelection'; import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils'; const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection'; @@ -672,8 +669,6 @@ export function applyTableHandlers( const toY = Math.max(startY, stopY); const gridRowNodes = gridNode.getChildren(); let newRowIdx = 0; - let newAnchorCellKey; - let newFocusCellKey; for (let r = fromY; r <= toY; r++) { const currentGridRowNode = gridRowNodes[r]; @@ -705,12 +700,6 @@ export function applyTableHandlers( return false; } - if (r === fromY && c === fromX) { - newAnchorCellKey = currentGridCellNode.getKey(); - } else if (r === toY && c === toX) { - newFocusCellKey = currentGridCellNode.getKey(); - } - const originalChildren = currentGridCellNode.getChildren(); newGridCellNode.getChildren().forEach((child) => { if ($isTextNode(child)) { @@ -727,15 +716,6 @@ export function applyTableHandlers( newRowIdx++; } - if (newAnchorCellKey && newFocusCellKey) { - const newTableSelection = $createTableSelection(); - newTableSelection.set( - nodes[0].getKey(), - newAnchorCellKey, - newFocusCellKey, - ); - $setSelection(newTableSelection); - } return true; }, COMMAND_PRIORITY_CRITICAL, From 25f543ea9d86bb5af782f7da6c8ec9984983d764 Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 22 Aug 2024 12:16:06 -0500 Subject: [PATCH 080/103] [lexical-yjs] Bug Fix: Properly sync when emptying document via undo (#6523) Co-authored-by: Ivaylo Pavlov Co-authored-by: Bob Ippolito --- .../src/__tests__/unit/Collaboration.test.ts | 99 ++++++++++++++++++- .../src/__tests__/unit/utils.tsx | 19 +++- packages/lexical-yjs/src/SyncEditorStates.ts | 13 ++- 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/packages/lexical-react/src/__tests__/unit/Collaboration.test.ts b/packages/lexical-react/src/__tests__/unit/Collaboration.test.ts index 98402f8a93e..d8781f3d6d6 100644 --- a/packages/lexical-react/src/__tests__/unit/Collaboration.test.ts +++ b/packages/lexical-react/src/__tests__/unit/Collaboration.test.ts @@ -6,7 +6,14 @@ * */ -import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + ParagraphNode, + TextNode, + UNDO_COMMAND, +} from 'lexical'; import {Client, createTestConnection, waitForReact} from './utils'; @@ -312,4 +319,94 @@ describe('Collaboration', () => { client1.stop(); client2.stop(); }); + + /** + * When a document is not bootstrapped (via `shouldBootstrap`), the document only initializes the initial paragraph + * node upon the first user interaction. Then, both a new paragraph as well as the user character are inserted as a + * single Yjs change. However, when the user undos this initial change, the document now has no initial paragraph + * node. syncYjsChangesToLexical addresses this by doing a check: `$getRoot().getChildrenSize() === 0)` and if true, + * inserts the paragraph node. However, this insertion was previously being done in an editor.update block that had + * either the tag 'collaboration' or 'historic'. Then, when `syncLexicalUpdateToYjs` was called, because one of these + * tags were present, the function would early-return, and this change would not be synced to other clients, causing + * permanent desync and corruption of the doc for both users. Not only was the change not syncing to other clients, + * but even the initiating client was not notified via the proper callbacks, and the change would fall through from + * persistence, causing permanent desync. The fix was to move the insertion of the paragraph node outside of the + * editor.update block that included the 'collaboration' or 'historic' tag, and instead insert it in a separate + * editor.update block that did not have these tags. + */ + it('Should sync to other clients when inserting a new paragraph node when document is emptied via undo', async () => { + const connector = createTestConnection(); + + const client1 = connector.createClient('1'); + const client2 = connector.createClient('2'); + + client1.start(container!, undefined, {shouldBootstrapEditor: false}); + client2.start(container!, undefined, {shouldBootstrapEditor: false}); + + expect(client1.getHTML()).toEqual(''); + expect(client1.getHTML()).toEqual(client2.getHTML()); + + // Wait for clients to render the initial content + await Promise.resolve().then(); + + expect(client1.getHTML()).toEqual(''); + expect(client1.getHTML()).toEqual(client2.getHTML()); + + await waitForReact(() => { + client1.update(() => { + const root = $getRoot(); + + // Since bootstrap is false, we create our own paragraph node + const paragraph = $createParagraphNode(); + const text = $createTextNode('Hello'); + paragraph.append(text); + + root.append(paragraph); + }); + }); + + expect(client1.getHTML()).toEqual( + '

                    Hello

                    ', + ); + expect(client1.getHTML()).toEqual(client2.getHTML()); + expect(client1.getDocJSON()).toEqual({ + root: '[object Object]Hello', + }); + expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); + + await waitForReact(() => { + // Undo the insertion of the initial paragraph and text node + client1.getEditor().dispatchCommand(UNDO_COMMAND, undefined); + }); + + // We expect the safety check in syncYjsChangesToLexical to + // insert a new paragraph node and prevent the document from being empty + expect(client1.getHTML()).toEqual('


                    '); + expect(client1.getHTML()).toEqual(client2.getHTML()); + expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); + + await waitForReact(() => { + client1.update(() => { + const root = $getRoot(); + + const paragraph = $createParagraphNode(); + const text = $createTextNode('Hello world'); + paragraph.append(text); + + root.append(paragraph); + }); + }); + + expect(client1.getHTML()).toEqual( + '


                    Hello world

                    ', + ); + expect(client1.getHTML()).toEqual(client2.getHTML()); + expect(client1.getDocJSON()).toEqual({ + root: '[object Object]Hello world', + }); + expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); + + client1.stop(); + client2.stop(); + }); }); diff --git a/packages/lexical-react/src/__tests__/unit/utils.tsx b/packages/lexical-react/src/__tests__/unit/utils.tsx index 3f1433f3534..36b591e1988 100644 --- a/packages/lexical-react/src/__tests__/unit/utils.tsx +++ b/packages/lexical-react/src/__tests__/unit/utils.tsx @@ -27,11 +27,13 @@ function Editor({ provider, setEditor, awarenessData, + shouldBootstrapEditor = true, }: { doc: Y.Doc; provider: Provider; setEditor: (editor: LexicalEditor) => void; awarenessData?: object | undefined; + shouldBootstrapEditor?: boolean; }) { const context = useCollaborationContext(); @@ -48,7 +50,7 @@ function Editor({ provider} - shouldBootstrap={true} + shouldBootstrap={shouldBootstrapEditor} awarenessData={awarenessData} /> { - throw Error(); + onError: (e) => { + throw e; }, }}> (this._editor = editor)} awarenessData={awarenessData} + shouldBootstrapEditor={options.shouldBootstrapEditor} /> , ); diff --git a/packages/lexical-yjs/src/SyncEditorStates.ts b/packages/lexical-yjs/src/SyncEditorStates.ts index ab996bbec43..beca7904176 100644 --- a/packages/lexical-yjs/src/SyncEditorStates.ts +++ b/packages/lexical-yjs/src/SyncEditorStates.ts @@ -100,11 +100,6 @@ export function syncYjsChangesToLexical( const event = events[i]; $syncEvent(binding, event); } - // If there was a collision on the top level paragraph - // we need to re-add a paragraph - if ($getRoot().getChildrenSize() === 0) { - $getRoot().append($createParagraphNode()); - } const selection = $getSelection(); @@ -135,6 +130,14 @@ export function syncYjsChangesToLexical( { onUpdate: () => { syncCursorPositions(binding, provider); + // If there was a collision on the top level paragraph + // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients, + // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'. + editor.update(() => { + if ($getRoot().getChildrenSize() === 0) { + $getRoot().append($createParagraphNode()); + } + }); }, skipTransforms: true, tag: isFromUndoManger ? 'historic' : 'collaboration', From afc7386fbcf9c2c0c699020c60dcfc1747459d44 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Thu, 22 Aug 2024 20:46:53 +0300 Subject: [PATCH 081/103] [lexical-react] Fix multiple node selection deletion (#6538) Co-authored-by: Bob Ippolito --- .../ExcalidrawNode/ExcalidrawComponent.tsx | 23 ++++++++++--------- .../src/nodes/ImageComponent.tsx | 17 ++++++++------ .../InlineImageNode/InlineImageComponent.tsx | 17 +++++++++----- .../src/nodes/PageBreakNode/index.tsx | 18 ++++++++------- .../src/nodes/PollComponent.tsx | 17 ++++++++------ .../src/LexicalBlockWithAlignableContents.tsx | 18 ++++++++------- .../src/LexicalHorizontalRuleNode.tsx | 18 ++++++++------- 7 files changed, 73 insertions(+), 55 deletions(-) diff --git a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx index 91c0992bdf2..903fe0f1e71 100644 --- a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +++ b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx @@ -48,21 +48,22 @@ export default function ExcalidrawComponent({ useLexicalNodeSelection(nodeKey); const [isResizing, setIsResizing] = useState(false); - const onDelete = useCallback( + const $onDelete = useCallback( (event: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { event.preventDefault(); editor.update(() => { - const node = $getNodeByKey(nodeKey); - if ($isExcalidrawNode(node)) { - node.remove(); - return true; - } + deleteSelection.getNodes().forEach((node) => { + if ($isExcalidrawNode(node)) { + node.remove(); + } + }); }); } return false; }, - [editor, isSelected, nodeKey], + [editor, isSelected], ); // Set editor to readOnly if excalidraw is open to prevent unwanted changes @@ -103,16 +104,16 @@ export default function ExcalidrawComponent({ ), editor.registerCommand( KEY_DELETE_COMMAND, - onDelete, + $onDelete, COMMAND_PRIORITY_LOW, ), editor.registerCommand( KEY_BACKSPACE_COMMAND, - onDelete, + $onDelete, COMMAND_PRIORITY_LOW, ), ); - }, [clearSelection, editor, isSelected, isResizing, onDelete, setSelected]); + }, [clearSelection, editor, isSelected, isResizing, $onDelete, setSelected]); const deleteNode = useCallback(() => { setModalOpen(false); diff --git a/packages/lexical-playground/src/nodes/ImageComponent.tsx b/packages/lexical-playground/src/nodes/ImageComponent.tsx index d53eb30db33..855c316e6e3 100644 --- a/packages/lexical-playground/src/nodes/ImageComponent.tsx +++ b/packages/lexical-playground/src/nodes/ImageComponent.tsx @@ -174,18 +174,21 @@ export default function ImageComponent({ const $onDelete = useCallback( (payload: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { const event: KeyboardEvent = payload; event.preventDefault(); - const node = $getNodeByKey(nodeKey); - if ($isImageNode(node)) { - node.remove(); - return true; - } + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isImageNode(node)) { + node.remove(); + } + }); + }); } return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); const $onEnter = useCallback( diff --git a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx index ad2861ba3d0..5cb86ca2426 100644 --- a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx +++ b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx @@ -203,18 +203,23 @@ export default function InlineImageComponent({ const $onDelete = useCallback( (payload: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { const event: KeyboardEvent = payload; event.preventDefault(); - const node = $getNodeByKey(nodeKey); - if ($isInlineImageNode(node)) { - node.remove(); - return true; + if (isSelected && $isNodeSelection(deleteSelection)) { + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isInlineImageNode(node)) { + node.remove(); + } + }); + }); } } return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); const $onEnter = useCallback( diff --git a/packages/lexical-playground/src/nodes/PageBreakNode/index.tsx b/packages/lexical-playground/src/nodes/PageBreakNode/index.tsx index 44f0f84d107..8e6a1f551a1 100644 --- a/packages/lexical-playground/src/nodes/PageBreakNode/index.tsx +++ b/packages/lexical-playground/src/nodes/PageBreakNode/index.tsx @@ -11,7 +11,6 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection'; import {mergeRegister} from '@lexical/utils'; import { - $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, @@ -39,16 +38,19 @@ function PageBreakComponent({nodeKey}: {nodeKey: NodeKey}) { const $onDelete = useCallback( (event: KeyboardEvent) => { event.preventDefault(); - if (isSelected && $isNodeSelection($getSelection())) { - const node = $getNodeByKey(nodeKey); - if ($isPageBreakNode(node)) { - node.remove(); - return true; - } + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isPageBreakNode(node)) { + node.remove(); + } + }); + }); } return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); useEffect(() => { diff --git a/packages/lexical-playground/src/nodes/PollComponent.tsx b/packages/lexical-playground/src/nodes/PollComponent.tsx index 45d38ef9d28..687a924bc8b 100644 --- a/packages/lexical-playground/src/nodes/PollComponent.tsx +++ b/packages/lexical-playground/src/nodes/PollComponent.tsx @@ -146,18 +146,21 @@ export default function PollComponent({ const $onDelete = useCallback( (payload: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { const event: KeyboardEvent = payload; event.preventDefault(); - const node = $getNodeByKey(nodeKey); - if ($isPollNode(node)) { - node.remove(); - return true; - } + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isPollNode(node)) { + node.remove(); + } + }); + }); } return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); useEffect(() => { diff --git a/packages/lexical-react/src/LexicalBlockWithAlignableContents.tsx b/packages/lexical-react/src/LexicalBlockWithAlignableContents.tsx index 35ee3d2d7a0..0a48094ce42 100644 --- a/packages/lexical-react/src/LexicalBlockWithAlignableContents.tsx +++ b/packages/lexical-react/src/LexicalBlockWithAlignableContents.tsx @@ -54,18 +54,20 @@ export function BlockWithAlignableContents({ const $onDelete = useCallback( (event: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { event.preventDefault(); - const node = $getNodeByKey(nodeKey); - if ($isDecoratorNode(node)) { - node.remove(); - return true; - } + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isDecoratorNode(node)) { + node.remove(); + } + }); + }); } - return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); useEffect(() => { diff --git a/packages/lexical-react/src/LexicalHorizontalRuleNode.tsx b/packages/lexical-react/src/LexicalHorizontalRuleNode.tsx index b13b4ba02be..19aae08faed 100644 --- a/packages/lexical-react/src/LexicalHorizontalRuleNode.tsx +++ b/packages/lexical-react/src/LexicalHorizontalRuleNode.tsx @@ -26,7 +26,6 @@ import { } from '@lexical/utils'; import { $applyNodeReplacement, - $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, @@ -51,17 +50,20 @@ function HorizontalRuleComponent({nodeKey}: {nodeKey: NodeKey}) { const $onDelete = useCallback( (event: KeyboardEvent) => { - if (isSelected && $isNodeSelection($getSelection())) { + const deleteSelection = $getSelection(); + if (isSelected && $isNodeSelection(deleteSelection)) { event.preventDefault(); - const node = $getNodeByKey(nodeKey); - if ($isHorizontalRuleNode(node)) { - node.remove(); - return true; - } + editor.update(() => { + deleteSelection.getNodes().forEach((node) => { + if ($isHorizontalRuleNode(node)) { + node.remove(); + } + }); + }); } return false; }, - [isSelected, nodeKey], + [editor, isSelected], ); useEffect(() => { From fd7a93d00359f391398adeef7aea6f783830cc8a Mon Sep 17 00:00:00 2001 From: keiseiTi Date: Fri, 23 Aug 2024 02:02:26 +0800 Subject: [PATCH 082/103] [lexical-playground] Fix: in playground show component-menu when scroll (#6510) Co-authored-by: Ivaylo Pavlov --- packages/lexical-react/src/shared/LexicalMenu.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.ts index 590e5cfdc57..62b0dc8e3a0 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.ts @@ -494,9 +494,7 @@ export function useMenuAnchorRef( if (rootElement !== null && resolution !== null) { const {left, top, width, height} = resolution.getRect(); const anchorHeight = anchorElementRef.current.offsetHeight; // use to position under anchor - containerDiv.style.top = `${ - top + window.pageYOffset + anchorHeight + 3 - }px`; + containerDiv.style.top = `${top + anchorHeight + 3}px`; containerDiv.style.left = `${left + window.pageXOffset}px`; containerDiv.style.height = `${height}px`; containerDiv.style.width = `${width}px`; @@ -518,9 +516,7 @@ export function useMenuAnchorRef( top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight + height ) { - containerDiv.style.top = `${ - top - menuHeight + window.pageYOffset - height - }px`; + containerDiv.style.top = `${top - menuHeight - height}px`; } } From 4b646a162d73b73d0d93d14e7171abc137963330 Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 22 Aug 2024 16:36:01 -0500 Subject: [PATCH 083/103] Bug Fix: Fix issue where triple-clicking a cell would dangerously select entire document (#6542) --- .../__tests__/e2e/Selection.spec.mjs | 32 +++++++++++++++++++ .../src/LexicalTableSelectionHelpers.ts | 8 ----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 651e8707cdb..502feb160ba 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -648,6 +648,38 @@ test.describe.parallel('Selection', () => { ); }); + test('Triple-clicking last cell in table should not select entire document', async ({ + page, + isPlainText, + isCollab, + browserName, + legacyEvents, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + await page.keyboard.type('Line1'); + await insertTable(page, 1, 2); + + const lastCell = page.locator( + '.PlaygroundEditorTheme__tableCell:last-child', + ); + await lastCell.click(); + await page.keyboard.type('Foo'); + + const lastCellText = lastCell.locator('span'); + const tripleClickDelay = 50; + await lastCellText.click({clickCount: 3, delay: tripleClickDelay}); + + // Only the last cell should be selected, and not the entire docuemnt + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [1, 0, 1, 0], + focusOffset: 1, + focusPath: [1, 0, 1, 0], + }); + }); + test('Can persist the text format from the paragraph', async ({ page, isPlainText, diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 53ee28115b8..ad0746226ce 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -764,14 +764,6 @@ export function applyTableHandlers( : lastCell.getChildrenSize(), 'element', ); - } else { - newSelection.anchor.set( - tableNode.getParentOrThrow().getKey(), - isBackward - ? tableNode.getIndexWithinParent() + 1 - : tableNode.getIndexWithinParent(), - 'element', - ); } $setSelection(newSelection); $addHighlightStyleToTable(editor, tableObserver); From 365f91f3ad8d351ef62ef957b41999ee055582d3 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 23 Aug 2024 22:56:46 -0700 Subject: [PATCH 084/103] [lexical-playground] Bug Fix: Fix firefox e2e test regression in Selection.spec.mjs (#6546) --- .github/workflows/call-e2e-test.yml | 3 +- .../__tests__/e2e/Selection.spec.mjs | 75 +++++++++++-------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/.github/workflows/call-e2e-test.yml b/.github/workflows/call-e2e-test.yml index fe7b194c973..0d559ba38b5 100644 --- a/.github/workflows/call-e2e-test.yml +++ b/.github/workflows/call-e2e-test.yml @@ -3,6 +3,7 @@ name: Lexical e2e test runner on: workflow_call: inputs: + # Make sure that all of these are present in the name of the actions/upload-artifact@v4 action below os: {required: true, type: string} node-version: {required: true, type: string} browser: {required: true, type: string} @@ -65,6 +66,6 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ inputs.override-react-version }} + name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ inputs.override-react-version }}-${{ inputs.flaky && 'flaky' || ''}} path: ${{ env.test_results_path }} retention-days: 7 diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 502feb160ba..c1999142254 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -655,7 +655,7 @@ test.describe.parallel('Selection', () => { browserName, legacyEvents, }) => { - test.skip(isPlainText); + test.skip(isPlainText || isCollab); await focusEditor(page); await page.keyboard.type('Line1'); @@ -665,19 +665,32 @@ test.describe.parallel('Selection', () => { '.PlaygroundEditorTheme__tableCell:last-child', ); await lastCell.click(); - await page.keyboard.type('Foo'); + const cellText = 'Foo'; + await page.keyboard.type(cellText); 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 - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1, 0, 1, 0], - focusOffset: 1, - focusPath: [1, 0, 1, 0], - }); + 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], + }); + } else { + // Other browsers select the p + await assertSelection(page, { + anchorOffset: 0, + anchorPath, + focusOffset: 1, + focusPath: anchorPath, + }); + } }); test('Can persist the text format from the paragraph', async ({ @@ -929,30 +942,28 @@ test.describe.parallel('Selection', () => { }, ); - test('shift+arrowup into a table, when the table is the first node, selects the whole table', async ({ - page, - isPlainText, - isCollab, - browserName, - legacyEvents, - }) => { - test.skip(isPlainText); - test.fixme(browserName === 'chromium' && legacyEvents); - await focusEditor(page); - await insertTable(page, 2, 2); - await moveToEditorBeginning(page); - await deleteBackward(page); - await moveToEditorEnd(page); - await page.keyboard.down('Shift'); - await page.keyboard.press('ArrowUp'); - await page.keyboard.up('Shift'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1], - focusOffset: 1, - focusPath: [0, 0, 0], - }); - }); + test( + 'shift+arrowup into a table, when the table is the first node, selects the whole table', + {tag: '@flaky'}, + async ({page, isPlainText, isCollab, browserName, legacyEvents}) => { + test.skip(isPlainText); + test.fixme(browserName === 'chromium' && legacyEvents); + await focusEditor(page); + await insertTable(page, 2, 2); + await moveToEditorBeginning(page); + await deleteBackward(page); + await moveToEditorEnd(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.up('Shift'); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [1], + focusOffset: 1, + focusPath: [0, 0, 0], + }); + }, + ); test( 'shift+arrowdown into a table, when the table is the only node, selects the whole table', From 149806b67cc959f424fdd4acf1b5cb93369baa07 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sat, 24 Aug 2024 12:55:51 -0700 Subject: [PATCH 085/103] [lexical-table][lexical-clipboard] Bug Fix: Race condition in table CUT_COMMAND (#6550) --- packages/lexical-clipboard/src/clipboard.ts | 127 +++++++++++++----- packages/lexical-clipboard/src/index.ts | 3 + .../src/LexicalTableSelectionHelpers.ts | 24 ++-- 3 files changed, 108 insertions(+), 46 deletions(-) diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index 92f7d0a9c07..b8511cbd619 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -12,6 +12,7 @@ import {objectKlassEquals} from '@lexical/utils'; import { $cloneWithProperties, $createTabNode, + $getEditor, $getRoot, $getSelection, $isElementNode, @@ -34,6 +35,12 @@ 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; + 'text/plain': string; +} + /** * Returns the *currently selected* Lexical content as an HTML string, relying on the * logic defined in the exportDOM methods on the LexicalNode classes. Note that @@ -41,11 +48,13 @@ const getDOMSelection = (targetWindow: Window | null): Selection | null => * in the current selection). * * @param editor - LexicalEditor instance to get HTML content from + * @param selection - The selection to use (default is $getSelection()) * @returns a string of HTML content */ -export function $getHtmlContent(editor: LexicalEditor): string { - const selection = $getSelection(); - +export function $getHtmlContent( + editor: LexicalEditor, + selection = $getSelection(), +): string { if (selection == null) { invariant(false, 'Expected valid LexicalSelection'); } @@ -68,11 +77,13 @@ export function $getHtmlContent(editor: LexicalEditor): string { * in the current selection). * * @param editor - LexicalEditor instance to get the JSON content from + * @param selection - The selection to use (default is $getSelection()) * @returns */ -export function $getLexicalContent(editor: LexicalEditor): null | string { - const selection = $getSelection(); - +export function $getLexicalContent( + editor: LexicalEditor, + selection = $getSelection(), +): null | string { if (selection == null) { invariant(false, 'Expected valid LexicalSelection'); } @@ -383,6 +394,7 @@ let clipboardEventTimeout: null | number = null; export async function copyToClipboard( editor: LexicalEditor, event: null | ClipboardEvent, + data?: LexicalClipboardData, ): Promise { if (clipboardEventTimeout !== null) { // Prevent weird race conditions that can happen when this function is run multiple times @@ -392,7 +404,7 @@ export async function copyToClipboard( if (event !== null) { return new Promise((resolve, reject) => { editor.update(() => { - resolve($copyToClipboardEvent(editor, event)); + resolve($copyToClipboardEvent(editor, event, data)); }); }); } @@ -423,7 +435,9 @@ export async function copyToClipboard( window.clearTimeout(clipboardEventTimeout); clipboardEventTimeout = null; } - resolve($copyToClipboardEvent(editor, secondEvent as ClipboardEvent)); + resolve( + $copyToClipboardEvent(editor, secondEvent as ClipboardEvent, data), + ); } // Block the entire copy flow while we wait for the next ClipboardEvent return true; @@ -446,38 +460,83 @@ export async function copyToClipboard( function $copyToClipboardEvent( editor: LexicalEditor, event: ClipboardEvent, + data?: LexicalClipboardData, ): boolean { - const domSelection = getDOMSelection(editor._window); - if (!domSelection) { - return false; - } - const anchorDOM = domSelection.anchorNode; - const focusDOM = domSelection.focusNode; - if ( - anchorDOM !== null && - focusDOM !== null && - !isSelectionWithinEditor(editor, anchorDOM, focusDOM) - ) { - return false; + if (data === undefined) { + const domSelection = getDOMSelection(editor._window); + if (!domSelection) { + return false; + } + const anchorDOM = domSelection.anchorNode; + const focusDOM = domSelection.focusNode; + if ( + anchorDOM !== null && + focusDOM !== null && + !isSelectionWithinEditor(editor, anchorDOM, focusDOM) + ) { + return false; + } + const selection = $getSelection(); + if (selection === null) { + return false; + } + data = $getClipboardDataFromSelection(selection); } event.preventDefault(); const clipboardData = event.clipboardData; - const selection = $getSelection(); - if (clipboardData === null || selection === null) { + if (clipboardData === null) { return false; } - const htmlString = $getHtmlContent(editor); - const lexicalString = $getLexicalContent(editor); - let plainString = ''; - if (selection !== null) { - plainString = selection.getTextContent(); - } - if (htmlString !== null) { - clipboardData.setData('text/html', htmlString); + setLexicalClipboardDataTransfer(clipboardData, data); + return true; +} + +const clipboardDataFunctions = [ + ['text/html', $getHtmlContent], + ['application/x-lexical-editor', $getLexicalContent], +] as const; + +/** + * Serialize the content of the current selection to strings in + * text/plain, text/html, and application/x-lexical-editor (Lexical JSON) + * formats (as available). + * + * @param selection the selection to serialize (defaults to $getSelection()) + * @returns LexicalClipboardData + */ +export function $getClipboardDataFromSelection( + selection: BaseSelection | null = $getSelection(), +): LexicalClipboardData { + const clipboardData: LexicalClipboardData = { + 'text/plain': selection ? selection.getTextContent() : '', + }; + if (selection) { + const editor = $getEditor(); + for (const [mimeType, $editorFn] of clipboardDataFunctions) { + const v = $editorFn(editor, selection); + if (v !== null) { + clipboardData[mimeType] = v; + } + } } - if (lexicalString !== null) { - clipboardData.setData('application/x-lexical-editor', lexicalString); + return clipboardData; +} + +/** + * Call setData on the given clipboardData for each MIME type present + * in the given data (from {@link $getClipboardDataFromSelection}) + * + * @param clipboardData the event.clipboardData to populate from data + * @param data The lexical data + */ +export function setLexicalClipboardDataTransfer( + clipboardData: DataTransfer, + data: LexicalClipboardData, +) { + for (const k in data) { + const v = data[k as keyof LexicalClipboardData]; + if (v !== undefined) { + clipboardData.setData(k, v); + } } - clipboardData.setData('text/plain', plainString); - return true; } diff --git a/packages/lexical-clipboard/src/index.ts b/packages/lexical-clipboard/src/index.ts index 538ff4c56de..ffa1f19f6ac 100644 --- a/packages/lexical-clipboard/src/index.ts +++ b/packages/lexical-clipboard/src/index.ts @@ -9,10 +9,13 @@ export { $generateJSONFromSelectedNodes, $generateNodesFromSerializedNodes, + $getClipboardDataFromSelection, $getHtmlContent, $getLexicalContent, $insertDataTransferForPlainText, $insertDataTransferForRichText, $insertGeneratedNodes, copyToClipboard, + type LexicalClipboardData, + setLexicalClipboardDataTransfer, } from './clipboard'; diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index ad0746226ce..e4672f3b235 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -24,7 +24,10 @@ import type { TextFormatType, } from 'lexical'; -import {copyToClipboard} from '@lexical/clipboard'; +import { + $getClipboardDataFromSelection, + copyToClipboard, +} from '@lexical/clipboard'; import {$findMatchingParent, objectKlassEquals} from '@lexical/utils'; import { $createParagraphNode, @@ -35,7 +38,6 @@ import { $getSelection, $isDecoratorNode, $isElementNode, - $isNodeSelection, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, @@ -381,25 +383,23 @@ export function applyTableHandlers( (event) => { const selection = $getSelection(); if (selection) { - if ($isNodeSelection(selection)) { + if (!($isTableSelection(selection) || $isRangeSelection(selection))) { return false; } - - copyToClipboard( + // Copying to the clipboard is async so we must capture the data + // before we delete it + void copyToClipboard( editor, objectKlassEquals(event, ClipboardEvent) ? (event as ClipboardEvent) : null, + $getClipboardDataFromSelection(selection), ); - - if ($isTableSelection(selection)) { - $deleteCellHandler(event); - return true; - } else if ($isRangeSelection(selection)) { - $deleteCellHandler(event); + const intercepted = $deleteCellHandler(event); + if ($isRangeSelection(selection)) { selection.removeText(); - return true; } + return intercepted; } return false; }, From f06e1460e0424963df7cacb65fb5724450216d17 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 25 Aug 2024 16:06:08 +0300 Subject: [PATCH 086/103] [lexical-table] Fix table selection paste as plain text (#6548) --- .../src/LexicalTableSelection.ts | 7 ++- .../__tests__/unit/LexicalTableNode.test.tsx | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/lexical-table/src/LexicalTableSelection.ts b/packages/lexical-table/src/LexicalTableSelection.ts index ff1377eb63a..f4e94bdc87f 100644 --- a/packages/lexical-table/src/LexicalTableSelection.ts +++ b/packages/lexical-table/src/LexicalTableSelection.ts @@ -331,10 +331,13 @@ export class TableSelection implements BaseSelection { } getTextContent(): string { - const nodes = this.getNodes(); + const nodes = this.getNodes().filter((node) => $isTableCellNode(node)); let textContent = ''; for (let i = 0; i < nodes.length; i++) { - textContent += nodes[i].getTextContent(); + const node = nodes[i]; + const row = node.__parent; + const nextRow = (nodes[i + 1] || {}).__parent; + textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t'); } return textContent; } diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index a560884f8d6..b11b99490b6 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -294,6 +294,56 @@ describe('LexicalTableNode tests', () => { `


















                    `, ); }); + + 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`); + } + } + }); + }); }, undefined, , From c191687f6ef3ed9e90212b57e55d1c2f96ed54c9 Mon Sep 17 00:00:00 2001 From: Minseo Kang <65164815+kmslab20@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:04:38 +0900 Subject: [PATCH 087/103] [lexical-table] Bug Fix: Append a ParagraphNode to each cell when unmerging (#6556) --- .../__tests__/e2e/Tables.spec.mjs | 182 +++++++++++++++++- .../lexical-table/src/LexicalTableUtils.ts | 14 +- 2 files changed, 189 insertions(+), 7 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 694f9c86e4b..d639bc9e5b3 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -15,6 +15,7 @@ import { moveUp, pressBackspace, selectAll, + selectCharacters, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -1888,7 +1889,9 @@ test.describe.parallel('Tables', () => { second

                    -
                    + +


                    +


                    @@ -2007,15 +2010,21 @@ test.describe.parallel('Tables', () => {


                    -
                    + +


                    +


                    -
                    -
                    + +


                    + + +


                    +


                    @@ -3112,4 +3121,169 @@ test.describe.parallel('Tables', () => { ); }, ); + + test('Paste and insert new lines after unmerging cells', 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 insertTable(page, 3, 3); + + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 2, y: 2}, + false, + false, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


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


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    + `, + ); + + await unmergeTableCell(page); + + await focusEditor(page); + + // move caret to the end of the editor + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + + await page.keyboard.type('Hello'); + await selectCharacters(page, 'left', 'Hello'.length); + + const clipboard = await copyToClipboard(page); + + // move caret to the first position of the editor + await click(page, '.PlaygroundEditorTheme__paragraph'); + + // move caret to the table cell (2,2) + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + + await pasteFromClipboard(page, clipboard); + await pasteFromClipboard(page, clipboard); + await pasteFromClipboard(page, clipboard); + + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + + await pasteFromClipboard(page, clipboard); + + await assertHTML( + page, + html` +


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


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +


                    +
                    +

                    + HelloHelloHello +

                    +


                    +


                    +

                    + Hello +

                    +
                    +

                    + Hello +

                    + `, + ); + }); }); diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index 0633384ad1f..3293881e823 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -675,7 +675,11 @@ export function $unmergeCell(): void { const rowSpan = cell.__rowSpan; if (colSpan > 1) { for (let i = 1; i < colSpan; i++) { - cell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS)); + cell.insertAfter( + $createTableCellNode(TableCellHeaderStates.NO_STATUS).append( + $createParagraphNode(), + ), + ); } cell.setColSpan(1); } @@ -706,13 +710,17 @@ export function $unmergeCell(): void { for (let j = 0; j < colSpan; j++) { $insertFirst( currentRowNode, - $createTableCellNode(TableCellHeaderStates.NO_STATUS), + $createTableCellNode(TableCellHeaderStates.NO_STATUS).append( + $createParagraphNode(), + ), ); } } else { for (let j = 0; j < colSpan; j++) { insertAfterCell.insertAfter( - $createTableCellNode(TableCellHeaderStates.NO_STATUS), + $createTableCellNode(TableCellHeaderStates.NO_STATUS).append( + $createParagraphNode(), + ), ); } } From 5d56371e5fd55fbca463324727f53d0c4c24988d Mon Sep 17 00:00:00 2001 From: wnhlee <40269597+2wheeh@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:10:25 +0900 Subject: [PATCH 088/103] [lexical] Add tests for HTMLConfig (#5507) --- packages/lexical/src/LexicalEditor.ts | 6 +- packages/lexical/src/LexicalNode.ts | 5 + .../src/__tests__/unit/LexicalEditor.test.tsx | 92 +++++++++++++++++++ .../lexical/src/__tests__/utils/index.tsx | 7 +- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 44ae24f4b6b..e86e7b215e4 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -11,6 +11,7 @@ import type { DOMConversion, DOMConversionMap, DOMExportOutput, + DOMExportOutputMap, NodeKey, } from './LexicalNode'; @@ -170,10 +171,7 @@ export type LexicalNodeReplacement = { }; export type HTMLConfig = { - export?: Map< - Klass, - (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput - >; + export?: DOMExportOutputMap; import?: DOMConversionMap; }; diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 9d24f72867b..5f70d3e36df 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -148,6 +148,11 @@ export type DOMConversionOutput = { node: null | LexicalNode | Array; }; +export type DOMExportOutputMap = Map< + Klass, + (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput +>; + export type DOMExportOutput = { after?: ( generatedElement: HTMLElement | Text | null | undefined, diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index cc81f334678..b7714f03532 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -6,6 +6,7 @@ * */ +import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {ContentEditable} from '@lexical/react/LexicalContentEditable'; import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary'; @@ -57,6 +58,7 @@ import { } from 'react'; import {createPortal} from 'react-dom'; import {createRoot, Root} from 'react-dom/client'; +import invariant from 'shared/invariant'; import * as ReactTestUtils from 'shared/react-test-utils'; import { @@ -2777,4 +2779,94 @@ describe('LexicalEditor tests', () => { newEditor1.setRootElement(null); newEditor2.setRootElement(null); }); + + describe('html config', () => { + it('should override export output function', async () => { + const onError = jest.fn(); + + const newEditor = createTestEditor({ + html: { + export: new Map([ + [ + TextNode, + (_, target) => { + invariant($isTextNode(target)); + + return { + element: target.hasFormat('bold') + ? document.createElement('bor') + : document.createElement('foo'), + }; + }, + ], + ]), + }, + onError: onError, + }); + + newEditor.setRootElement(container); + + newEditor.update(() => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const text = $createTextNode(); + root.append(paragraph); + paragraph.append(text); + + const selection = $createNodeSelection(); + selection.add(text.getKey()); + + const htmlFoo = $generateHtmlFromNodes(newEditor, selection); + expect(htmlFoo).toBe(''); + + text.toggleFormat('bold'); + + const htmlBold = $generateHtmlFromNodes(newEditor, selection); + expect(htmlBold).toBe(''); + }); + + expect(onError).not.toHaveBeenCalled(); + }); + + it('should override import conversion function', async () => { + const onError = jest.fn(); + + const newEditor = createTestEditor({ + html: { + import: { + figure: () => ({ + conversion: () => ({node: $createTextNode('yolo')}), + priority: 4, + }), + }, + }, + onError: onError, + }); + + newEditor.setRootElement(container); + + newEditor.update(() => { + const html = '
                    '; + + const parser = new DOMParser(); + const dom = parser.parseFromString(html, 'text/html'); + const node = $generateNodesFromDOM(newEditor, dom)[0]; + + expect(node).toEqual({ + __detail: 0, + __format: 0, + __key: node.getKey(), + __mode: 0, + __next: null, + __parent: null, + __prev: null, + __style: '', + __text: 'yolo', + __type: 'text', + }); + }); + + expect(onError).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index fac3879975b..5292fdd5a5f 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -46,7 +46,11 @@ import {createRef} from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'shared/react-test-utils'; -import {CreateEditorArgs, LexicalNodeReplacement} from '../../LexicalEditor'; +import { + CreateEditorArgs, + HTMLConfig, + LexicalNodeReplacement, +} from '../../LexicalEditor'; import {resetRandomKey} from '../../LexicalUtils'; const prettierConfig = prettier.resolveConfig.sync( @@ -520,6 +524,7 @@ export function createTestEditor( onError?: (error: Error) => void; disableEvents?: boolean; readOnly?: boolean; + html?: HTMLConfig; } = {}, ): LexicalEditor { const customNodes = config.nodes || []; From 8dafdb1efa20d10f5c40818afec035d1198ff476 Mon Sep 17 00:00:00 2001 From: Ira Hopkinson Date: Tue, 27 Aug 2024 03:10:41 +1200 Subject: [PATCH 089/103] [lexical-playground] Bug Fix: fix comment timestamps (#6555) --- packages/lexical-playground/src/commenting/index.ts | 5 ++++- .../lexical-playground/src/plugins/CommentPlugin/index.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/commenting/index.ts b/packages/lexical-playground/src/commenting/index.ts index 9182a2599ca..59aa95baec0 100644 --- a/packages/lexical-playground/src/commenting/index.ts +++ b/packages/lexical-playground/src/commenting/index.ts @@ -56,7 +56,10 @@ export function createComment( content, deleted: deleted === undefined ? false : deleted, id: id === undefined ? createUID() : id, - timeStamp: timeStamp === undefined ? performance.now() : timeStamp, + timeStamp: + timeStamp === undefined + ? performance.timeOrigin + performance.now() + : timeStamp, type: 'comment', }; } diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx index 1fc15288d41..67aa66662c5 100644 --- a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx @@ -466,7 +466,9 @@ function CommentsPanelListComment({ rtf: Intl.RelativeTimeFormat; thread?: Thread; }): JSX.Element { - const seconds = Math.round((comment.timeStamp - performance.now()) / 1000); + const seconds = Math.round( + (comment.timeStamp - (performance.timeOrigin + performance.now())) / 1000, + ); const minutes = Math.round(seconds / 60); const [modal, showModal] = useModal(); From 36b8eab0b5ed8acd2be816dad85fe92cc908e592 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 26 Aug 2024 19:28:34 +0100 Subject: [PATCH 090/103] =?UTF-8?q?Revert=20"[lexical-playground]=20Fix:?= =?UTF-8?q?=20in=20playground=20show=20component-menu=20w=E2=80=A6=20(#655?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lexical-react/src/shared/LexicalMenu.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.ts index 62b0dc8e3a0..590e5cfdc57 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.ts @@ -494,7 +494,9 @@ export function useMenuAnchorRef( if (rootElement !== null && resolution !== null) { const {left, top, width, height} = resolution.getRect(); const anchorHeight = anchorElementRef.current.offsetHeight; // use to position under anchor - containerDiv.style.top = `${top + anchorHeight + 3}px`; + containerDiv.style.top = `${ + top + window.pageYOffset + anchorHeight + 3 + }px`; containerDiv.style.left = `${left + window.pageXOffset}px`; containerDiv.style.height = `${height}px`; containerDiv.style.width = `${width}px`; @@ -516,7 +518,9 @@ export function useMenuAnchorRef( top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight + height ) { - containerDiv.style.top = `${top - menuHeight - height}px`; + containerDiv.style.top = `${ + top - menuHeight + window.pageYOffset - height + }px`; } } From 366cc18412c0d68ae46622ad2c56876e8f869041 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Tue, 27 Aug 2024 00:43:12 +0300 Subject: [PATCH 091/103] v0.17.1 (#6559) 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 | 434 +++++++++--------- 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 | 6 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 6 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 11 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- scripts/error-codes/codes.json | 7 +- 42 files changed, 426 insertions(+), 381 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975c9b20eb0..4c9ecca8b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +## v0.17.1 (2024-08-26) + +- lexical-playground Bug Fix fix comment timestamps (#6555) Ira Hopkinson +- lexical Add tests for HTMLConfig (#5507) wnhlee +- lexical-table Bug Fix Append a ParagraphNode to each cell when unmerging (#6556) Minseo Kang +- lexical-table Fix table selection paste as plain text (#6548) Ivaylo Pavlov +- lexical-tablelexical-clipboard Bug Fix Race condition in table CUTCOMMAND (#6550) Bob Ippolito +- lexical-playground Bug Fix Fix firefox e2e test regression in Selection.spec.mjs (#6546) Bob Ippolito +- Bug Fix Fix issue where triple-clicking a cell would dangerously select entire document (#6542) Mo +- lexical-playground Fix in playground show component-menu when scroll (#6510) keiseiTi +- lexical-react Fix multiple node selection deletion (#6538) Ivaylo Pavlov +- lexical-yjs Bug Fix Properly sync when emptying document via undo (#6523) Mo +- lexical-table Stop selecting the whole table after pasting cells (#6539) Ivaylo Pavlov +- lexical-table Fix a number of table Cut command scenarios (#6528) Ivaylo Pavlov +- Chore change className props in TreeView component to optional (#6531) Mingxuan Wang +- lexical-list Bug Fix handle non-integer numbers in setIndent (#6522) jrfitzsimmons +- Chore Mark additional tests as flaky from #6535 test runs (#6536) Bob Ippolito +- lexical-table Bug Fix Selection in tables with merged cells (#6529) Botho +- Revert Fix OverflowNode configuration (#6535) Bob Ippolito +- lexical-react Fix Fix React.startTransition on Webpack React 17 (#6517) Turner +- Fix OverflowNode configuration (#6027) Gerard Rovira +- lexical-react remove editorDEPRECATED that has been deprecated for two years (#6494) Bob Ippolito +- lexical Refactor RFC LexicalNode.afterCloneFrom to simplify clone implementation (#6505) Bob Ippolito +- lexicalselection Feature yield target to style patch fn (#6472) Divyansh Kumar +- lexical surface more error details in reconciler (#6511) Sherry +- lexical Bug Fix Fix decorator selection regression with short-circuiting (#6508) Bob Ippolito +- Fix splitText when detached (#6501) Gerard Rovira +- Flow add more HTMLDivElementDOMProps (#6500) Gerard Rovira +- lexical-playground Bug Fix Update tooltip for redo button with correct macOS shortcut (#6497) Bob Ippolito +- lexical Feature Add version identifier to LexicalEditor constructor (#6488) Bob Ippolito +- docs prevent automatic p tag wrapping (#6491) Devy +- Revert lexicalplayground fix block cursor show horizontal (#6490) Bob Ippolito +- When creating a new check list, set the checked value of the list item to false instead of undefined (#5978) Aman Harwara +- lexicalplayground fix block cursor show horizontal (#6486) keiseiTi +- lexical Bug Fix Merge pasted paragraph into empty quote (#6367) wnhlee +- lexical-table Bug Fix Enable observer updates on table elements attributes change (#6479) Evgeny Vorobyev +- v0.17.0 (#6487) Sherry +- v0.17.0 Lexical GitHub Actions Bot + ## v0.17.0 (2024-07-31) - LexicaCI run extended tests for safari in mac-os and chromefirefox in linuxwindows (#6466) Sahejkm diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index fc31de898a8..8b6a1e32f47 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.17.0", + "version": "0.17.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.17.0", - "lexical": "0.17.0", + "@lexical/react": "0.17.1", + "lexical": "0.17.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 b9a8b8cde8f..7ea1556935a 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.17.0", + "version": "0.17.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.17.0", - "@lexical/yjs": "0.17.0", - "lexical": "0.17.0", + "@lexical/react": "0.17.1", + "@lexical/yjs": "0.17.1", + "lexical": "0.17.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 5dbda0fd5db..001d080f988 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.17.0", + "version": "0.17.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.17.0", - "lexical": "0.17.0", + "@lexical/react": "0.17.1", + "lexical": "0.17.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 20deb65f206..7b1d3f90e0b 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.17.0", + "version": "0.17.1", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.17.0", - "lexical": "0.17.0", + "@lexical/react": "0.17.1", + "lexical": "0.17.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 370684b5f36..3fcc072d9e6 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.17.0", + "version": "0.17.1", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/utils": "0.17.0", + "@lexical/dragon": "0.17.1", + "@lexical/history": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/utils": "0.17.1", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.17.0" + "lexical": "0.17.1" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 871e5fd7aa8..93f90b943eb 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.17.0", + "version": "0.17.1", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/dragon": "0.17.1", + "@lexical/history": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index 4f248e3ac2e..8c7a10a01af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.17.0", + "version": "0.17.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "workspaces": [ "packages/*" @@ -36267,28 +36267,28 @@ } }, "packages/lexical": { - "version": "0.17.0", + "version": "0.17.1", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/html": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/html": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1", "prismjs": "^1.27.0" }, "devDependencies": { @@ -36297,7 +36297,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.17.0", + "version": "0.17.1", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -36314,12 +36314,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.17.0", + "@lexical/devtools-core": "0.17.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.17.0", + "lexical": "0.17.1", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -36327,15 +36327,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/html": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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", @@ -36344,15 +36344,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -36363,136 +36363,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/code": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-playground": { - "version": "0.17.0", + "version": "0.17.1", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/file": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/react": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", + "@lexical/clipboard": "0.17.1", + "@lexical/code": "0.17.1", + "@lexical/file": "0.17.1", + "@lexical/hashtag": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/mark": "0.17.1", + "@lexical/overflow": "0.17.1", + "@lexical/plain-text": "0.17.1", + "@lexical/react": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "@lexical/utils": "0.17.1", "katex": "^0.16.10", - "lexical": "0.17.0", + "lexical": "0.17.1", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -36515,28 +36515,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/devtools-core": "0.17.0", - "@lexical/dragon": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/markdown": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "@lexical/yjs": "0.17.0", - "lexical": "0.17.0", + "@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": { @@ -36546,54 +36546,54 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "lexical": "0.17.0" + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "lexical": "0.17.1" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.17.0", + "version": "0.17.1", "dependencies": { "@docusaurus/core": "^3.3.2", "@docusaurus/preset-classic": "^3.3.2", @@ -36622,11 +36622,11 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "@lexical/offset": "0.17.0", - "lexical": "0.17.0" + "@lexical/offset": "0.17.1", + "lexical": "0.17.1" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -36659,10 +36659,10 @@ } }, "packages/shared": { - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } }, @@ -40987,19 +40987,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.17.0", + "@lexical/utils": "0.17.1", "@types/prismjs": "^1.26.0", - "lexical": "0.17.0", + "lexical": "0.17.1", "prismjs": "^1.27.0" } }, @@ -41011,7 +41011,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.17.0", + "@lexical/devtools-core": "0.17.1", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -41020,7 +41020,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.17.0", + "lexical": "0.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -41032,18 +41032,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/eslint-plugin": { @@ -41055,151 +41055,151 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/devtools-core": "0.17.0", - "@lexical/dragon": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/markdown": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "@lexical/yjs": "0.17.0", - "lexical": "0.17.0", + "@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" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "lexical": "0.17.0" + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "lexical": "0.17.1" } }, "@lexical/website": { @@ -41231,8 +41231,8 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.17.0", - "lexical": "0.17.0" + "@lexical/offset": "0.17.1", + "lexical": "0.17.1" } }, "@mdx-js/mdx": { @@ -52952,26 +52952,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/file": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/react": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", + "@lexical/clipboard": "0.17.1", + "@lexical/code": "0.17.1", + "@lexical/file": "0.17.1", + "@lexical/hashtag": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/mark": "0.17.1", + "@lexical/overflow": "0.17.1", + "@lexical/plain-text": "0.17.1", + "@lexical/react": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "@lexical/utils": "0.17.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.17.0", + "lexical": "0.17.1", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -58853,7 +58853,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.17.0" + "lexical": "0.17.1" } }, "shebang-command": { diff --git a/package.json b/package.json index 0a60a5f1659..5e088f09a9d 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.17.0", + "version": "0.17.1", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index 6a79bbcfdf6..e5dc942e4a3 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/html": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index 8e27fd6547f..51b999e6a12 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index a0eb2c427db..c830cfca468 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index b2bb5da5af0..4addb5ec3f7 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.17.0", + "version": "0.17.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.17.0", + "@lexical/devtools-core": "0.17.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.17.0", + "lexical": "0.17.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 e9e276358b1..d1a881dc738 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index 265dabcafe3..331a6f96a57 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.17.0", + "version": "0.17.1", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 1cbc2b4d8d8..cbf65c7db66 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 784622d7830..3937e76a996 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index 4b651029d17..196f562a1bb 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index acb4e60bed2..98c4b920508 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index 14c5da6f845..756af25c0b7 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 34730f514a9..488f1a06c51 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index b2b085f279b..6f2d51154a3 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index f3705f4274f..75209308dc0 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index b866034b957..879ee1e14cc 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@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" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index d7ef4445bb0..7af6c012272 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index d908bead1f2..8c5da5a1a63 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index 554107f5bd5..215158accc6 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.17.0", + "version": "0.17.1", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 066a66037f5..cd07bf6e8da 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.17.0", + "version": "0.17.1", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/file": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/react": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/utils": "0.17.0", + "@lexical/clipboard": "0.17.1", + "@lexical/code": "0.17.1", + "@lexical/file": "0.17.1", + "@lexical/hashtag": "0.17.1", + "@lexical/link": "0.17.1", + "@lexical/list": "0.17.1", + "@lexical/mark": "0.17.1", + "@lexical/overflow": "0.17.1", + "@lexical/plain-text": "0.17.1", + "@lexical/react": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "@lexical/utils": "0.17.1", "katex": "^0.16.10", - "lexical": "0.17.0", + "lexical": "0.17.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 7afe294bcac..0e8f9788c79 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/code": "0.17.0", - "@lexical/devtools-core": "0.17.0", - "@lexical/dragon": "0.17.0", - "@lexical/hashtag": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/link": "0.17.0", - "@lexical/list": "0.17.0", - "@lexical/mark": "0.17.0", - "@lexical/markdown": "0.17.0", - "@lexical/overflow": "0.17.0", - "@lexical/plain-text": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "@lexical/text": "0.17.0", - "@lexical/utils": "0.17.0", - "@lexical/yjs": "0.17.0", - "lexical": "0.17.0", + "@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": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index 64efaa0958b..39175860776 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.17.0", + "version": "0.17.1", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/clipboard": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 6a797221998..22a8f87cdfc 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index fdef1dd4887..01676f90f01 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,12 +8,12 @@ "table" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.17.0", - "lexical": "0.17.0" + "@lexical/utils": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 09aa1528aa9..dbf68a9dba5 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.1" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 4f3f899ef2f..0ac4aec507c 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.17.0", - "@lexical/selection": "0.17.0", - "@lexical/table": "0.17.0", - "lexical": "0.17.0" + "@lexical/list": "0.17.1", + "@lexical/selection": "0.17.1", + "@lexical/table": "0.17.1", + "lexical": "0.17.1" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index 568e8e44931..daa79715a9b 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.17.0", + "version": "0.17.1", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index 2df54014c08..9a246dd5a57 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,12 +11,12 @@ "crdt" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.17.0", - "lexical": "0.17.0" + "@lexical/offset": "0.17.1", + "lexical": "0.17.1" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index f3ca77bb732..7096a3b74ae 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index e1acff658d3..8fe88be6fb0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.17.0", + "version": "0.17.1", "dependencies": { - "lexical": "0.17.0" + "lexical": "0.17.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 0f2d6f14d19..dc92f3c2030 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.17.0", + "version": "0.17.1", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.5.9", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.17.0", - "@lexical/utils": "0.17.0", + "@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.17.0", + "lexical": "0.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" @@ -26,5 +26,6 @@ "devDependencies": { "@playwright/test": "^1.43.1" }, - "sideEffects": false + "sideEffects": false, + "exports": {} } diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index 4c3d8394e12..b419263b0c5 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.17.0", + "version": "0.17.1", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.17.0", - "@lexical/react": "0.17.0", - "lexical": "0.17.0", + "@lexical/plain-text": "0.17.1", + "@lexical/react": "0.17.1", + "lexical": "0.17.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 249b430f3b2..098d5a26a2a 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.17.0", + "version": "0.17.1", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.17.0", - "@lexical/history": "0.17.0", - "@lexical/rich-text": "0.17.0", - "@lexical/utils": "0.17.0", + "@lexical/dragon": "0.17.1", + "@lexical/history": "0.17.1", + "@lexical/rich-text": "0.17.1", + "@lexical/utils": "0.17.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.17.0", + "lexical": "0.17.1", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.7", diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 74daa5f1314..0d5ae282bf6 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -193,5 +193,10 @@ "191": "Unexpected empty pending editor state on discrete nested update", "192": "getCachedTypeToNodeMap called with a writable EditorState", "193": "$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor", - "194": "Children of root nodes must be elements or decorators" + "194": "Children of root nodes must be elements or decorators", + "195": "Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update(), editor.read(), or editorState.read().%s", + "196": "Unable to find an active editor. This method can only be used synchronously during the callback of editor.update() or editor.read().%s", + "197": "$cloneWithProperties: %s.clone(node) (with type '%s') overrided afterCloneFrom but did not call super.afterCloneFrom(prevNode)", + "198": "Attempted to remove event handlers from a node that does not belong to this build of Lexical", + "199": "Indent value must be non-negative." } From 0683d58243a226b066d5231d9a0040185bd08398 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 28 Aug 2024 09:22:10 -0700 Subject: [PATCH 092/103] [lexical-table] Bug Fix: Add @lexical/clipboard as a direct dependency of @lexical/table (#6571) --- packages/lexical-table/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index 01676f90f01..fb990dbe15c 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -12,6 +12,7 @@ "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { + "@lexical/clipboard": "0.17.1", "@lexical/utils": "0.17.1", "lexical": "0.17.1" }, From cd934aff7d81d6a9b1cf92a616864800b697e964 Mon Sep 17 00:00:00 2001 From: Sherry Date: Thu, 29 Aug 2024 02:09:00 +0800 Subject: [PATCH 093/103] [lexical-react] menu positioning: Unrevert PR6510 but with gating (#6566) --- .../lexical-react/src/shared/LexicalMenu.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.ts index 590e5cfdc57..42877233258 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.ts @@ -482,6 +482,7 @@ export function useMenuAnchorRef( setResolution: (r: MenuResolution | null) => void, className?: string, parent: HTMLElement = document.body, + shouldIncludePageYOffset__EXPERIMENTAL: boolean = true, ): MutableRefObject { const [editor] = useLexicalComposerContext(); const anchorElementRef = useRef(document.createElement('div')); @@ -495,7 +496,10 @@ export function useMenuAnchorRef( const {left, top, width, height} = resolution.getRect(); const anchorHeight = anchorElementRef.current.offsetHeight; // use to position under anchor containerDiv.style.top = `${ - top + window.pageYOffset + anchorHeight + 3 + top + + anchorHeight + + 3 + + (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0) }px`; containerDiv.style.left = `${left + window.pageXOffset}px`; containerDiv.style.height = `${height}px`; @@ -519,7 +523,10 @@ export function useMenuAnchorRef( top - rootElementRect.top > menuHeight + height ) { containerDiv.style.top = `${ - top - menuHeight + window.pageYOffset - height + top - + menuHeight - + height + + (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0) }px`; } } @@ -538,7 +545,13 @@ export function useMenuAnchorRef( anchorElementRef.current = containerDiv; rootElement.setAttribute('aria-controls', 'typeahead-menu'); } - }, [editor, resolution, className, parent]); + }, [ + editor, + resolution, + shouldIncludePageYOffset__EXPERIMENTAL, + className, + parent, + ]); useEffect(() => { const rootElement = editor.getRootElement(); From 1f778da76afbd991121712c0f2f9cf6087ecc8d9 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 28 Aug 2024 12:41:29 -0700 Subject: [PATCH 094/103] [*] Feature: Check undeclared dependencies in build (#6574) --- packages/lexical-yjs/package.json | 1 + scripts/build.js | 36 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index 9a246dd5a57..4b861b7c8fd 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -16,6 +16,7 @@ "types": "index.d.ts", "dependencies": { "@lexical/offset": "0.17.1", + "@lexical/selection": "0.17.1", "lexical": "0.17.1" }, "peerDependencies": { diff --git a/scripts/build.js b/scripts/build.js index 14bc48af584..ec1b6c470f7 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -47,13 +47,16 @@ const closureOptions = { warning_level: 'QUIET', }; +const modulePackageMappings = Object.fromEntries( + packagesManager.getPublicPackages().flatMap((pkg) => { + const pkgName = pkg.getNpmName(); + return pkg.getExportedNpmModuleNames().map((npm) => [npm, pkgName]); + }), +); + const wwwMappings = { ...Object.fromEntries( - packagesManager - .getPublicPackages() - .flatMap((pkg) => - pkg.getExportedNpmModuleNames().map((npm) => [npm, npmToWwwName(npm)]), - ), + Object.keys(modulePackageMappings).map((npm) => [npm, npmToWwwName(npm)]), ), 'prismjs/components/prism-c': 'prism-c', 'prismjs/components/prism-clike': 'prism-clike', @@ -126,6 +129,7 @@ function getExtension(format) { * @param {boolean} isProd * @param {'cjs'|'esm'} format * @param {string} version + * @param {import('./shared/PackageMetadata').PackageMetadata} pkg * @returns {Promise>} the exports of the built module */ async function build( @@ -136,10 +140,30 @@ async function build( isProd, format, version, + pkg, ) { const extensions = ['.js', '.jsx', '.ts', '.tsx']; const inputOptions = { external(modulePath, src) { + const modulePkgName = modulePackageMappings[modulePath]; + if ( + typeof modulePkgName === 'string' && + !( + modulePkgName in pkg.packageJson.dependencies || + modulePkgName === pkg.getNpmName() + ) + ) { + console.error( + `Error: ${path.relative( + '.', + src, + )} has an undeclared dependency in its import of ${modulePath}.\nAdd the following to the dependencies in ${path.relative( + '.', + pkg.resolve('package.json'), + )}: "${modulePkgName}": "${version}"`, + ); + process.exit(1); + } return ( monorepoExternalsSet.has(modulePath) || thirdPartyExternalsRegExp.test(modulePath) @@ -422,6 +446,7 @@ async function buildAll() { isProduction, format, version, + pkg, ); if (isRelease) { @@ -436,6 +461,7 @@ async function buildAll() { false, format, version, + pkg, ); buildForkModules(outputPath, outputFileName, format, exports); } From 96e27671261220780e99ccbb285df992c6a84c37 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Fri, 30 Aug 2024 23:21:59 +0300 Subject: [PATCH 095/103] [lexical-table] feat: Add row striping (#6547) Co-authored-by: Bob Ippolito --- .../plugins/TableActionMenuPlugin/index.tsx | 25 +++++- .../src/plugins/TableCellResizer/index.tsx | 7 +- .../src/themes/PlaygroundEditorTheme.css | 3 + .../src/themes/PlaygroundEditorTheme.ts | 1 + .../lexical-react/src/LexicalTablePlugin.ts | 61 ++++++++------ .../lexical-table/src/LexicalTableNode.ts | 79 ++++++++++++++++--- .../lexical-table/src/LexicalTableObserver.ts | 6 ++ .../src/LexicalTableSelectionHelpers.ts | 54 ++++++++++--- .../__tests__/unit/LexicalTableNode.test.tsx | 42 ++++++++++ 9 files changed, 225 insertions(+), 53 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index eb72fd9d7dc..0796241e5e1 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -282,9 +282,9 @@ function TableActionMenu({ throw new Error('Expected to find tableElement in DOM'); } - const tableSelection = getTableObserverFromTableElement(tableElement); - if (tableSelection !== null) { - tableSelection.clearHighlight(); + const tableObserver = getTableObserverFromTableElement(tableElement); + if (tableObserver !== null) { + tableObserver.clearHighlight(); } tableNode.markDirty(); @@ -457,7 +457,19 @@ function TableActionMenu({ tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN); } + clearTableSelection(); + onClose(); + }); + }, [editor, tableCellNode, clearTableSelection, onClose]); + const toggleRowStriping = useCallback(() => { + editor.update(() => { + if (tableCellNode.isAttached()) { + const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); + if (tableNode) { + tableNode.setRowStriping(!tableNode.getRowStriping()); + } + } clearTableSelection(); onClose(); }); @@ -537,6 +549,13 @@ function TableActionMenu({ data-test-id="table-background-color"> Background color +