From a7d3b37b7c81e4ac328427f5d79714ec6cc26004 Mon Sep 17 00:00:00 2001 From: Sherry Date: Mon, 29 Apr 2024 12:18:59 +0800 Subject: [PATCH 01/18] Documentation: add PR template (#5851) --- .github/pull_request_template.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..c8949b400c3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ + + +## Description + +*Describe the changes in this pull request* + +**Closes:** # + +## Test plan + +### Before + +*Insert relevant screenshots/recordings/automated-tests* + + +### After + +*Insert relevant screenshots/recordings/automated-tests* \ No newline at end of file From 34c70f19a37222f7cd58f62dc3bb9b0d0cd350c7 Mon Sep 17 00:00:00 2001 From: Sherry Date: Mon, 29 Apr 2024 14:46:28 +0800 Subject: [PATCH 02/18] refactor: cleanup unused react imports in tests files (#5983) --- .../lexical-react/src/__tests__/unit/LexicalComposer.test.tsx | 1 - .../src/__tests__/unit/PlainRichTextPlugin.test.tsx | 1 - .../src/__tests__/unit/LexicalSelection.test.tsx | 1 - .../src/__tests__/unit/LexicalTableSelection.test.tsx | 1 - .../src/__tests__/unit/LexicalEventHelpers.test.tsx | 1 - packages/lexical/src/__tests__/unit/LexicalListPlugin.test.tsx | 1 - 6 files changed, 6 deletions(-) diff --git a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx index afb35f9fea5..6775ffac0ab 100644 --- a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx +++ b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx @@ -7,7 +7,6 @@ */ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import * as React from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; diff --git a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx index b1878b14921..2af120036c9 100644 --- a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx +++ b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx @@ -23,7 +23,6 @@ import { $getSelection, $isRangeSelection, } from 'lexical'; -import * as React from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index d6a7b9a588a..5cef71ba65b 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -41,7 +41,6 @@ import { invariant, TestComposer, } from 'lexical/src/__tests__/utils'; -import * as React from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx index 4d5f9f3ae95..f098b0b9c86 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx @@ -15,7 +15,6 @@ import { $setSelection, } from 'lexical'; import {createTestEditor} from 'lexical/src/__tests__/utils'; -import * as React from 'react'; import {createRef, useEffect, useMemo} from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx index 7434d0a0937..7a4ed67ba7f 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx +++ b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx @@ -23,7 +23,6 @@ import { } from '@lexical/selection/src/__tests__/utils'; import {TableCellNode, TableNode, TableRowNode} from '@lexical/table'; import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils'; -import * as React from 'react'; import {createRoot} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; diff --git a/packages/lexical/src/__tests__/unit/LexicalListPlugin.test.tsx b/packages/lexical/src/__tests__/unit/LexicalListPlugin.test.tsx index 37c0aa4de3a..f9e6544fe95 100644 --- a/packages/lexical/src/__tests__/unit/LexicalListPlugin.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalListPlugin.test.tsx @@ -21,7 +21,6 @@ import { html, TestComposer, } from 'lexical/src/__tests__/utils'; -import * as React from 'react'; import {createRoot, Root} from 'react-dom/client'; import * as ReactTestUtils from 'react-dom/test-utils'; From 02ec0bb8755ee2a574c834332df811149c527c22 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:21:51 +0800 Subject: [PATCH 03/18] Fix #5738 wrong selection on mouse click (#5969) Co-authored-by: Gerard Rovira --- packages/lexical/src/LexicalSelection.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 12067bad8e3..8bd4f1467d9 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -1912,6 +1912,7 @@ function internalResolveSelectionPoint( // We use the anchor to find which child node to select const childNodes = dom.childNodes; const childNodesLength = childNodes.length; + const blockCursorElement = editor._blockCursorElement; // If the anchor is the same as length, then this means we // need to select the very last text node. if (resolvedOffset === childNodesLength) { @@ -1920,11 +1921,20 @@ function internalResolveSelectionPoint( } let childDOM = childNodes[resolvedOffset]; let hasBlockCursor = false; - if (childDOM === editor._blockCursorElement) { + if (childDOM === blockCursorElement) { childDOM = childNodes[resolvedOffset + 1]; hasBlockCursor = true; - } else if (editor._blockCursorElement !== null) { - resolvedOffset--; + } else if (blockCursorElement !== null) { + const blockCursorElementParent = blockCursorElement.parentNode; + if (dom === blockCursorElementParent) { + const blockCursorOffset = Array.prototype.indexOf.call( + blockCursorElementParent.children, + blockCursorElement, + ); + if (offset > blockCursorOffset) { + resolvedOffset--; + } + } } resolvedNode = getNodeFromDOM(childDOM); From 7e4c8c572eb4248ed1edf2e20c5461abef40ae6e Mon Sep 17 00:00:00 2001 From: Katsiaryna <47710336+KatsiarynaDzibrova@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:30:04 +0100 Subject: [PATCH 04/18] fix(@lexical/playground): We now skip auto-scrolling on table resize (#5986) Co-authored-by: Katsia --- .../src/plugins/TableCellResizer/index.tsx | 125 +++++++++--------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 484338f8919..70d588a833e 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -154,30 +154,34 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { throw new Error('TableCellResizer: Expected active cell.'); } - editor.update(() => { - const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem); - if (!$isTableCellNode(tableCellNode)) { - throw new Error('TableCellResizer: Table cell node not found.'); - } + editor.update( + () => { + const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem); + if (!$isTableCellNode(tableCellNode)) { + throw new Error('TableCellResizer: Table cell node not found.'); + } - const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); + const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode); + const tableRowIndex = + $getTableRowIndexFromTableCellNode(tableCellNode); - const tableRows = tableNode.getChildren(); + const tableRows = tableNode.getChildren(); - if (tableRowIndex >= tableRows.length || tableRowIndex < 0) { - throw new Error('Expected table cell to be inside of table row.'); - } + if (tableRowIndex >= tableRows.length || tableRowIndex < 0) { + throw new Error('Expected table cell to be inside of table row.'); + } - const tableRow = tableRows[tableRowIndex]; + const tableRow = tableRows[tableRowIndex]; - if (!$isTableRowNode(tableRow)) { - throw new Error('Expected table row'); - } + if (!$isTableRowNode(tableRow)) { + throw new Error('Expected table row'); + } - tableRow.setHeight(newHeight); - }); + tableRow.setHeight(newHeight); + }, + {tag: 'skip-scroll-into-view'}, + ); }, [activeCell, editor], ); @@ -187,57 +191,60 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { if (!activeCell) { throw new Error('TableCellResizer: Expected active cell.'); } - editor.update(() => { - const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem); - if (!$isTableCellNode(tableCellNode)) { - throw new Error('TableCellResizer: Table cell node not found.'); - } + editor.update( + () => { + const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem); + if (!$isTableCellNode(tableCellNode)) { + throw new Error('TableCellResizer: Table cell node not found.'); + } - const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); + const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const tableColumnIndex = - $getTableColumnIndexFromTableCellNode(tableCellNode); + const tableColumnIndex = + $getTableColumnIndexFromTableCellNode(tableCellNode); - const tableRows = tableNode.getChildren(); + const tableRows = tableNode.getChildren(); - for (let r = 0; r < tableRows.length; r++) { - const tableRow = tableRows[r]; + for (let r = 0; r < tableRows.length; r++) { + const tableRow = tableRows[r]; - if (!$isTableRowNode(tableRow)) { - throw new Error('Expected table row'); - } + if (!$isTableRowNode(tableRow)) { + throw new Error('Expected table row'); + } - const rowCells = tableRow.getChildren(); - const rowCellsSpan = rowCells.map((cell) => cell.getColSpan()); - - const aggregatedRowSpans = rowCellsSpan.reduce( - (rowSpans: number[], cellSpan) => { - const previousCell = rowSpans[rowSpans.length - 1] ?? 0; - rowSpans.push(previousCell + cellSpan); - return rowSpans; - }, - [], - ); - const rowColumnIndexWithSpan = aggregatedRowSpans.findIndex( - (cellSpan: number) => cellSpan > tableColumnIndex, - ); - - if ( - rowColumnIndexWithSpan >= rowCells.length || - rowColumnIndexWithSpan < 0 - ) { - throw new Error('Expected table cell to be inside of table row.'); - } + const rowCells = tableRow.getChildren(); + const rowCellsSpan = rowCells.map((cell) => cell.getColSpan()); + + const aggregatedRowSpans = rowCellsSpan.reduce( + (rowSpans: number[], cellSpan) => { + const previousCell = rowSpans[rowSpans.length - 1] ?? 0; + rowSpans.push(previousCell + cellSpan); + return rowSpans; + }, + [], + ); + const rowColumnIndexWithSpan = aggregatedRowSpans.findIndex( + (cellSpan: number) => cellSpan > tableColumnIndex, + ); - const tableCell = rowCells[rowColumnIndexWithSpan]; + if ( + rowColumnIndexWithSpan >= rowCells.length || + rowColumnIndexWithSpan < 0 + ) { + throw new Error('Expected table cell to be inside of table row.'); + } - if (!$isTableCellNode(tableCell)) { - throw new Error('Expected table cell'); - } + const tableCell = rowCells[rowColumnIndexWithSpan]; - tableCell.setWidth(newWidth); - } - }); + if (!$isTableCellNode(tableCell)) { + throw new Error('Expected table cell'); + } + + tableCell.setWidth(newWidth); + } + }, + {tag: 'skip-scroll-into-view'}, + ); }, [activeCell, editor], ); From f596b1bdc963de798dede0ea0d01b624376fd907 Mon Sep 17 00:00:00 2001 From: Sahejkm <163521239+Sahejkm@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:05:28 +0800 Subject: [PATCH 05/18] [TreeView] Minor enhancement: Add option to obfuscate text in the TreeView plugin (#5990) --- .../src/generateContent.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/lexical-devtools-core/src/generateContent.ts b/packages/lexical-devtools-core/src/generateContent.ts index db702c6ddd0..37309ccfeb1 100644 --- a/packages/lexical-devtools-core/src/generateContent.ts +++ b/packages/lexical-devtools-core/src/generateContent.ts @@ -88,6 +88,7 @@ export function generateContent( editor: LexicalEditor, commandsLog: ReadonlyArray & {payload: unknown}>, exportDOM: boolean, + obfuscateText: boolean = false, ): string { const editorState = editor.getEditorState(); const editorConfig = editor._config; @@ -118,7 +119,10 @@ export function generateContent( res += `${isSelected ? SYMBOLS.selectedLine : ' '} ${indent.join( ' ', - )} ${nodeKeyDisplay} ${typeDisplay} ${idsDisplay} ${printNode(node)}\n`; + )} ${nodeKeyDisplay} ${typeDisplay} ${idsDisplay} ${printNode( + node, + obfuscateText, + )}\n`; res += printSelectedCharsLine({ indent, @@ -230,18 +234,23 @@ function visitTree( }); } -function normalize(text: string) { - return Object.entries(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).reduce( +function normalize(text: string, obfuscateText: boolean = false) { + const textToPrint = Object.entries(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).reduce( (acc, [key, value]) => acc.replace(new RegExp(key, 'g'), String(value)), text, ); + if (obfuscateText) { + return textToPrint.replace(/[^\s]/g, '*'); + } + return textToPrint; } // TODO Pass via props to allow customizability -function printNode(node: LexicalNode) { +function printNode(node: LexicalNode, obfuscateText: boolean = false) { if ($isTextNode(node)) { const text = node.getTextContent(); - const title = text.length === 0 ? '(empty)' : `"${normalize(text)}"`; + const title = + text.length === 0 ? '(empty)' : `"${normalize(text, obfuscateText)}"`; const properties = printAllTextNodeProperties(node); return [title, properties.length !== 0 ? `{ ${properties} }` : null] .filter(Boolean) @@ -249,7 +258,8 @@ function printNode(node: LexicalNode) { .trim(); } else if ($isLinkNode(node)) { const link = node.getURL(); - const title = link.length === 0 ? '(empty)' : `"${normalize(link)}"`; + const title = + link.length === 0 ? '(empty)' : `"${normalize(link, obfuscateText)}"`; const properties = printAllLinkNodeProperties(node); return [title, properties.length !== 0 ? `{ ${properties} }` : null] .filter(Boolean) From 053e96e769c3e9198ff3868d3d29bc35378bf533 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 30 Apr 2024 14:42:54 -0700 Subject: [PATCH 06/18] fix(build): fix build-www rewriteImports to always rewrite flow modules (#5995) --- scripts/www/rewriteImports.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/www/rewriteImports.js b/scripts/www/rewriteImports.js index b27a1e35f8b..076172cf0d2 100644 --- a/scripts/www/rewriteImports.js +++ b/scripts/www/rewriteImports.js @@ -55,6 +55,10 @@ async function transformFlowFileContents(source) { / \* @flow strict/g, ' * @flow strict\n * @generated\n * @oncall lexical_web_text_editor', ); + // Let the transform know we actually did something. + // Could not figure out the right way to update the + // docblock without an in-place update + context.addLeadingComments(node, ''); } }, }), From d76b93b096c5b2f2f5fc4a719d102e3f1fe1c793 Mon Sep 17 00:00:00 2001 From: Acy Watson Date: Tue, 30 Apr 2024 16:50:19 -0600 Subject: [PATCH 07/18] add flow annotations (#5997) --- packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow | 1 + packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow b/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow index 27b66459534..4fe8d867aae 100644 --- a/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalContextMenuPlugin.js.flow @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict */ /** diff --git a/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow b/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow index a7fed0f44a0..904a6d285fe 100644 --- a/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalNodeEventPlugin.js.flow @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict */ /** From 3f9974bef302b0f6836844f2cc839a48672c5525 Mon Sep 17 00:00:00 2001 From: Acy Watson Date: Tue, 30 Apr 2024 20:07:37 -0600 Subject: [PATCH 08/18] add flow for devtools (#5998) --- .../lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow index e124beb2302..11f5fdbc79e 100644 --- a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow +++ b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow @@ -6,3 +6,7 @@ * * @flow strict */ + +/** + * LexicalDevtoolsCore + */ From 3f61e3354efefeabbc95cf634b48265e2bd584af Mon Sep 17 00:00:00 2001 From: Katsiaryna <47710336+KatsiarynaDzibrova@users.noreply.github.com> Date: Wed, 1 May 2024 12:48:35 +0100 Subject: [PATCH 09/18] Fix table of contents for headings in tables and collapsible sections (#5946) --- .../src/LexicalTableOfContents.tsx | 78 ++++++++++++++++--- packages/lexical-utils/src/index.ts | 31 ++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/packages/lexical-react/src/LexicalTableOfContents.tsx b/packages/lexical-react/src/LexicalTableOfContents.tsx index 602b196af2c..97e8920094e 100644 --- a/packages/lexical-react/src/LexicalTableOfContents.tsx +++ b/packages/lexical-react/src/LexicalTableOfContents.tsx @@ -6,11 +6,19 @@ * */ -import type {LexicalEditor, NodeKey, NodeMutation} from 'lexical'; - import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {$isHeadingNode, HeadingNode, HeadingTagType} from '@lexical/rich-text'; -import {$getNodeByKey, $getRoot, TextNode} from 'lexical'; +import {$getNextRightPreorderNode} from '@lexical/utils'; +import { + $getNodeByKey, + $getRoot, + $isElementNode, + ElementNode, + LexicalEditor, + NodeKey, + NodeMutation, + TextNode, +} from 'lexical'; import {useEffect, useState} from 'react'; export type TableOfContentsEntry = [ @@ -34,12 +42,26 @@ function $insertHeadingIntoTableOfContents( const newEntry: TableOfContentsEntry = toEntry(newHeading); let newTableOfContents: Array = []; if (prevHeading === null) { + // check if key already exists + if ( + currentTableOfContents.length > 0 && + currentTableOfContents[0][0] === newHeading.__key + ) { + return currentTableOfContents; + } newTableOfContents = [newEntry, ...currentTableOfContents]; } else { for (let i = 0; i < currentTableOfContents.length; i++) { const key = currentTableOfContents[i][0]; newTableOfContents.push(currentTableOfContents[i]); if (key === prevHeading.getKey() && key !== newHeading.getKey()) { + // check if key already exists + if ( + i + 1 < currentTableOfContents.length && + currentTableOfContents[i + 1][0] === newHeading.__key + ) { + return currentTableOfContents; + } newTableOfContents.push(newEntry); } } @@ -103,6 +125,14 @@ function $updateHeadingPosition( return newTableOfContents; } +function getPreviousHeading(node: HeadingNode): HeadingNode | null { + let prevHeading = $getNextRightPreorderNode(node); + while (prevHeading !== null && !$isHeadingNode(prevHeading)) { + prevHeading = $getNextRightPreorderNode(prevHeading); + } + return prevHeading; +} + type Props = { children: ( values: Array, @@ -135,6 +165,37 @@ export default function LexicalTableOfContentsPlugin({ setTableOfContents(currentTableOfContents); }); + const removeRootUpdateListener = editor.registerUpdateListener( + ({editorState, dirtyElements}) => { + editorState.read(() => { + const updateChildHeadings = (node: ElementNode) => { + for (const child of node.getChildren()) { + if ($isHeadingNode(child)) { + const prevHeading = getPreviousHeading(child); + currentTableOfContents = $updateHeadingPosition( + prevHeading, + child, + currentTableOfContents, + ); + setTableOfContents(currentTableOfContents); + } else if ($isElementNode(child)) { + updateChildHeadings(child); + } + } + }; + + // If a node is changes, all child heading positions need to be updated + $getRoot() + .getChildren() + .forEach((node) => { + if ($isElementNode(node) && dirtyElements.get(node.__key)) { + updateChildHeadings(node); + } + }); + }); + }, + ); + // Listen to updates to heading mutations and update state const removeHeaderMutationListener = editor.registerMutationListener( HeadingNode, @@ -144,10 +205,7 @@ export default function LexicalTableOfContentsPlugin({ if (mutation === 'created') { const newHeading = $getNodeByKey(nodeKey); if (newHeading !== null) { - let prevHeading = newHeading.getPreviousSibling(); - while (prevHeading !== null && !$isHeadingNode(prevHeading)) { - prevHeading = prevHeading.getPreviousSibling(); - } + const prevHeading = getPreviousHeading(newHeading); currentTableOfContents = $insertHeadingIntoTableOfContents( prevHeading, newHeading, @@ -162,10 +220,7 @@ export default function LexicalTableOfContentsPlugin({ } else if (mutation === 'updated') { const newHeading = $getNodeByKey(nodeKey); if (newHeading !== null) { - let prevHeading = newHeading.getPreviousSibling(); - while (prevHeading !== null && !$isHeadingNode(prevHeading)) { - prevHeading = prevHeading.getPreviousSibling(); - } + const prevHeading = getPreviousHeading(newHeading); currentTableOfContents = $updateHeadingPosition( prevHeading, newHeading, @@ -206,6 +261,7 @@ export default function LexicalTableOfContentsPlugin({ return () => { removeHeaderMutationListener(); removeTextNodeMutationListener(); + removeRootUpdateListener(); }; }, [editor]); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 1d4bd037de0..8a83301a309 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -222,6 +222,37 @@ function $getDepth(node: LexicalNode): number { return depth; } +/** + * Performs a right-to-left preorder tree traversal. + * From the starting node it goes to the rightmost child, than backtracks to paret and finds new rightmost path. + * It will return the next node in traversal sequence after the startingNode. + * The traversal is similar to $dfs functions above, but the nodes are visited right-to-left, not left-to-right. + * @param startingNode - The node to start the search. + * @returns The next node in pre-order right to left traversal sequence or `null`, if the node does not exist + */ +export function $getNextRightPreorderNode( + startingNode: LexicalNode, +): LexicalNode | null { + let node: LexicalNode | null = startingNode; + + if ($isElementNode(node) && node.getChildrenSize() > 0) { + node = node.getLastChild(); + } else { + let sibling = null; + + while (sibling === null && node !== null) { + sibling = node.getPreviousSibling(); + + if (sibling === null) { + node = node.getParent(); + } else { + node = sibling; + } + } + } + return node; +} + /** * Takes a node and traverses up its ancestors (toward the root node) * in order to find a specific type of node. From 116c5977045d500f9f8399486e06dadca8d51f0f Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 1 May 2024 22:08:34 +0100 Subject: [PATCH 10/18] Fix add root element count (#6002) --- packages/lexical/src/LexicalEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 96cf9ad7c86..3086802ba66 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -1210,7 +1210,7 @@ export function addRootElementEvents( ) { doc.addEventListener('selectionchange', onDocumentSelectionChange); } - rootElementsRegistered.set(doc, documentRootElementsCount || 0 + 1); + rootElementsRegistered.set(doc, (documentRootElementsCount || 0) + 1); // @ts-expect-error: internal field rootElement.__lexicalEditor = editor; From d6cdef134f6f00b7b4b5e00e017a8922cf086961 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 1 May 2024 23:07:13 +0100 Subject: [PATCH 11/18] Root element count invariant & test (#6003) --- packages/lexical/src/LexicalEvents.ts | 6 ++++-- .../src/__tests__/unit/LexicalEditor.test.tsx | 16 ++++++++++++++++ scripts/error-codes/codes.json | 3 ++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 3086802ba66..6b3c2973901 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -1319,8 +1319,10 @@ export function removeRootElementEvents(rootElement: HTMLElement): void { // We only want to have a single global selectionchange event handler, shared // between all editor instances. - rootElementsRegistered.set(doc, documentRootElementsCount - 1); - if (rootElementsRegistered.get(doc) === 0) { + const newCount = documentRootElementsCount - 1; + invariant(newCount >= 0, 'Root element count less than 0'); + rootElementsRegistered.set(doc, newCount); + if (newCount === 0) { doc.removeEventListener('selectionchange', onDocumentSelectionChange); } diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 8e5d86c9f4f..0b9ab667f63 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -2215,4 +2215,20 @@ describe('LexicalEditor tests', () => { expect(ParagraphNode.importDOM).toHaveBeenCalledTimes(1); }); + + it('root element count is always positive', () => { + const newEditor1 = createTestEditor(); + const newEditor2 = createTestEditor(); + + const container1 = document.createElement('div'); + const container2 = document.createElement('div'); + + newEditor1.setRootElement(container1); + newEditor1.setRootElement(null); + + newEditor1.setRootElement(container1); + newEditor2.setRootElement(container2); + newEditor1.setRootElement(null); + newEditor2.setRootElement(null); + }); }); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 8869d17b2a5..bdb4a97372d 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -162,5 +162,6 @@ "160": "Expected TableCellNode parent to be a TableRowNode", "161": "Unexpected dirty selection to be null", "162": "Root element not registered", - "163": "node is not a ListNode" + "163": "node is not a ListNode", + "164": "Root element count less than 0" } From 6bb75a4ac8fbe4bd011eb429cd0e03cd9eec8b71 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Wed, 1 May 2024 17:04:52 -0700 Subject: [PATCH 12/18] fix(build): clean up of build-www rewriteImports (follow-up to #5995) (#5999) --- .../unit/transformFlowFileContents.test.js | 107 ++++++++++++++++++ scripts/www/rewriteImports.js | 54 +-------- scripts/www/transformFlowFileContents.js | 90 +++++++++++++++ 3 files changed, 198 insertions(+), 53 deletions(-) create mode 100644 scripts/www/__tests__/unit/transformFlowFileContents.test.js create mode 100644 scripts/www/transformFlowFileContents.js diff --git a/scripts/www/__tests__/unit/transformFlowFileContents.test.js b/scripts/www/__tests__/unit/transformFlowFileContents.test.js new file mode 100644 index 00000000000..af3b931fede --- /dev/null +++ b/scripts/www/__tests__/unit/transformFlowFileContents.test.js @@ -0,0 +1,107 @@ +/** + * 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. + * + */ +// @ts-check +'use strict'; + +const transformFlowFileContents = require('../../transformFlowFileContents'); + +const HEADER_BEFORE = + ` +/** + * 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 + */ +`.trim() + '\n'; + +const HEADER_AFTER = + ` +/** + * 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 + * @generated + * @oncall lexical_web_text_editor + */ +`.trim() + '\n'; + +const IMPORTS_BEFORE = + ` +import type {Doc, RelativePosition, UndoManager, XmlText} from 'yjs'; +import type { + DecoratorNode, + EditorState, + ElementNode, + LexicalCommand, + LexicalEditor, + LexicalNode, + LineBreakNode, + NodeMap, + NodeKey, + TextNode, +} from 'lexical'; +`.trim() + '\n'; + +const IMPORTS_AFTER = + ` +import type {Doc, RelativePosition, UndoManager, XmlText} from 'yjs'; +import type { + DecoratorNode, + EditorState, + ElementNode, + LexicalCommand, + LexicalEditor, + LexicalNode, + LineBreakNode, + NodeMap, + NodeKey, + TextNode, +} from 'Lexical'; +`.trim() + '\n'; + +const EXTRA_BLOCK_COMMENT = + ` +/** + * LexicalDevToolsCore + */ +`.trim() + '\n'; + +describe('transformFlowFileContents', () => { + [ + { + input: [HEADER_BEFORE, IMPORTS_BEFORE, EXTRA_BLOCK_COMMENT].join('\n'), + output: [HEADER_AFTER, IMPORTS_AFTER, EXTRA_BLOCK_COMMENT].join('\n'), + title: 'header-imports-comment', + }, + { + input: [HEADER_BEFORE, EXTRA_BLOCK_COMMENT].join('\n'), + output: [HEADER_AFTER, EXTRA_BLOCK_COMMENT].join('\n'), + title: 'header-comment', + }, + { + input: [HEADER_BEFORE, IMPORTS_BEFORE].join('\n'), + output: [HEADER_AFTER, IMPORTS_AFTER].join('\n'), + title: 'header-imports', + }, + { + input: [HEADER_BEFORE].join('\n'), + output: [HEADER_AFTER].join('\n'), + title: 'header', + }, + ].forEach(({input, output, title}) => { + it(`transforms ${title}`, async () => { + expect(await transformFlowFileContents(input)).toBe(output); + }); + }); +}); diff --git a/scripts/www/rewriteImports.js b/scripts/www/rewriteImports.js index 076172cf0d2..6635300f5ad 100644 --- a/scripts/www/rewriteImports.js +++ b/scripts/www/rewriteImports.js @@ -12,59 +12,7 @@ const fs = require('fs-extra'); const glob = require('glob'); const path = require('node:path'); const {packagesManager} = require('../shared/packagesManager'); -const npmToWwwName = require('./npmToWwwName'); -const {t, transform} = require('hermes-transform'); - -const wwwMappings = Object.fromEntries( - packagesManager - .getPublicPackages() - .flatMap((pkg) => - pkg.getExportedNpmModuleNames().map((npm) => [npm, npmToWwwName(npm)]), - ), -); - -/** - * It would be nice to use jscodeshift for this but the flow sources are using - * ast features that are not supported in ast-types (as of 2024-04-11) so it's - * not possible to traverse the tree and replace the imports & comments. - * - * It might be possible going straight to flow-parser, but it was a slew of - * hardcoded regexps before and now it's at least automated based on the - * exports. - * - * @param {string} source - * @returns {Promise} transformed source - */ -async function transformFlowFileContents(source) { - return await transform( - source, - (context) => ({ - ImportDeclaration(node) { - const value = wwwMappings[node.source.value]; - if (value) { - context.replaceNode(node.source, t.StringLiteral({value})); - } - }, - Program(node) { - if ( - node.docblock && - node.docblock.comment && - node.docblock.comment.value.includes('@flow strict') - ) { - node.docblock.comment.value = node.docblock.comment.value.replace( - / \* @flow strict/g, - ' * @flow strict\n * @generated\n * @oncall lexical_web_text_editor', - ); - // Let the transform know we actually did something. - // Could not figure out the right way to update the - // docblock without an in-place update - context.addLeadingComments(node, ''); - } - }, - }), - {}, - ); -} +const transformFlowFileContents = require('./transformFlowFileContents'); // This script attempts to find all Flow definition modules, and makes // them compatible with www. Specifically, it finds any imports that diff --git a/scripts/www/transformFlowFileContents.js b/scripts/www/transformFlowFileContents.js new file mode 100644 index 00000000000..4dc505c3d36 --- /dev/null +++ b/scripts/www/transformFlowFileContents.js @@ -0,0 +1,90 @@ +/** + * 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. + * + */ + +'use strict'; + +const {packagesManager} = require('../shared/packagesManager'); +const npmToWwwName = require('./npmToWwwName'); +const {t, transform} = require('hermes-transform'); +const prettier = require('prettier'); + +const wwwMappings = Object.fromEntries( + packagesManager + .getPublicPackages() + .flatMap((pkg) => + pkg.getExportedNpmModuleNames().map((npm) => [npm, npmToWwwName(npm)]), + ), +); + +const prettierConfig = prettier.resolveConfig('./').then((cfg) => cfg || {}); + +/** + * Add a statement to the end of the code so the comments don't + * disappear. This is a workaround for a hermes transform issue. + * + * @param {string} code + */ +function wrapCode(code) { + return [code, 'export {};\n'].join('\n'); +} + +/** + * The inverse transform of wrapCode, removes the added statement. + * + * @param {string} code + */ +function unwrapCode(code) { + return code.replace(/\n+export {};\n?$/, '\n'); +} + +/** + * It would be nice to use jscodeshift for this but the flow sources are using + * ast features that are not supported in ast-types (as of 2024-04-11) so it's + * not possible to traverse the tree and replace the imports & comments. + * + * It might be possible going straight to flow-parser, but it was a slew of + * hardcoded regexps before and now it's at least automated based on the + * exports. + * + * @param {string} source + * @returns {Promise} transformed source + */ +module.exports = async function transformFlowFileContents(source) { + return unwrapCode( + await transform( + wrapCode(source), + (context) => ({ + ImportDeclaration(node) { + const value = wwwMappings[node.source.value]; + if (value) { + context.replaceNode(node.source, t.StringLiteral({value})); + } + }, + Program(node) { + if ( + node.docblock && + node.docblock.comment && + node.docblock.comment.value.includes('@flow strict') + ) { + // This is mutated in-place because I couldn't find a mutation that + // did not fail for replacing the Program node. + node.docblock.comment.value = node.docblock.comment.value.replace( + / \* @flow strict/g, + ' * @flow strict\n * @generated\n * @oncall lexical_web_text_editor', + ); + // We need the mutations array to be non-empty, so remove something + // that is not there. The AST traversals use object identity in a + // Set so we don't have to worry about some other line changing. + context.removeComments(t.LineComment({value: ''})); + } + }, + }), + await prettierConfig, + ), + ); +}; From a346764d43cd027544fa4fd563e02bc91f2f668a Mon Sep 17 00:00:00 2001 From: matsuyama-k1 <111737064+matsuyama-k1@users.noreply.github.com> Date: Thu, 2 May 2024 22:45:23 +0900 Subject: [PATCH 13/18] Fix: #5976 Passing an empty ParagraphNode to $dfs incorrectly returns content from subsequent paragraphs (#5977) --- .../__tests__/unit/LexicalNodeHelpers.test.ts | 29 +++++++++++++++++++ packages/lexical-utils/src/index.ts | 3 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts index 28783003cdd..aa88bd53b30 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts +++ b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts @@ -203,5 +203,34 @@ describe('LexicalNodeHelpers tests', () => { ]); }); }); + + test('DFS of empty ParagraphNode returns only itself', async () => { + const editor: LexicalEditor = testEnv.editor; + + let paragraphKey; + + await editor.update(() => { + const root = $getRoot(); + + const paragraph = $createParagraphNode(); + const paragraph2 = $createParagraphNode(); + const text = $createTextNode('test'); + + paragraphKey = paragraph.getKey(); + + paragraph2.append(text); + root.append(paragraph, paragraph2); + }); + await editor.update(() => { + const paragraph = $getNodeByKey(paragraphKey); + + expect($dfs(paragraph)).toEqual([ + { + depth: 1, + node: paragraph.getLatest(), + }, + ]); + }); + }); }); }); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 8a83301a309..2a52e780973 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -177,7 +177,8 @@ export function $dfs( const nodes = []; const start = (startingNode || $getRoot()).getLatest(); const end = - endingNode || ($isElementNode(start) ? start.getLastDescendant() : start); + endingNode || + ($isElementNode(start) ? start.getLastDescendant() || start : start); let node: LexicalNode | null = start; let depth = $getDepth(node); From 14e9f8e4616913e4d016a2ec0c434025052f18bf Mon Sep 17 00:00:00 2001 From: Vlad Fedosov Date: Thu, 2 May 2024 17:52:09 -0400 Subject: [PATCH 14/18] [@lexical/website] Documentation: Added FAQ page around Lexical collaborative mode (#5993) --- .../lexical-website/docs/collaboration/faq.md | 158 ++++++++++++++++++ packages/lexical-website/sidebars.js | 2 +- 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/lexical-website/docs/collaboration/faq.md diff --git a/packages/lexical-website/docs/collaboration/faq.md b/packages/lexical-website/docs/collaboration/faq.md new file mode 100644 index 00000000000..eaa36ff23cd --- /dev/null +++ b/packages/lexical-website/docs/collaboration/faq.md @@ -0,0 +1,158 @@ +--- +sidebar_position: 2 +--- + +# Collaboration FAQ + +## Source of truth: Lexical State, Yjs and App's DB + +It's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing. +But, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server. + +You can also treat the database as the source of truth. This is how it could be achieved: + +- Clients receive a `sessionId` when they connect to the server +- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId` +- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour) +- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client +- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high. + +Or, there is an ever simpler approach: + +- When a client connects to the server, the server populates the room content if empty +- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour) +- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server) + +When the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs. + +_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_ + + +## Initializing `EditorState` from Yjs Document + +It's achievable by leveraging headless Lexical and no-op provider for Yjs: + +
+ createHeadlessCollaborativeEditor.ts + + ```typescript + import type {Binding, Provider} from '@lexical/yjs'; + import type { + Klass, + LexicalEditor, + LexicalNode, + LexicalNodeReplacement, + SerializedEditorState, + SerializedLexicalNode, + } from 'lexical'; + + import {createHeadlessEditor} from '@lexical/headless'; + import { + createBinding, + syncLexicalUpdateToYjs, + syncYjsChangesToLexical, + } from '@lexical/yjs'; + import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs'; + + export default function headlessConvertYDocStateToLexicalJSON( + nodes: ReadonlyArray | LexicalNodeReplacement>, + yDocState: Uint8Array, + ): SerializedEditorState { + return withHeadlessCollaborationEditor(nodes, (editor, binding) => { + applyUpdate(binding.doc, yDocState, {isUpdateRemote: true}); + editor.update(() => {}, {discrete: true}); + + return editor.getEditorState().toJSON(); + }); + } + + /** + * Creates headless collaboration editor with no-op provider (since it won't + * connect to message distribution infra) and binding. It also sets up + * bi-directional synchronization between yDoc and editor + */ + function withHeadlessCollaborationEditor( + nodes: ReadonlyArray | LexicalNodeReplacement>, + callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T, + ): T { + const editor = createHeadlessEditor({ + nodes, + }); + + const id = 'main'; + const doc = new Doc(); + const docMap = new Map([[id, doc]]); + const provider = createNoOpProvider(); + const binding = createBinding(editor, provider, id, doc, docMap); + + const unsubscribe = registerCollaborationListeners(editor, provider, binding); + + const res = callback(editor, binding, provider); + + unsubscribe(); + + return res; + } + + function registerCollaborationListeners( + editor: LexicalEditor, + provider: Provider, + binding: Binding, + ): () => void { + const unsubscribeUpdateListener = editor.registerUpdateListener( + ({ + dirtyElements, + dirtyLeaves, + editorState, + normalizedNodes, + prevEditorState, + tags, + }) => { + if (tags.has('skip-collab') === false) { + syncLexicalUpdateToYjs( + binding, + provider, + prevEditorState, + editorState, + dirtyElements, + dirtyLeaves, + normalizedNodes, + tags, + ); + } + }, + ); + + const observer = (events: Array>, transaction: Transaction) => { + if (transaction.origin !== binding) { + syncYjsChangesToLexical(binding, provider, events, false); + } + }; + + binding.root.getSharedType().observeDeep(observer); + + return () => { + unsubscribeUpdateListener(); + binding.root.getSharedType().unobserveDeep(observer); + }; + } + + function createNoOpProvider(): Provider { + const emptyFunction = () => {}; + + return { + awareness: { + getLocalState: () => null, + getStates: () => new Map(), + off: emptyFunction, + on: emptyFunction, + setLocalState: emptyFunction, + }, + connect: emptyFunction, + disconnect: emptyFunction, + off: emptyFunction, + on: emptyFunction, + }; + } + ``` +
diff --git a/packages/lexical-website/sidebars.js b/packages/lexical-website/sidebars.js index cb284eda073..08a7c9c36f3 100644 --- a/packages/lexical-website/sidebars.js +++ b/packages/lexical-website/sidebars.js @@ -72,7 +72,7 @@ const sidebars = { type: 'category', }, { - items: ['collaboration/react'], + items: ['collaboration/react', 'collaboration/faq'], label: 'Collaboration', type: 'category', }, From fdce31d056e09c41b5e57cf6814907df5ad2aef9 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 2 May 2024 14:53:25 -0700 Subject: [PATCH 15/18] [lexical-website] Bug Fix: add allow-popups-to-escape-sandbox to iframe sandbox flags (#5988) --- packages/lexical-website/docs/concepts/node-replacement.md | 2 +- .../lexical-website/docs/getting-started/creating-plugin.md | 2 +- .../lexical-website/docs/getting-started/quick-start.md | 2 +- packages/lexical-website/docs/getting-started/react.md | 2 +- .../src/components/HomepageExamples/index.js | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/lexical-website/docs/concepts/node-replacement.md b/packages/lexical-website/docs/concepts/node-replacement.md index 10d3f06906a..06435667189 100644 --- a/packages/lexical-website/docs/concepts/node-replacement.md +++ b/packages/lexical-website/docs/concepts/node-replacement.md @@ -28,5 +28,5 @@ Once this is done, Lexical will replace all ParagraphNode instances with CustomP style="width:100%; height:700px; border:0; border-radius:4px; overflow:hidden;" title="lexical-collapsible-container-plugin-example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" - sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" + sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts" > diff --git a/packages/lexical-website/docs/getting-started/creating-plugin.md b/packages/lexical-website/docs/getting-started/creating-plugin.md index 1e76287ba81..32791754217 100644 --- a/packages/lexical-website/docs/getting-started/creating-plugin.md +++ b/packages/lexical-website/docs/getting-started/creating-plugin.md @@ -183,4 +183,4 @@ mergeRegister( ); ``` - + diff --git a/packages/lexical-website/docs/getting-started/quick-start.md b/packages/lexical-website/docs/getting-started/quick-start.md index 3275560dfc2..6c31823aa1e 100644 --- a/packages/lexical-website/docs/getting-started/quick-start.md +++ b/packages/lexical-website/docs/getting-started/quick-start.md @@ -128,4 +128,4 @@ editor.registerUpdateListener(({editorState}) => { Here we have simplest Lexical setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`) and accessibility (`@lexical/dragon`) features enabled. - + diff --git a/packages/lexical-website/docs/getting-started/react.md b/packages/lexical-website/docs/getting-started/react.md index 9c6b12f447d..b5ec59a2086 100644 --- a/packages/lexical-website/docs/getting-started/react.md +++ b/packages/lexical-website/docs/getting-started/react.md @@ -81,7 +81,7 @@ Below you can find an example of the integration from the previous chapter that However no UI can be created w/o CSS and Lexical is not an exception here. Pay attention to `ExampleTheme.ts` and how it's used in this example, with corresponding styles defined in `styles.css`. - + ## Saving Lexical State diff --git a/packages/lexical-website/src/components/HomepageExamples/index.js b/packages/lexical-website/src/components/HomepageExamples/index.js index 38436d3960d..f47cacafa88 100644 --- a/packages/lexical-website/src/components/HomepageExamples/index.js +++ b/packages/lexical-website/src/components/HomepageExamples/index.js @@ -79,7 +79,7 @@ export default function HomepageExamples() { - {EXAMPLES.map(({id, content, src}) => ( + {EXAMPLES.map(({id, content, src, label}) => (
@@ -98,8 +98,8 @@ export default function HomepageExamples() {