From 97481c9d1035c8759f2b94897847e7d0e2b0c12a Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 6 Dec 2024 06:56:51 -0800 Subject: [PATCH 01/31] [lexical-table][lexical-utils][lexical-react]: Bug Fix: Enforce table integrity with transforms and move non-React plugin code to @lexical/table (#6914) --- .../__tests__/e2e/Tables.spec.mjs | 12 +- .../src/plugins/TablePlugin.tsx | 46 +-- .../lexical-react/src/LexicalTablePlugin.ts | 211 +----------- .../src/shared/useCharacterLimit.ts | 14 +- .../lexical-table/flow/LexicalTable.js.flow | 14 +- .../lexical-table/src/LexicalTableCellNode.ts | 2 +- .../lexical-table/src/LexicalTableNode.ts | 9 +- .../src/LexicalTablePluginHelpers.ts | 275 +++++++++++++++ .../lexical-table/src/LexicalTableRowNode.ts | 8 +- .../__tests__/unit/LexicalTableNode.test.tsx | 312 ++++++++++++++++++ packages/lexical-table/src/index.ts | 5 + .../lexical-utils/flow/LexicalUtils.js.flow | 11 + .../unit/descendantsMatching.test.tsx | 84 +++++ .../src/__tests__/unit/iterators.test.tsx | 216 ++++++++++++ .../unit/unwrapAndFilterDescendants.test.tsx | 101 ++++++ packages/lexical-utils/src/index.ts | 155 +++++++++ .../src/__tests__/unit/LexicalEditor.test.tsx | 4 +- 17 files changed, 1217 insertions(+), 262 deletions(-) create mode 100644 packages/lexical-table/src/LexicalTablePluginHelpers.ts create mode 100644 packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx create mode 100644 packages/lexical-utils/src/__tests__/unit/iterators.test.tsx create mode 100644 packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 4fd7ca25b30..1cf4e25a7b1 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -3737,7 +3737,9 @@ test.describe.parallel('Tables', () => { Hello world

-
+ +


+

{ -
-
+ +


+ + +


+

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

+ Items +

+
+

+ Expenditure +

+
+

+ Donuts +

+
+

+ 3,000 +

+
+

+ Stationery +

+
+

+ 18,000 +

+
+

+ Totals +

+
+

+ 21,000 +

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


+
+

+ He-Man +

+
+

+ Skeletor +

+
+

+ Role +

+
+

+ Hero +

+
+

+ Villain +

+
+

+ Weapon +

+
+

+ Power Sword +

+
+

+ Havoc Staff +

+
+

+ Dark secret +

+
+

+ Expert florist +

+
+

+ Cries at romcoms +

+
+ `, + ); + }); + test('Copy table from an external source like gdoc with formatting', async () => { const {editor} = testEnv; diff --git a/packages/lexical-table/src/index.ts b/packages/lexical-table/src/index.ts index be452681b98..c4fe6ace096 100644 --- a/packages/lexical-table/src/index.ts +++ b/packages/lexical-table/src/index.ts @@ -29,6 +29,11 @@ export { } from './LexicalTableNode'; export type {TableDOMCell} from './LexicalTableObserver'; export {$getTableAndElementByKey, TableObserver} from './LexicalTableObserver'; +export { + registerTableCellUnmergeTransform, + registerTablePlugin, + registerTableSelectionObserver, +} from './LexicalTablePluginHelpers'; export type {SerializedTableRowNode} from './LexicalTableRowNode'; export { $createTableRowNode, diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 11524eee950..958dd8acfa7 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -125,3 +125,14 @@ declare export function $splitNode( declare export function calculateZoomLevel(element: Element | null): number; declare export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean; + +declare export function $unwrapAndFilterDescendants( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, +): boolean; + +declare export function $firstToLastIterator(node: ElementNode): Iterable; + +declare export function $lastToFirstIterator(node: ElementNode): Iterable; + +declare export function $unwrapNode(node: ElementNode): void; diff --git a/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx b/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx new file mode 100644 index 00000000000..3a4e596626f --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/descendantsMatching.test.tsx @@ -0,0 +1,84 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import type {Klass, LexicalEditor, LexicalNode} from 'lexical'; + +import {$descendantsMatching} from '@lexical/utils'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $isTextNode, + ParagraphNode, +} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +function $createTextAndParagraphWithDepth(depth: number): LexicalNode[] { + if (depth <= 0) { + return [$createTextNode(`<${depth} />`)]; + } + return [ + $createTextNode(`<${depth}>`), + $createParagraphNode().append( + ...$createTextAndParagraphWithDepth(depth - 1), + ), + $createTextNode(``), + ]; +} + +function textContentForDepth(i: number): string { + return i > 0 ? `<${i}>${textContentForDepth(i - 1)}` : `<${i} />`; +} + +describe('$descendantsMatching', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + [0, 1, 2].forEach((depth) => + it(`Can un-nest children at depth ${depth}`, () => { + editor.update( + () => { + const firstNode = $createParagraphNode(); + $getRoot() + .clear() + .append( + firstNode.append(...$createTextAndParagraphWithDepth(depth)), + ); + }, + {discrete: true}, + ); + editor.update( + () => { + const firstNode = assertClass( + $getRoot().getFirstChildOrThrow(), + ParagraphNode, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(depth === 0); + firstNode.splice( + 0, + firstNode.getChildrenSize(), + $descendantsMatching(firstNode.getChildren(), $isTextNode), + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(true); + expect(firstNode.getTextContent()).toBe(textContentForDepth(depth)); + }, + {discrete: true}, + ); + }), + ); +}); diff --git a/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx b/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx new file mode 100644 index 00000000000..f7042e14e0e --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/iterators.test.tsx @@ -0,0 +1,216 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import type {Klass, LexicalEditor, LexicalNode} from 'lexical'; + +import {$firstToLastIterator, $lastToFirstIterator} from '@lexical/utils'; +import {$createParagraphNode, $createTextNode, TextNode} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +describe('$firstToLastIterator', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it(`Iterates from first to last`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + return assertClass(node, TextNode).getTextContent(); + }), + ).toEqual(['0', '1', '2', '3', '4']); + // Parent was not affected + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + }, + {discrete: true}, + ); + }); + it(`Can handle node removal`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + node.remove(); + return rval; + }), + ).toEqual(['0', '1', '2', '3', '4']); + expect(parent.getChildren()).toEqual([]); + }, + {discrete: true}, + ); + }); + it(`Detects cycles when nodes move incorrectly`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect(() => + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + parent.append(node); + return rval; + }), + ).toThrow(/\$childIterator: Cycle detected/); + }, + {discrete: true}, + ); + }); + it(`Can handle nodes moving in the other direction`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($firstToLastIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + if (node.getPreviousSibling() !== null) { + parent.splice(0, 0, [node]); + } + return rval; + }), + ).toEqual(['0', '1', '2', '3', '4']); + // This mutation reversed the nodes while traversing + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['4', '3', '2', '1', '0']); + }, + {discrete: true}, + ); + }); +}); + +describe('$lastToFirstIterator', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it(`Iterates from last to first`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + return assertClass(node, TextNode).getTextContent(); + }), + ).toEqual(['4', '3', '2', '1', '0']); + // Parent was not affected + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + }, + {discrete: true}, + ); + }); + it(`Can handle node removal`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + node.remove(); + return rval; + }), + ).toEqual(['4', '3', '2', '1', '0']); + expect(parent.getChildren()).toEqual([]); + }, + {discrete: true}, + ); + }); + it(`Can handle nodes moving in the other direction`, () => { + editor.update( + () => { + const parent = $createParagraphNode().splice( + 0, + 0, + Array.from({length: 5}, (_v, i) => $createTextNode(`${i}`)), + ); + // Check initial state + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['0', '1', '2', '3', '4']); + expect( + Array.from($lastToFirstIterator(parent), (node) => { + const rval = assertClass(node, TextNode).getTextContent(); + parent.append(node); + return rval; + }), + ).toEqual(['4', '3', '2', '1', '0']); + // This mutation reversed the nodes while traversing + expect( + parent.getAllTextNodes().map((node) => node.getTextContent()), + ).toEqual(['4', '3', '2', '1', '0']); + }, + {discrete: true}, + ); + }); +}); diff --git a/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx b/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx new file mode 100644 index 00000000000..cb42d0fe643 --- /dev/null +++ b/packages/lexical-utils/src/__tests__/unit/unwrapAndFilterDescendants.test.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import type {Klass, LexicalEditor, LexicalNode} from 'lexical'; + +import {$unwrapAndFilterDescendants} from '@lexical/utils'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $isParagraphNode, + $isTextNode, + ParagraphNode, +} from 'lexical'; +import {createTestEditor} from 'lexical/src/__tests__/utils'; + +function assertClass(v: unknown, klass: Klass): T { + if (v instanceof klass) { + return v as T; + } + throw new Error(`Value does not extend ${klass.name}`); +} + +function $createTextAndParagraphWithDepth(depth: number): LexicalNode[] { + if (depth <= 0) { + return [$createTextNode(`<${depth} />`)]; + } + return [ + $createTextNode(`<${depth}>`), + $createParagraphNode().append( + ...$createTextAndParagraphWithDepth(depth - 1), + ), + $createTextNode(``), + ]; +} + +function textContentForDepth(i: number): string { + return i > 0 ? `<${i}>${textContentForDepth(i - 1)}` : `<${i} />`; +} + +describe('$unwrapAndFilterDescendants', () => { + let editor: LexicalEditor; + + beforeEach(async () => { + editor = createTestEditor(); + editor._headless = true; + }); + + it('Is a no-op with valid children', () => { + editor.update( + () => { + $getRoot().clear().append($createParagraphNode()); + }, + {discrete: true}, + ); + editor.update( + () => { + expect($unwrapAndFilterDescendants($getRoot(), $isParagraphNode)).toBe( + false, + ); + expect($getRoot().getChildrenSize()).toBe(1); + expect($isParagraphNode($getRoot().getFirstChild())).toBe(true); + }, + {discrete: true}, + ); + }); + [0, 1, 2].forEach((depth) => + it(`Can un-nest children at depth ${depth}`, () => { + editor.update( + () => { + const firstNode = $createParagraphNode(); + $getRoot() + .clear() + .append( + firstNode.append(...$createTextAndParagraphWithDepth(depth)), + ); + }, + {discrete: true}, + ); + editor.update( + () => { + const firstNode = assertClass( + $getRoot().getFirstChildOrThrow(), + ParagraphNode, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(depth === 0); + expect($unwrapAndFilterDescendants(firstNode, $isTextNode)).toBe( + depth > 0, + ); + expect(firstNode.getChildren().every($isTextNode)).toBe(true); + expect(firstNode.getTextContent()).toBe(textContentForDepth(depth)); + }, + {discrete: true}, + ); + }), + ); +}); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 8994e3dad65..0a758f40f2f 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -23,6 +23,7 @@ import { Klass, LexicalEditor, LexicalNode, + NodeKey, } from 'lexical'; // This underscore postfixing is used as a hotfix so we do not // export shared types from this module #5918 @@ -690,3 +691,157 @@ export function calculateZoomLevel(element: Element | null): number { export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean { return editor._parentEditor !== null; } + +/** + * A depth first last-to-first traversal of root that stops at each node that matches + * $predicate and ensures that its parent is root. This is typically used to discard + * invalid or unsupported wrapping nodes. For example, a TableNode must only have + * TableRowNode as children, but an importer might add invalid nodes based on + * caption, tbody, thead, etc. and this will unwrap and discard those. + * + * @param root The root to start the traversal + * @param $predicate Should return true for nodes that are permitted to be children of root + * @returns true if this unwrapped or removed any nodes + */ +export function $unwrapAndFilterDescendants( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, +): boolean { + return $unwrapAndFilterDescendantsImpl(root, $predicate, null); +} + +function $unwrapAndFilterDescendantsImpl( + root: ElementNode, + $predicate: (node: LexicalNode) => boolean, + $onSuccess: null | ((node: LexicalNode) => void), +): boolean { + let didMutate = false; + for (const node of $lastToFirstIterator(root)) { + if ($predicate(node)) { + if ($onSuccess !== null) { + $onSuccess(node); + } + continue; + } + didMutate = true; + if ($isElementNode(node)) { + $unwrapAndFilterDescendantsImpl( + node, + $predicate, + $onSuccess ? $onSuccess : (child) => node.insertAfter(child), + ); + } + node.remove(); + } + return didMutate; +} + +/** + * A depth first traversal of the children array that stops at and collects + * each node that `$predicate` matches. This is typically used to discard + * invalid or unsupported wrapping nodes on a children array in the `after` + * of an {@link lexical!DOMConversionOutput}. For example, a TableNode must only have + * TableRowNode as children, but an importer might add invalid nodes based on + * caption, tbody, thead, etc. and this will unwrap and discard those. + * + * This function is read-only and performs no mutation operations, which makes + * it suitable for import and export purposes but likely not for any in-place + * mutation. You should use {@link $unwrapAndFilterDescendants} for in-place + * mutations such as node transforms. + * + * @param children The children to traverse + * @param $predicate Should return true for nodes that are permitted to be children of root + * @returns The children or their descendants that match $predicate + */ +export function $descendantsMatching( + children: LexicalNode[], + $predicate: (node: LexicalNode) => node is T, +): T[]; +export function $descendantsMatching( + children: LexicalNode[], + $predicate: (node: LexicalNode) => boolean, +): LexicalNode[] { + const result: LexicalNode[] = []; + const stack = [...children].reverse(); + for (let child = stack.pop(); child !== undefined; child = stack.pop()) { + if ($predicate(child)) { + result.push(child); + } else if ($isElementNode(child)) { + for (const grandchild of $lastToFirstIterator(child)) { + stack.push(grandchild); + } + } + } + return result; +} + +/** + * Return an iterator that yields each child of node from first to last, taking + * care to preserve the next sibling before yielding the value in case the caller + * removes the yielded node. + * + * @param node The node whose children to iterate + * @returns An iterator of the node's children + */ +export function $firstToLastIterator(node: ElementNode): Iterable { + return { + [Symbol.iterator]: () => + $childIterator(node.getFirstChild(), (child) => child.getNextSibling()), + }; +} + +/** + * Return an iterator that yields each child of node from last to first, taking + * care to preserve the previous sibling before yielding the value in case the caller + * removes the yielded node. + * + * @param node The node whose children to iterate + * @returns An iterator of the node's children + */ +export function $lastToFirstIterator(node: ElementNode): Iterable { + return { + [Symbol.iterator]: () => + $childIterator(node.getLastChild(), (child) => + child.getPreviousSibling(), + ), + }; +} + +function $childIterator( + initialNode: LexicalNode | null, + nextNode: (node: LexicalNode) => LexicalNode | null, +): Iterator { + let state = initialNode; + const seen = __DEV__ ? new Set() : null; + return { + next() { + if (state === null) { + return iteratorDone; + } + const rval = iteratorNotDone(state); + if (__DEV__ && seen !== null) { + const key = state.getKey(); + invariant( + !seen.has(key), + '$childIterator: Cycle detected, node with key %s has already been traversed', + String(key), + ); + seen.add(key); + } + state = nextNode(state); + return rval; + }, + }; +} + +/** + * Insert all children before this node, and then remove it. + * + * @param node The ElementNode to unwrap and remove + */ +export function $unwrapNode(node: ElementNode): void { + for (const child of $firstToLastIterator(node)) { + node.insertBefore(child); + } + node.remove(); +} diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index cf33a568d3f..3986f27806f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -2210,7 +2210,7 @@ describe('LexicalEditor tests', () => { await editor.update(() => { const root = $getRoot(); - const tableCell = $createTableCellNode(0); + const tableCell = $createTableCellNode(); const tableRow = $createTableRowNode(); const table = $createTableNode(); @@ -2225,7 +2225,7 @@ describe('LexicalEditor tests', () => { await editor.update(() => { const tableRow = $getNodeByKey(tableRowKey) as TableRowNode; - const tableCell = $createTableCellNode(0); + const tableCell = $createTableCellNode(); tableRow.append(tableCell); }); From 55ef7cad49c303a343575745d311fb1a3d05c15b Mon Sep 17 00:00:00 2001 From: Hadi Elghoul <113323394+elgh0ul@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:25:05 -0500 Subject: [PATCH 02/31] [lexical][@lexical/selection] Feature: Unify $selectAll Implementations (#6902) Co-authored-by: Hadi Elghoul --- packages/lexical-selection/src/index.ts | 4 +-- .../lexical-selection/src/range-selection.ts | 35 ------------------- packages/lexical/src/LexicalUtils.ts | 21 +++++++++-- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/packages/lexical-selection/src/index.ts b/packages/lexical-selection/src/index.ts index d901ab4d4d9..8d9d47ce635 100644 --- a/packages/lexical-selection/src/index.ts +++ b/packages/lexical-selection/src/index.ts @@ -18,7 +18,6 @@ import { $isParentElementRTL, $moveCaretSelection, $moveCharacter, - $selectAll, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, @@ -32,7 +31,9 @@ import { export { /** @deprecated moved to the lexical package */ $cloneWithProperties, + /** @deprecated moved to the lexical package */ $selectAll, } from 'lexical'; + export { $addNodeStyle, $isAtNodeEnd, @@ -48,7 +49,6 @@ export { $isParentElementRTL, $moveCaretSelection, $moveCharacter, - $selectAll, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $wrapNodes, diff --git a/packages/lexical-selection/src/range-selection.ts b/packages/lexical-selection/src/range-selection.ts index e92a81b6188..15b4e66c5c7 100644 --- a/packages/lexical-selection/src/range-selection.ts +++ b/packages/lexical-selection/src/range-selection.ts @@ -452,41 +452,6 @@ export function $moveCharacter( ); } -/** - * Expands the current Selection to cover all of the content in the editor. - * @param selection - The current selection. - */ -export function $selectAll(selection: RangeSelection): void { - const anchor = selection.anchor; - const focus = selection.focus; - const anchorNode = anchor.getNode(); - const topParent = anchorNode.getTopLevelElementOrThrow(); - const root = topParent.getParentOrThrow(); - let firstNode = root.getFirstDescendant(); - let lastNode = root.getLastDescendant(); - let firstType: 'element' | 'text' = 'element'; - let lastType: 'element' | 'text' = 'element'; - let lastOffset = 0; - - if ($isTextNode(firstNode)) { - firstType = 'text'; - } else if (!$isElementNode(firstNode) && firstNode !== null) { - firstNode = firstNode.getParentOrThrow(); - } - - if ($isTextNode(lastNode)) { - lastType = 'text'; - lastOffset = lastNode.getTextContentSize(); - } else if (!$isElementNode(lastNode) && lastNode !== null) { - lastNode = lastNode.getParentOrThrow(); - } - - if (firstNode && lastNode) { - anchor.set(firstNode.getKey(), 0, firstType); - focus.set(lastNode.getKey(), lastOffset, lastType); - } -} - /** * Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue. * @param node - The node whose style value to get. diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index f4cc88ac86a..d4bc2d1ea46 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -1084,10 +1084,25 @@ export function isSelectAll( return key.toLowerCase() === 'a' && controlOrMeta(metaKey, ctrlKey); } -export function $selectAll(): void { +export function $selectAll(selection?: RangeSelection | null): RangeSelection { const root = $getRoot(); - const selection = root.select(0, root.getChildrenSize()); - $setSelection($normalizeSelection(selection)); + + if ($isRangeSelection(selection)) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = anchor.getNode(); + const topParent = anchorNode.getTopLevelElementOrThrow(); + const rootNode = topParent.getParentOrThrow(); + anchor.set(rootNode.getKey(), 0, 'element'); + focus.set(rootNode.getKey(), rootNode.getChildrenSize(), 'element'); + $normalizeSelection(selection); + return selection; + } else { + // Create a new RangeSelection + const newSelection = root.select(0, root.getChildrenSize()); + $setSelection($normalizeSelection(newSelection)); + return newSelection; + } } export function getCachedClassNameArray( From 7776cea0f1862cb57b3c2d2118da629e098b784c Mon Sep 17 00:00:00 2001 From: Basile Savouret <47100280+basile-savouret@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:26:53 +0100 Subject: [PATCH 03/31] [lexical-playground]: Fix empty layout item causes 100% CPU usage (#6906) Co-authored-by: Ivaylo Pavlov --- .../src/plugins/LayoutPlugin/LayoutPlugin.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx b/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx index cbdeee1fe23..dd226849e62 100644 --- a/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx +++ b/packages/lexical-playground/src/plugins/LayoutPlugin/LayoutPlugin.tsx @@ -97,6 +97,25 @@ export function LayoutPlugin(): null { return false; }; + const $fillLayoutItemIfEmpty = (node: LayoutItemNode) => { + if (node.isEmpty()) { + node.append($createParagraphNode()); + } + }; + + const $removeIsolatedLayoutItem = (node: LayoutItemNode): boolean => { + const parent = node.getParent(); + if (!$isLayoutContainerNode(parent)) { + const children = node.getChildren(); + for (const child of children) { + node.insertBefore(child); + } + node.remove(); + return true; + } + return false; + }; + return mergeRegister( // When layout is the last child pressing down/right arrow will insert paragraph // below it to allow adding more content. It's similar what $insertBlockNode @@ -186,17 +205,17 @@ export function LayoutPlugin(): null { }, COMMAND_PRIORITY_EDITOR, ), - // Structure enforcing transformers for each node type. In case nesting structure is not - // "Container > Item" it'll unwrap nodes and convert it back - // to regular content. + editor.registerNodeTransform(LayoutItemNode, (node) => { - const parent = node.getParent(); - if (!$isLayoutContainerNode(parent)) { - const children = node.getChildren(); - for (const child of children) { - node.insertBefore(child); - } - node.remove(); + // Structure enforcing transformers for each node type. In case nesting structure is not + // "Container > Item" it'll unwrap nodes and convert it back + // to regular content. + const isRemoved = $removeIsolatedLayoutItem(node); + + if (!isRemoved) { + // Layout item should always have a child. this function will listen + // for any empty layout item and fill it with a paragraph node + $fillLayoutItemIfEmpty(node); } }), editor.registerNodeTransform(LayoutContainerNode, (node) => { From 05fa244bd0f6043114ffb8feab2922d8e4de7e6f Mon Sep 17 00:00:00 2001 From: daichan132 <71433925+daichan132@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:42:29 +0900 Subject: [PATCH 04/31] [lexical-playground] Chore: Update Prettier to v3 (#6920) --- package-lock.json | 30 ++++++- .../__tests__/utils/index.mjs | 26 +++--- packages/lexical-playground/package.json | 2 +- .../components/PrettierButton/index.tsx | 85 ++++++++++--------- 4 files changed, 86 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5387c0ffdc..4488fab50c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30584,6 +30584,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, "bin": { "prettier": "bin-prettier.js" }, @@ -39153,7 +39154,7 @@ "katex": "^0.16.10", "lexical": "0.21.0", "lodash-es": "^4.17.21", - "prettier": "^2.3.2", + "prettier": "^3.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", @@ -39173,6 +39174,21 @@ "vite-plugin-static-copy": "^2.1.0" } }, + "packages/lexical-playground/node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "packages/lexical-react": { "name": "@lexical/react", "version": "0.21.0", @@ -56265,7 +56281,7 @@ "katex": "^0.16.10", "lexical": "0.21.0", "lodash-es": "^4.17.21", - "prettier": "^2.3.2", + "prettier": "^3.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", @@ -56275,6 +56291,13 @@ "vite-plugin-static-copy": "^2.1.0", "y-websocket": "^1.5.4", "yjs": ">=13.5.42" + }, + "dependencies": { + "prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==" + } } }, "lib0": { @@ -60208,7 +60231,8 @@ "prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==" + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true }, "prettier-plugin-hermes-parser": { "version": "0.20.1", diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index ed81a581866..a9e13d587ec 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -225,7 +225,7 @@ async function assertHTMLOnPageOrFrame( frameName, actualHtmlModificationsCallback = (actualHtml) => actualHtml, ) { - const expected = prettifyHTML(expectedHtml.replace(/\n/gm, ''), { + const expected = await prettifyHTML(expectedHtml.replace(/\n/gm, ''), { ignoreClasses, ignoreInlineStyles, }); @@ -236,7 +236,7 @@ async function assertHTMLOnPageOrFrame( .first() .innerHTML(), ); - let actual = prettifyHTML(actualHtml.replace(/\n/gm, ''), { + let actual = await prettifyHTML(actualHtml.replace(/\n/gm, ''), { ignoreClasses, ignoreInlineStyles, }); @@ -780,7 +780,10 @@ export async function dragImage( ); } -export function prettifyHTML(string, {ignoreClasses, ignoreInlineStyles} = {}) { +export async function prettifyHTML( + string, + {ignoreClasses, ignoreInlineStyles} = {}, +) { let output = string; if (ignoreClasses) { @@ -793,15 +796,14 @@ export function prettifyHTML(string, {ignoreClasses, ignoreInlineStyles} = {}) { output = output.replace(/\s__playwright_target__="[^"]+"/, ''); - return prettier - .format(output, { - attributeGroups: ['$DEFAULT', '^data-'], - attributeSort: 'ASC', - bracketSameLine: true, - htmlWhitespaceSensitivity: 'ignore', - parser: 'html', - }) - .trim(); + return await prettier.format(output, { + attributeGroups: ['$DEFAULT', '^data-'], + attributeSort: 'asc', + bracketSameLine: true, + htmlWhitespaceSensitivity: 'ignore', + parser: 'html', + plugins: ['prettier-plugin-organize-attributes'], + }); } // This function does not suppose to do anything, it's only used as a trigger diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index cd7ced6303f..5a2751577cf 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -29,7 +29,7 @@ "katex": "^0.16.10", "lexical": "0.21.0", "lodash-es": "^4.17.21", - "prettier": "^2.3.2", + "prettier": "^3.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", diff --git a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx index 203e63702b9..c1e47f23ec4 100644 --- a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +++ b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx @@ -10,7 +10,6 @@ import './index.css'; import {$isCodeNode} from '@lexical/code'; import {$getNearestNodeFromDOMNode, LexicalEditor} from 'lexical'; import {Options} from 'prettier'; -import * as React from 'react'; import {useState} from 'react'; interface Props { @@ -20,17 +19,27 @@ interface Props { } const PRETTIER_PARSER_MODULES = { - css: () => import('prettier/parser-postcss'), - html: () => import('prettier/parser-html'), - js: () => import('prettier/parser-babel'), - markdown: () => import('prettier/parser-markdown'), + css: [() => import('prettier/parser-postcss')], + html: [() => import('prettier/parser-html')], + js: [ + () => import('prettier/parser-babel'), + () => import('prettier/plugins/estree'), + ], + markdown: [() => import('prettier/parser-markdown')], + typescript: [ + () => import('prettier/parser-typescript'), + () => import('prettier/plugins/estree'), + ], } as const; type LanguagesType = keyof typeof PRETTIER_PARSER_MODULES; async function loadPrettierParserByLang(lang: string) { - const dynamicImport = PRETTIER_PARSER_MODULES[lang as LanguagesType]; - return await dynamicImport(); + const dynamicImports = PRETTIER_PARSER_MODULES[lang as LanguagesType]; + const modules = await Promise.all( + dynamicImports.map((dynamicImport) => dynamicImport()), + ); + return modules; } async function loadPrettierFormat() { @@ -39,18 +48,11 @@ async function loadPrettierFormat() { } const PRETTIER_OPTIONS_BY_LANG: Record = { - css: { - parser: 'css', - }, - html: { - parser: 'html', - }, - js: { - parser: 'babel', - }, - markdown: { - parser: 'markdown', - }, + css: {parser: 'css'}, + html: {parser: 'html'}, + js: {parser: 'babel'}, + markdown: {parser: 'markdown'}, + typescript: {parser: 'typescript'}, }; const LANG_CAN_BE_PRETTIER = Object.keys(PRETTIER_OPTIONS_BY_LANG); @@ -76,36 +78,37 @@ export function PrettierButton({lang, editor, getCodeDOMNode}: Props) { async function handleClick(): Promise { const codeDOMNode = getCodeDOMNode(); + if (!codeDOMNode) { + return; + } + + let content = ''; + editor.update(() => { + const codeNode = $getNearestNodeFromDOMNode(codeDOMNode); + if ($isCodeNode(codeNode)) { + content = codeNode.getTextContent(); + } + }); + if (content === '') { + return; + } try { const format = await loadPrettierFormat(); const options = getPrettierOptions(lang); - options.plugins = [await loadPrettierParserByLang(lang)]; - - if (!codeDOMNode) { - return; - } + const prettierParsers = await loadPrettierParserByLang(lang); + options.plugins = prettierParsers.map( + (parser) => parser.default || parser, + ); + const formattedCode = await format(content, options); editor.update(() => { const codeNode = $getNearestNodeFromDOMNode(codeDOMNode); - if ($isCodeNode(codeNode)) { - const content = codeNode.getTextContent(); - - let parsed = ''; - - try { - parsed = format(content, options); - } catch (error: unknown) { - setError(error); - } - - if (parsed !== '') { - const selection = codeNode.select(0); - selection.insertText(parsed); - setSyntaxError(''); - setTipsVisible(false); - } + const selection = codeNode.select(0); + selection.insertText(formattedCode); + setSyntaxError(''); + setTipsVisible(false); } }); } catch (error: unknown) { From 4e8d5352f5c629195670317bd202cb77d33b5e8a Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 9 Dec 2024 16:49:59 +0000 Subject: [PATCH 05/31] Doc nits (#6927) --- packages/lexical-website/docs/react/plugins.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/lexical-website/docs/react/plugins.md b/packages/lexical-website/docs/react/plugins.md index 3f6d24cc9d5..ca97f7935ca 100644 --- a/packages/lexical-website/docs/react/plugins.md +++ b/packages/lexical-website/docs/react/plugins.md @@ -41,7 +41,7 @@ const initialConfig = { ### `LexicalPlainTextPlugin` -React wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting +React wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting. ```jsx @@ -73,7 +73,7 @@ Plugin that calls `onChange` whenever Lexical state is updated. Using `ignoreHis ### `LexicalHistoryPlugin` -React wrapper for `@lexical/history` that adds support for history stack management and `undo` / `redo` commands +React wrapper for `@lexical/history` that adds support for history stack management and `undo` / `redo` commands. ```jsx @@ -81,7 +81,7 @@ React wrapper for `@lexical/history` that adds support for history stack managem ### `LexicalLinkPlugin` -React wrapper for `@lexical/link` that adds support for links, including `$toggleLink` command support that toggles link for selected text +React wrapper for `@lexical/link` that adds support for links, including `$toggleLink` command support that toggles link for selected text. ```jsx @@ -107,7 +107,7 @@ React wrapper for `@lexical/list` that adds support for check lists. Note that i [![See API Documentation](/img/see-api-documentation.svg)](/docs/api/modules/lexical_react_LexicalTablePlugin) -React wrapper for `@lexical/table` that adds support for tables +React wrapper for `@lexical/table` that adds support for tables. ```jsx @@ -153,7 +153,7 @@ const MATCHERS = [ ### `LexicalClearEditorPlugin` -Adds `clearEditor` command support to clear editor's content +Adds `clearEditor` command support to clear editor's content. ```jsx @@ -161,7 +161,7 @@ Adds `clearEditor` command support to clear editor's content ### `LexicalMarkdownShortcutPlugin` -Adds markdown shortcut support: headings, lists, code blocks, quotes, links and inline styles (bold, italic, strikethrough) +Adds markdown shortcut support: headings, lists, code blocks, quotes, links and inline styles (bold, italic, strikethrough). ```jsx From 09779cb2cc8461b1245d4aea0db77e5b205a33e5 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 9 Dec 2024 19:26:45 +0000 Subject: [PATCH 06/31] Update core-tests workflow triggers (#6928) --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f5074869c1..d3826cbbdc0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,9 +9,6 @@ on: - 'packages/lexical-website/**' pull_request: types: [opened, synchronize, reopened] - paths-ignore: - - 'examples/**' - - 'packages/lexical-website/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 9198eb0f6980c6736a7bac688725c62ebc7dbe81 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Tue, 10 Dec 2024 01:26:57 -0800 Subject: [PATCH 07/31] [*] Bug Fix: add merge_group to the tests workflow (#6932) --- .github/workflows/tests.yml | 1 + .../__tests__/e2e/List.spec.mjs | 103 +++++++++--------- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3826cbbdc0..431c342226a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,7 @@ on: - 'packages/lexical-website/**' pull_request: types: [opened, synchronize, reopened] + merge_group: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index 8c1703c15c9..f6d46104fc2 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -28,7 +28,6 @@ import { focusEditor, html, initialize, - IS_LINUX, pasteFromClipboard, repeat, selectFromAlignDropdown, @@ -72,60 +71,62 @@ test.beforeEach(({isPlainText}) => { test.describe.parallel('Nested List', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test(`Can create a list and partially copy some content out of it`, async ({ - page, - isCollab, - }) => { - test.fixme(isCollab && IS_LINUX, 'Flaky on Linux + Collab'); - await focusEditor(page); - await page.keyboard.type( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam venenatis risus ac cursus efficitur. Cras efficitur magna odio, lacinia posuere mauris placerat in. Etiam eu congue nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla vulputate justo id eros convallis, vel pellentesque orci hendrerit. Pellentesque accumsan molestie eros, vitae tempor nisl semper sit amet. Sed vulputate leo dolor, et bibendum quam feugiat eget. Praesent vestibulum libero sed enim ornare, in consequat dui posuere. Maecenas ornare vestibulum felis, non elementum urna imperdiet sit amet.', - ); - await toggleBulletList(page); - await moveToEditorBeginning(page); - await moveRight(page, 6); - await selectCharacters(page, 'right', 11); + test( + `Can create a list and partially copy some content out of it`, + { + tag: '@flaky', + }, + async ({page, isCollab}) => { + await focusEditor(page); + await page.keyboard.type( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam venenatis risus ac cursus efficitur. Cras efficitur magna odio, lacinia posuere mauris placerat in. Etiam eu congue nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla vulputate justo id eros convallis, vel pellentesque orci hendrerit. Pellentesque accumsan molestie eros, vitae tempor nisl semper sit amet. Sed vulputate leo dolor, et bibendum quam feugiat eget. Praesent vestibulum libero sed enim ornare, in consequat dui posuere. Maecenas ornare vestibulum felis, non elementum urna imperdiet sit amet.', + ); + await toggleBulletList(page); + await moveToEditorBeginning(page); + await moveRight(page, 6); + await selectCharacters(page, 'right', 11); - await withExclusiveClipboardAccess(async () => { - const clipboard = await copyToClipboard(page); + await withExclusiveClipboardAccess(async () => { + const clipboard = await copyToClipboard(page); - await moveToEditorEnd(page); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); + await moveToEditorEnd(page); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); - await pasteFromClipboard(page, clipboard); - }); + await pasteFromClipboard(page, clipboard); + }); - await assertHTML( - page, - html` -

-

- ipsum dolor -

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

+ ipsum dolor +

+ `, + ); + }, + ); test('Should outdent if indented when the backspace key is pressed', async ({ page, From 49580d4212437e31e5f81b5388da065022252b7b Mon Sep 17 00:00:00 2001 From: "C." <106287207+citruscai@users.noreply.github.com> Date: Tue, 10 Dec 2024 07:47:02 -0600 Subject: [PATCH 08/31] [Breaking Change][lexical-list] Fix: Preserve original format after indenting list item (#6912) Co-authored-by: Bob Ippolito Co-authored-by: Gerard Rovira --- .../lexical-list/src/LexicalListItemNode.ts | 95 +++++++++---------- packages/lexical-list/src/formatList.ts | 11 +-- packages/lexical-list/src/utils.ts | 3 +- .../__tests__/e2e/List.spec.mjs | 45 +++++++++ .../src/__tests__/unit/LexicalEditor.test.tsx | 2 +- .../unit/LexicalSerialization.test.ts | 4 +- .../lexical/src/nodes/LexicalParagraphNode.ts | 13 ++- 7 files changed, 105 insertions(+), 68 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index f4fafcba71a..5581aba730b 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -7,20 +7,6 @@ */ import type {ListNode, ListType} from './'; -import type { - BaseSelection, - DOMConversionMap, - DOMConversionOutput, - DOMExportOutput, - EditorConfig, - EditorThemeClasses, - LexicalNode, - NodeKey, - ParagraphNode, - RangeSelection, - SerializedElementNode, - Spread, -} from 'lexical'; import { addClassNamesToElement, @@ -29,11 +15,24 @@ import { import { $applyNodeReplacement, $createParagraphNode, + $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection, + BaseSelection, + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + EditorThemeClasses, ElementNode, LexicalEditor, + LexicalNode, + NodeKey, + ParagraphNode, + RangeSelection, + SerializedParagraphNode, + Spread, } from 'lexical'; import invariant from 'shared/invariant'; import normalizeClassNames from 'shared/normalizeClassNames'; @@ -47,11 +46,11 @@ export type SerializedListItemNode = Spread< checked: boolean | undefined; value: number; }, - SerializedElementNode + SerializedParagraphNode >; /** @noInheritDoc */ -export class ListItemNode extends ElementNode { +export class ListItemNode extends ParagraphNode { /** @internal */ __value: number; /** @internal */ @@ -81,12 +80,11 @@ export class ListItemNode extends ElementNode { $setListItemThemeClassNames(element, config.theme, this); return element; } + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { + if (super.updateDOM(prevNode, dom, config)) { + return true; + } - updateDOM( - prevNode: ListItemNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { const parent = this.getParent(); if ($isListNode(parent) && parent.getListType() === 'check') { updateListItemChecked(dom, this, prevNode, parent); @@ -94,7 +92,6 @@ export class ListItemNode extends ElementNode { // @ts-expect-error - this is always HTMLListItemElement dom.value = this.__value; $setListItemThemeClassNames(dom, config.theme, this); - return false; } @@ -128,6 +125,12 @@ export class ListItemNode extends ElementNode { node.setValue(serializedNode.value); node.setFormat(serializedNode.format); node.setDirection(serializedNode.direction); + if (typeof serializedNode.textFormat === 'number') { + node.setTextFormat(serializedNode.textFormat); + } + if (typeof serializedNode.textStyle === 'string') { + node.setTextStyle(serializedNode.textStyle); + } return node; } @@ -224,15 +227,11 @@ export class ListItemNode extends ElementNode { } const siblings = this.getNextSiblings(); - - // Split the lists and insert the node in between them listNode.insertAfter(node, restoreSelection); if (siblings.length !== 0) { const newListNode = $createListNode(listNode.getListType()); - siblings.forEach((sibling) => newListNode.append(sibling)); - node.insertAfter(newListNode, restoreSelection); } @@ -256,51 +255,49 @@ export class ListItemNode extends ElementNode { } insertNewAfter( - _: RangeSelection, + selection: RangeSelection, restoreSelection = true, ): ListItemNode | ParagraphNode { const newElement = $createListItemNode( this.__checked == null ? undefined : false, ); + + const format = selection.format; + newElement.setTextFormat(format); + + newElement.setFormat(this.getFormatType()); this.insertAfter(newElement, restoreSelection); return newElement; } - collapseAtStart(selection: RangeSelection): true { + collapseAtStart(): boolean { + const selection = $getSelection(); + + if (!$isRangeSelection(selection)) { + return false; + } + const paragraph = $createParagraphNode(); const children = this.getChildren(); children.forEach((child) => paragraph.append(child)); + const listNode = this.getParentOrThrow(); - const listNodeParent = listNode.getParentOrThrow(); - const isIndented = $isListItemNode(listNodeParent); + const listNodeParent = listNode.getParent(); + + if (!$isListNode(listNode)) { + return false; + } if (listNode.getChildrenSize() === 1) { - if (isIndented) { - // if the list node is nested, we just want to remove it, - // effectively unindenting it. + if ($isListItemNode(listNodeParent)) { listNode.remove(); listNodeParent.select(); } else { listNode.insertBefore(paragraph); listNode.remove(); - // If we have selection on the list item, we'll need to move it - // to the paragraph - const anchor = selection.anchor; - const focus = selection.focus; - const key = paragraph.getKey(); - - if (anchor.type === 'element' && anchor.getNode().is(this)) { - anchor.set(key, anchor.offset, 'element'); - } - - if (focus.type === 'element' && focus.getNode().is(this)) { - focus.set(key, focus.offset, 'element'); - } + paragraph.select(); } - } else { - listNode.insertBefore(paragraph); - this.remove(); } return true; diff --git a/packages/lexical-list/src/formatList.ts b/packages/lexical-list/src/formatList.ts index 3dc4a22ea20..83172275908 100644 --- a/packages/lexical-list/src/formatList.ts +++ b/packages/lexical-list/src/formatList.ts @@ -243,7 +243,6 @@ export function removeList(editor: LexicalEditor): void { if ($isLeafNode(node)) { const listItemNode = $getNearestNodeOfType(node, ListItemNode); - if (listItemNode != null) { listNodes.add($getTopListNode(listItemNode)); } @@ -479,11 +478,13 @@ export function $handleListInsertParagraph(): boolean { return false; } // Only run this code on empty list items + const anchor = selection.anchor.getNode(); if (!$isListItemNode(anchor) || anchor.getChildrenSize() !== 0) { return false; } + const topListNode = $getTopListNode(anchor); const parent = anchor.getParent(); @@ -493,8 +494,7 @@ export function $handleListInsertParagraph(): boolean { ); const grandparent = parent.getParent(); - - let replacementNode; + let replacementNode: ElementNode; if ($isRootOrShadowRoot(grandparent)) { replacementNode = $createParagraphNode(); @@ -505,13 +505,12 @@ export function $handleListInsertParagraph(): boolean { } else { return false; } + replacementNode.select(); const nextSiblings = anchor.getNextSiblings(); - if (nextSiblings.length > 0) { const newList = $createListNode(parent.getListType()); - if ($isParagraphNode(replacementNode)) { replacementNode.insertAfter(newList); } else { @@ -524,9 +523,7 @@ export function $handleListInsertParagraph(): boolean { newList.append(sibling); }); } - // Don't leave hanging nested empty lists $removeHighestEmptyListParent(anchor); - return true; } diff --git a/packages/lexical-list/src/utils.ts b/packages/lexical-list/src/utils.ts index 9c9b1bf9af1..9b443f76205 100644 --- a/packages/lexical-list/src/utils.ts +++ b/packages/lexical-list/src/utils.ts @@ -6,9 +6,8 @@ * */ -import type {LexicalNode, Spread} from 'lexical'; - import {$findMatchingParent} from '@lexical/utils'; +import {type LexicalNode, type Spread} from 'lexical'; import invariant from 'shared/invariant'; import { diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index f6d46104fc2..e1173a1f66f 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -17,6 +17,7 @@ import { redo, selectAll, selectCharacters, + toggleBold, undo, } from '../keyboardShortcuts/index.mjs'; import { @@ -1881,4 +1882,48 @@ test.describe.parallel('Nested List', () => { }); }, ); + test('new list item should preserve format from previous list item even after new list item is indented', async ({ + page, + }) => { + await focusEditor(page); + await toggleBulletList(page); + await toggleBold(page); + await page.keyboard.type('MLH Fellowship'); + await page.keyboard.press('Enter'); + await clickIndentButton(page); + await page.keyboard.type('Fall 2024'); + await assertHTML( + page, + html` +
    +
  • + + MLH Fellowship + +
  • +
  • +
      +
    • + + Fall 2024 + +
    • +
    +
  • +
+ `, + ); + }); }); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 3986f27806f..7f7be6d9c5f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -1026,7 +1026,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,"textStyle":""}],"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,"textFormat":0,"textStyle":""}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; init(); const contentEditable = editor.getRootElement(); editor.setEditable(editable); diff --git a/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts b/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts index 9237bc9d3dd..96820722dcd 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,"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}}`; + 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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"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}}`, + `{"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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index c1250aeae16..036799b3b71 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -120,11 +120,7 @@ export class ParagraphNode extends ElementNode { } return dom; } - updateDOM( - prevNode: ParagraphNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { return false; } @@ -164,7 +160,9 @@ export class ParagraphNode extends ElementNode { node.setFormat(serializedNode.format); node.setIndent(serializedNode.indent); node.setDirection(serializedNode.direction); - node.setTextFormat(serializedNode.textFormat); + if (typeof serializedNode.textFormat === 'number') { + node.setTextFormat(serializedNode.textFormat); + } return node; } @@ -190,7 +188,8 @@ export class ParagraphNode extends ElementNode { const direction = this.getDirection(); newElement.setDirection(direction); newElement.setFormat(this.getFormatType()); - newElement.setStyle(this.getTextStyle()); + newElement.setStyle(this.getStyle()); + this.insertAfter(newElement, restoreSelection); return newElement; } From df15f5398542f9bc5aae0a3212ab3f33cc5a4462 Mon Sep 17 00:00:00 2001 From: Oluwasanya Olaoluwa Date: Wed, 11 Dec 2024 03:54:55 +1100 Subject: [PATCH 09/31] [lexical-playground] Fix: tabs do not show strikethrough/underline (#6811) Co-authored-by: Bob Ippolito --- .../__tests__/e2e/CodeBlock.spec.mjs | 60 ++++++++++++++----- .../__tests__/e2e/Tab.spec.mjs | 8 ++- .../src/themes/PlaygroundEditorTheme.css | 44 ++++++++++++++ .../src/themes/PlaygroundEditorTheme.ts | 1 + packages/lexical/src/LexicalEditor.ts | 1 + packages/lexical/src/nodes/LexicalTabNode.ts | 14 ++++- 6 files changed, 109 insertions(+), 19 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs index 34dd9972778..dab99d16c5d 100644 --- a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs @@ -366,7 +366,9 @@ test.describe('CodeBlock', () => { ;
- + @@ -393,7 +395,9 @@ test.describe('CodeBlock', () => { ;
- + @@ -453,7 +457,9 @@ test.describe('CodeBlock', () => { {
- + @@ -501,8 +507,12 @@ test.describe('CodeBlock', () => { data-gutter="123" data-highlight-language="javascript" data-language="javascript"> - - + + @@ -527,9 +537,15 @@ test.describe('CodeBlock', () => { {
- - - + + + @@ -551,8 +567,12 @@ test.describe('CodeBlock', () => { ;
- - + + @@ -575,7 +595,9 @@ test.describe('CodeBlock', () => { data-gutter="123" data-highlight-language="javascript" data-language="javascript"> - + @@ -600,8 +622,12 @@ test.describe('CodeBlock', () => { {
- - + + @@ -623,7 +649,9 @@ test.describe('CodeBlock', () => { ;
- + @@ -913,10 +941,10 @@ test.describe('CodeBlock', () => { data-gutter="12" data-language="javascript" data-highlight-language="javascript"> - + a b
- + c d `, diff --git a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs index 118e39536d4..b9198e193de 100644 --- a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs @@ -81,7 +81,9 @@ test.describe('Tab', () => { dir="ltr" style="padding-inline-start: calc(40px)"> すし - + すし

`, @@ -106,7 +108,9 @@ test.describe('Tab', () => { data-gutter="1" data-highlight-language="javascript" data-language="javascript"> - + diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 527085b7539..031173135f9 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -57,12 +57,56 @@ .PlaygroundEditorTheme__textUnderline { text-decoration: underline; } + .PlaygroundEditorTheme__textStrikethrough { text-decoration: line-through; } + .PlaygroundEditorTheme__textUnderlineStrikethrough { text-decoration: underline line-through; } + +.PlaygroundEditorTheme__tabNode { + position: relative; + text-decoration: none; +} + +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textUnderline::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0.15em; + border-bottom: 0.1em solid currentColor; +} + +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textStrikethrough::before { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 0.69em; + border-top: 0.1em solid currentColor; +} + +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textUnderlineStrikethrough::before, +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textUnderlineStrikethrough::after { + content: ''; + position: absolute; + left: 0; + right: 0; +} + +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textUnderlineStrikethrough::before { + top: 0.69em; + border-top: 0.1em solid currentColor; +} + +.PlaygroundEditorTheme__tabNode.PlaygroundEditorTheme__textUnderlineStrikethrough::after { + bottom: 0.05em; + border-bottom: 0.1em solid currentColor; +} + .PlaygroundEditorTheme__textSubscript { font-size: 0.8em; vertical-align: sub !important; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index 0b45916782b..e473ada671c 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -91,6 +91,7 @@ const theme: EditorThemeClasses = { quote: 'PlaygroundEditorTheme__quote', rtl: 'PlaygroundEditorTheme__rtl', specialText: 'PlaygroundEditorTheme__specialText', + tab: 'PlaygroundEditorTheme__tabNode', table: 'PlaygroundEditorTheme__table', tableCell: 'PlaygroundEditorTheme__tableCell', tableCellActionButton: 'PlaygroundEditorTheme__tableCellActionButton', diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 1a67571c1a0..dcc91e85658 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -131,6 +131,7 @@ export type EditorThemeClasses = { quote?: EditorThemeClassName; root?: EditorThemeClassName; rtl?: EditorThemeClassName; + tab?: EditorThemeClassName; table?: EditorThemeClassName; tableAddColumns?: EditorThemeClassName; tableAddRows?: EditorThemeClassName; diff --git a/packages/lexical/src/nodes/LexicalTabNode.ts b/packages/lexical/src/nodes/LexicalTabNode.ts index d3182e40df0..8c5999b33c2 100644 --- a/packages/lexical/src/nodes/LexicalTabNode.ts +++ b/packages/lexical/src/nodes/LexicalTabNode.ts @@ -11,8 +11,9 @@ import type {DOMConversionMap, NodeKey} from '../LexicalNode'; import invariant from 'shared/invariant'; import {IS_UNMERGEABLE} from '../LexicalConstants'; +import {EditorConfig} from '../LexicalEditor'; import {LexicalNode} from '../LexicalNode'; -import {$applyNodeReplacement} from '../LexicalUtils'; +import {$applyNodeReplacement, getCachedClassNameArray} from '../LexicalUtils'; import { SerializedTextNode, TextDetailType, @@ -47,6 +48,17 @@ export class TabNode extends TextNode { return null; } + createDOM(config: EditorConfig): HTMLElement { + const dom = super.createDOM(config); + const classNames = getCachedClassNameArray(config.theme, 'tab'); + + if (classNames !== undefined) { + const domClassList = dom.classList; + domClassList.add(...classNames); + } + return dom; + } + static importJSON(serializedTabNode: SerializedTabNode): TabNode { const node = $createTabNode(); node.setFormat(serializedTabNode.format); From dba254081e59ae7f49e3b3a838f6f8958381cc2e Mon Sep 17 00:00:00 2001 From: Syed Umar Anis Date: Wed, 11 Dec 2024 03:55:40 +1100 Subject: [PATCH 10/31] [lexical-playground] Refactor: editor styles should in PlaygroundEditorTheme.css (#6934) --- packages/lexical-playground/src/index.css | 22 ------------------- .../src/themes/PlaygroundEditorTheme.css | 19 ++++++++++++++++ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 87fb8fdbbff..8ad3159aeeb 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -1766,28 +1766,6 @@ button.item.dropdown-item-active i { z-index: 3; } -.PlaygroundEditorTheme__blockCursor { - display: block; - pointer-events: none; - position: absolute; -} - -.PlaygroundEditorTheme__blockCursor:after { - content: ''; - display: block; - position: absolute; - top: -2px; - width: 20px; - border-top: 1px solid black; - animation: CursorBlink 1.1s steps(2, start) infinite; -} - -@keyframes CursorBlink { - to { - visibility: hidden; - } -} - .dialog-dropdown { background-color: #eee !important; margin-bottom: 10px; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 031173135f9..2e985c4dc8d 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -133,6 +133,25 @@ text-decoration: underline; cursor: pointer; } +.PlaygroundEditorTheme__blockCursor { + display: block; + pointer-events: none; + position: absolute; +} +.PlaygroundEditorTheme__blockCursor:after { + content: ''; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: CursorBlink 1.1s steps(2, start) infinite; +} +@keyframes CursorBlink { + to { + visibility: hidden; + } +} .PlaygroundEditorTheme__code { background-color: rgb(240, 242, 245); font-family: Menlo, Consolas, Monaco, monospace; From 42bd45d7e71bff9a2fe44aff18f50be3b3db1a14 Mon Sep 17 00:00:00 2001 From: lin-mt Date: Wed, 11 Dec 2024 00:58:35 +0800 Subject: [PATCH 11/31] [lexical-react]Bug Fix: the location of draggable-block-menu cannot be calculated #6818 (#6915) Co-authored-by: Bob Ippolito --- .../lexical-react/src/LexicalDraggableBlockPlugin.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx index d37bd69f3c9..ef7303ee281 100644 --- a/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx +++ b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx @@ -191,9 +191,15 @@ function setMenuPosition( const floatingElemRect = floatingElem.getBoundingClientRect(); const anchorElementRect = anchorElem.getBoundingClientRect(); + // top left + let targetCalculateHeight: number = parseInt(targetStyle.lineHeight, 10); + if (isNaN(targetCalculateHeight)) { + // middle + targetCalculateHeight = targetRect.bottom - targetRect.top; + } const top = targetRect.top + - (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - + (targetCalculateHeight - floatingElemRect.height) / 2 - anchorElementRect.top; const left = SPACE; From 3ba07059a909ea497113f1b1c905065cbd946f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandr=20Lapu=C5=A1kin?= Date: Tue, 10 Dec 2024 19:42:50 +0200 Subject: [PATCH 12/31] [lexical-list] Bug Fix: Ensure new paragraph node retains selection styling when exiting list (#6917) Co-authored-by: Aleksandr Lapuskin Co-authored-by: Bob Ippolito --- .../lexical-list/src/LexicalListItemNode.ts | 2 +- packages/lexical-list/src/formatList.ts | 16 +++++++--------- .../__tests__/e2e/List.spec.mjs | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index 5581aba730b..252c03704d0 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -382,7 +382,7 @@ export class ListItemNode extends ParagraphNode { } canMergeWith(node: LexicalNode): boolean { - return $isParagraphNode(node) || $isListItemNode(node); + return $isListItemNode(node) || $isParagraphNode(node); } extractWithChild(child: LexicalNode, selection: BaseSelection): boolean { diff --git a/packages/lexical-list/src/formatList.ts b/packages/lexical-list/src/formatList.ts index 83172275908..469d623590a 100644 --- a/packages/lexical-list/src/formatList.ts +++ b/packages/lexical-list/src/formatList.ts @@ -12,7 +12,6 @@ import { $getSelection, $isElementNode, $isLeafNode, - $isParagraphNode, $isRangeSelection, $isRootOrShadowRoot, ElementNode, @@ -494,10 +493,12 @@ export function $handleListInsertParagraph(): boolean { ); const grandparent = parent.getParent(); - let replacementNode: ElementNode; + let replacementNode: ParagraphNode | ListItemNode; if ($isRootOrShadowRoot(grandparent)) { replacementNode = $createParagraphNode(); + replacementNode.setTextStyle(selection.style); + replacementNode.setTextFormat(selection.format); topListNode.insertAfter(replacementNode); } else if ($isListItemNode(grandparent)) { replacementNode = $createListItemNode(); @@ -511,17 +512,14 @@ export function $handleListInsertParagraph(): boolean { const nextSiblings = anchor.getNextSiblings(); if (nextSiblings.length > 0) { const newList = $createListNode(parent.getListType()); - if ($isParagraphNode(replacementNode)) { - replacementNode.insertAfter(newList); - } else { + if ($isListItemNode(replacementNode)) { const newListItem = $createListItemNode(); newListItem.append(newList); replacementNode.insertAfter(newListItem); + } else { + replacementNode.insertAfter(newList); } - nextSiblings.forEach((sibling) => { - sibling.remove(); - newList.append(sibling); - }); + newList.append(...nextSiblings); } // Don't leave hanging nested empty lists $removeHighestEmptyListParent(anchor); diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index e1173a1f66f..d29495ee708 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -32,6 +32,7 @@ import { pasteFromClipboard, repeat, selectFromAlignDropdown, + selectFromColorPicker, selectFromFormatDropdown, test, waitForSelector, @@ -160,6 +161,24 @@ test.describe.parallel('Nested List', () => { ); }); + test('Should retain selection style when exiting list', async ({page}) => { + await focusEditor(page); + await toggleBulletList(page); + + await selectFromColorPicker(page); + await toggleBold(page); + await page.keyboard.type('Hello'); + //Double-enter to exit list + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.type('World'); + + await assertHTML( + page, + '
  • Hello

World

', + ); + }); + test(`Can indent/outdent mutliple list nodes in a list with multiple levels of indentation`, async ({ page, }) => { From 28c33d64d5a645010bb47062521f5b02d1ed6fcc Mon Sep 17 00:00:00 2001 From: Vinay Kushwaha <51132814+iamvinayvk@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:54:49 +0530 Subject: [PATCH 13/31] [lexical-table] Fix: Delete table row in merge cells (#6922) --- .../__tests__/e2e/Tables.spec.mjs | 443 ++++++++++++++++++ .../lexical-table/src/LexicalTableUtils.ts | 24 +- 2 files changed, 463 insertions(+), 4 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 1cf4e25a7b1..1b1ba63cd8b 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -4231,4 +4231,447 @@ test.describe.parallel('Tables', () => { `, ); }); + + test('Can delete table row when previous cell is a merged cell', async ({ + page, + isCollab, + isPlainText, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + + await focusEditor(page); + + await insertTable(page, 5, 5); + + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 1, y: 3}, + false, + false, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 1, y: 2}, + {x: 2, y: 4}, + false, + false, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


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


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }); + + test('Can delete table row when siblings are merged cell', async ({ + page, + isCollab, + isPlainText, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + + await focusEditor(page); + + await insertTable(page, 5, 5); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 3}, + true, + true, + ); + await mergeTableCells(page); + await selectCellsFromTableCords( + page, + {x: 2, y: 0}, + {x: 1, y: 2}, + true, + false, + ); + await mergeTableCells(page); + await assertHTML( + page, + html` +


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + + await selectCellsFromTableCords( + page, + {x: 0, y: 2}, + {x: 0, y: 2}, + false, + false, + ); + + await deleteTableRows(page); + + await assertHTML( + page, + html` +


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }); }); diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index e1c0c0884cd..6c354285ec4 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -548,6 +548,7 @@ export function $deleteTableRow__EXPERIMENTAL(): void { return; } const columnCount = gridMap[0].length; + const selectedRowCount = anchorCell.__rowSpan; const nextRow = gridMap[focusEndRow + 1]; const nextRowNode: null | TableRowNode = grid.getChildAtIndex( focusEndRow + 1, @@ -565,7 +566,11 @@ export function $deleteTableRow__EXPERIMENTAL(): void { } // Rows overflowing top have to be trimmed if (row === anchorStartRow && cellStartRow < anchorStartRow) { - cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow)); + const overflowTop = anchorStartRow - cellStartRow; + cell.setRowSpan( + cell.__rowSpan - + Math.min(selectedRowCount, cell.__rowSpan - overflowTop), + ); } // Rows overflowing bottom have to be trimmed and moved to the next row if ( @@ -574,11 +579,22 @@ export function $deleteTableRow__EXPERIMENTAL(): void { ) { cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1)); invariant(nextRowNode !== null, 'Expected nextRowNode not to be null'); - if (column === 0) { + let insertAfterCell: null | TableCellNode = null; + for (let columnIndex = 0; columnIndex < column; columnIndex++) { + const currentCellMap = nextRow[columnIndex]; + const currentCell = currentCellMap.cell; + // Checking the cell having startRow as same as nextRow + if (currentCellMap.startRow === row + 1) { + insertAfterCell = currentCell; + } + if (currentCell.__colSpan > 1) { + columnIndex += currentCell.__colSpan - 1; + } + } + if (insertAfterCell === null) { $insertFirst(nextRowNode, cell); } else { - const {cell: previousCell} = nextRow[column - 1]; - previousCell.insertAfter(cell); + insertAfterCell.insertAfter(cell); } } } From 6243c4b442b19e92aa6f9dd8c2cc629ba9460028 Mon Sep 17 00:00:00 2001 From: Luis Silva Date: Wed, 11 Dec 2024 19:29:02 +0000 Subject: [PATCH 14/31] [scripts-integration-fixtures] Address GitHub detected a vulnerability in the @sveltejs/kit dependency (#6943) Co-authored-by: Luis Fetzner da Silva --- .../package-lock.json | 812 +++++++++--------- .../package.json | 2 +- 2 files changed, 406 insertions(+), 408 deletions(-) diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package-lock.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package-lock.json index 113ad945203..c0bf25d4f21 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package-lock.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package-lock.json @@ -1,24 +1,24 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.17.1", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lexical-sveltekit-vanilla-js", - "version": "0.17.1", + "version": "0.21.0", "devDependencies": { - "@lexical/dragon": "0.17.1", - "@lexical/history": "0.17.1", - "@lexical/rich-text": "0.17.1", - "@lexical/utils": "0.17.1", + "@lexical/dragon": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/utils": "0.21.0", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.10.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.17.1", + "lexical": "0.21.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", @@ -41,9 +41,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -57,9 +57,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -73,9 +73,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -89,9 +89,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -105,9 +105,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -121,9 +121,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -137,9 +137,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -169,9 +169,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -185,9 +185,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -201,9 +201,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -217,9 +217,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -233,9 +233,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -249,9 +249,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -265,9 +265,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -281,9 +281,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -297,9 +297,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -313,9 +313,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -329,9 +329,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -345,9 +345,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -361,9 +361,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -377,9 +377,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -393,9 +393,9 @@ } }, "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==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -457,108 +457,100 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.17.1.tgz", - "integrity": "sha512-OVqnEfWX8XN5xxuMPo6BfgGKHREbz++D5V5ISOiml0Z8fV/TQkdgwqbBJcUdJHGRHWSUwdK7CWGs/VALvVvZyw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.21.0.tgz", + "integrity": "sha512-3lNMlMeUob9fcnRXGVieV/lmPbmet/SVWckNTOwzfKrZ/YW5HiiyJrWviLRVf50dGXTbmBGt7K/2pfPYvWCHFA==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/html": "0.17.1", - "@lexical/list": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/html": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/dragon": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.17.1.tgz", - "integrity": "sha512-lhBRKP7RlhiVCLtF0qiNqmMhEO6cQB43sMe7d4bvuY1G2++oKY/XAJPg6QJZdXRrCGRQ6vZ26QRNhRPmCxL5Ng==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.21.0.tgz", + "integrity": "sha512-ahTCaOtRFNauEzplN1qVuPjyGAlDd+XcVM5FQCdxVh/1DvqmBxEJRVuCBqatzUUVb89jRBekYUcEdnY9iNjvEQ==", "dev": true, - "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.21.0" } }, "node_modules/@lexical/history": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.17.1.tgz", - "integrity": "sha512-OU/ohajz4FXchUhghsWC7xeBPypFe50FCm5OePwo767G7P233IztgRKIng2pTT4zhCPW7S6Mfl53JoFHKehpWA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.21.0.tgz", + "integrity": "sha512-Sv2sici2NnAfHYHYRSjjS139MDT8fHP6PlYM2hVr+17dOg7/fJl22VBLRgQ7/+jLtAPxQjID69jvaMlOvt4Oog==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/html": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.17.1.tgz", - "integrity": "sha512-yGG+K2DXl7Wn2DpNuZ0Y3uCHJgfHkJN3/MmnFb4jLnH1FoJJiuy7WJb/BRRh9H+6xBJ9v70iv+kttDJ0u1xp5w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.21.0.tgz", + "integrity": "sha512-UGahVsGz8OD7Ya39qwquE+JPStTxCw/uaQrnUNorCM7owtPidO2H+tsilAB3A1GK3ksFGdHeEjBjG0Gf7gOg+Q==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/list": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.17.1.tgz", - "integrity": "sha512-k9ZnmQuBvW+xVUtWJZwoGtiVG2cy+hxzkLGU4jTq1sqxRIoSeGcjvhFAK8JSEj4i21SgkB1FmkWXoYK5kbwtRA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.21.0.tgz", + "integrity": "sha512-WItGlwwNJCS8b6SO1QPKzArShmD+OXQkLbhBcAh+EfpnkvmCW5T5LqY+OfIRmEN1dhDOnwqCY7mXkivWO8o5tw==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/rich-text": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.17.1.tgz", - "integrity": "sha512-T3kvj4P1OpedX9jvxN3WN8NP1Khol6mCW2ScFIRNRz2dsXgyN00thH1Q1J/uyu7aKyGS7rzcY0rb1Pz1qFufqQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.21.0.tgz", + "integrity": "sha512-+pvEKUneEkGfWOSTl9jU58N9knePilMLxxOtppCAcgnaCdilOh3n5YyRppXhvmprUe0JaTseCMoik2LP51G/JA==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/selection": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.17.1.tgz", - "integrity": "sha512-qBKVn+lMV2YIoyRELNr1/QssXx/4c0id9NCB/BOuYlG8du5IjviVJquEF56NEv2t0GedDv4BpUwkhXT2QbNAxA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.21.0.tgz", + "integrity": "sha512-4u53bc8zlPPF0rnHjsGQExQ1St8NafsDd70/t1FMw7yvoMtUsKdH7+ap00esLkJOMv45unJD7UOzKRqU1X0sEA==", "dev": true, - "license": "MIT", "dependencies": { - "lexical": "0.17.1" + "lexical": "0.21.0" } }, "node_modules/@lexical/table": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.17.1.tgz", - "integrity": "sha512-2fUYPmxhyuMQX3MRvSsNaxbgvwGNJpHaKx1Ldc+PT2MvDZ6ALZkfsxbi0do54Q3i7dOon8/avRp4TuVaCnqvoA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.21.0.tgz", + "integrity": "sha512-JhylAWcf4qKD4FmxMUt3YzH5zg2+baBr4+/haLZL7178hMvUzJwGIiWk+3hD3phzmW3WrP49uFXzM7DMSCkE8w==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/utils": "0.17.1", - "lexical": "0.17.1" + "@lexical/clipboard": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/utils": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.17.1.tgz", - "integrity": "sha512-jCQER5EsvhLNxKH3qgcpdWj/necUb82Xjp8qWQ3c0tyL07hIRm2tDRA/s9mQmvcP855HEZSmGVmR5SKtkcEAVg==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.21.0.tgz", + "integrity": "sha512-YzsNOAiLkCy6R3DuP18gtseDrzgx+30lFyqRvp5M7mckeYgQElwdfG5biNFDLv7BM9GjSzgU5Cunjycsx6Sjqg==", "dev": true, - "license": "MIT", "dependencies": { - "@lexical/list": "0.17.1", - "@lexical/selection": "0.17.1", - "@lexical/table": "0.17.1", - "lexical": "0.17.1" + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@playwright/test": { @@ -577,26 +569,27 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true }, "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", - "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^8.0.3", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0 || 14 >= 14.17" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" @@ -607,6 +600,32 @@ } } }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", @@ -628,15 +647,14 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -675,9 +693,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], @@ -688,9 +706,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], @@ -701,9 +719,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], @@ -714,9 +732,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], @@ -726,10 +744,49 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], @@ -740,9 +797,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], @@ -753,9 +810,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], @@ -765,10 +822,36 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], @@ -778,10 +861,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], @@ -792,9 +888,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], @@ -805,9 +901,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], @@ -818,9 +914,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], @@ -831,9 +927,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], @@ -844,26 +940,26 @@ ] }, "node_modules/@sveltejs/adapter-auto": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.1.1.tgz", - "integrity": "sha512-6LeZft2Fo/4HfmLBi5CucMYmgRxgcETweQl/yQoZo/895K3S9YWYN4Sfm/IhwlIpbJp3QNvhKmwCHbsqQNYQpw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", + "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", "dev": true, "dependencies": { - "import-meta-resolve": "^4.0.0" + "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "node_modules/@sveltejs/adapter-node": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.0.1.tgz", - "integrity": "sha512-eYdmxdUWMW+dad1JfMsWBPY2vjXz9eE+52A2AQnXPScPJlIxIVk5mmbaEEzrZivLfO2wEcLTZ5vdC03W69x+iA==", + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.9.tgz", + "integrity": "sha512-51euNrx0AcaTu8//wDfVh7xmqQSVgU52rfinE/MwvGkJa4nHPJMHmzv6+OIpmxg7gZaF6+5NVlxnieCzxLD59g==", "dev": true, "dependencies": { - "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-node-resolve": "^15.3.0", "rollup": "^4.9.5" }, "peerDependencies": { @@ -871,32 +967,32 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz", - "integrity": "sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz", + "integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==", "dev": true, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "node_modules/@sveltejs/kit": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.4.tgz", - "integrity": "sha512-eDxK2d4EGzk99QsZNoPXe7jlzA5EGqfcCpUwZ912bhnalsZ2ZsG5wGRthkydupVjYyqdmzEanVKFhLxU2vkPSQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.10.1.tgz", + "integrity": "sha512-2aormKTn94aU8Lfxj4gcbRGh1Dyw0hCFlNo51+njdRDn9P2ERuWC4bOtTuoy5HJpPYR3AH8oaaEjKDWUHbi1OA==", "dev": true, "hasInstallScript": true, "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", - "devalue": "^4.3.2", - "esm-env": "^1.0.0", - "import-meta-resolve": "^4.0.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.1", + "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.4", + "sirv": "^3.0.0", "tiny-glob": "^0.2.9" }, "bin": { @@ -906,9 +1002,9 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "vite": "^5.0.3 || ^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { @@ -957,9 +1053,9 @@ "dev": true }, "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==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/resolve": { @@ -998,33 +1094,6 @@ "dequal": "^2.0.3" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1111,15 +1180,15 @@ } }, "node_modules/devalue": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", - "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "dev": true }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -1129,35 +1198,35 @@ "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" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", + "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", "dev": true }, "node_modules/estree-walker": { @@ -1166,12 +1235,6 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1195,37 +1258,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", @@ -1251,53 +1283,25 @@ } }, "node_modules/import-meta-resolve": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", - "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "builtin-modules": "^3.3.0" + "hasown": "^2.0.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1328,11 +1332,10 @@ } }, "node_modules/lexical": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.17.1.tgz", - "integrity": "sha512-72/MhR7jqmyqD10bmJw8gztlCm4KDDT+TPtU4elqXrEvHoO5XENi34YAEUD9gIkPfqSwyLa9mwAX1nKzIr5xEA==", - "dev": true, - "license": "MIT" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.21.0.tgz", + "integrity": "sha512-Dxc5SCG4kB+wF+Rh55ism3SuecOKeOtCtGHFGKd6pj2QKVojtjkxGTQPMt7//2z5rMSue4R+hmRM0pCEZflupA==", + "dev": true }, "node_modules/locate-character": { "version": "3.0.0", @@ -1383,9 +1386,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -1400,15 +1403,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1445,9 +1439,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -1493,9 +1487,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -1513,8 +1507,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1563,12 +1557,12 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1578,19 +1572,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, @@ -1613,9 +1613,9 @@ "dev": true }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -1623,13 +1623,13 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "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==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1742,14 +1742,14 @@ } }, "node_modules/vite": { - "version": "5.2.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", - "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -1768,6 +1768,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -1785,6 +1786,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1823,12 +1827,6 @@ "optional": true } } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true } } } 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 79ae305e4b2..acb21924aa5 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 @@ -17,7 +17,7 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.10.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", "lexical": "0.21.0", "prettier": "^3.1.1", From 61e17c8489968e821148890a8d132c76e97ef3cc Mon Sep 17 00:00:00 2001 From: Sherry Date: Wed, 11 Dec 2024 23:36:58 -0800 Subject: [PATCH 15/31] [lexical-list] Revert PR 6912 (#6944) --- .../lexical-list/src/LexicalListItemNode.ts | 95 ++++++++++--------- packages/lexical-list/src/formatList.ts | 8 +- packages/lexical-list/src/utils.ts | 3 +- .../__tests__/e2e/List.spec.mjs | 44 --------- .../src/__tests__/unit/LexicalEditor.test.tsx | 2 +- .../unit/LexicalSerialization.test.ts | 4 +- .../lexical/src/nodes/LexicalParagraphNode.ts | 13 +-- 7 files changed, 66 insertions(+), 103 deletions(-) diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index 252c03704d0..9dade3ae0a3 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -7,6 +7,20 @@ */ import type {ListNode, ListType} from './'; +import type { + BaseSelection, + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + EditorThemeClasses, + LexicalNode, + NodeKey, + ParagraphNode, + RangeSelection, + SerializedElementNode, + Spread, +} from 'lexical'; import { addClassNamesToElement, @@ -15,24 +29,11 @@ import { import { $applyNodeReplacement, $createParagraphNode, - $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection, - BaseSelection, - DOMConversionMap, - DOMConversionOutput, - DOMExportOutput, - EditorConfig, - EditorThemeClasses, ElementNode, LexicalEditor, - LexicalNode, - NodeKey, - ParagraphNode, - RangeSelection, - SerializedParagraphNode, - Spread, } from 'lexical'; import invariant from 'shared/invariant'; import normalizeClassNames from 'shared/normalizeClassNames'; @@ -46,11 +47,11 @@ export type SerializedListItemNode = Spread< checked: boolean | undefined; value: number; }, - SerializedParagraphNode + SerializedElementNode >; /** @noInheritDoc */ -export class ListItemNode extends ParagraphNode { +export class ListItemNode extends ElementNode { /** @internal */ __value: number; /** @internal */ @@ -80,11 +81,12 @@ export class ListItemNode extends ParagraphNode { $setListItemThemeClassNames(element, config.theme, this); return element; } - updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { - if (super.updateDOM(prevNode, dom, config)) { - return true; - } + updateDOM( + prevNode: ListItemNode, + dom: HTMLElement, + config: EditorConfig, + ): boolean { const parent = this.getParent(); if ($isListNode(parent) && parent.getListType() === 'check') { updateListItemChecked(dom, this, prevNode, parent); @@ -92,6 +94,7 @@ export class ListItemNode extends ParagraphNode { // @ts-expect-error - this is always HTMLListItemElement dom.value = this.__value; $setListItemThemeClassNames(dom, config.theme, this); + return false; } @@ -125,12 +128,6 @@ export class ListItemNode extends ParagraphNode { node.setValue(serializedNode.value); node.setFormat(serializedNode.format); node.setDirection(serializedNode.direction); - if (typeof serializedNode.textFormat === 'number') { - node.setTextFormat(serializedNode.textFormat); - } - if (typeof serializedNode.textStyle === 'string') { - node.setTextStyle(serializedNode.textStyle); - } return node; } @@ -227,11 +224,15 @@ export class ListItemNode extends ParagraphNode { } const siblings = this.getNextSiblings(); + + // Split the lists and insert the node in between them listNode.insertAfter(node, restoreSelection); if (siblings.length !== 0) { const newListNode = $createListNode(listNode.getListType()); + siblings.forEach((sibling) => newListNode.append(sibling)); + node.insertAfter(newListNode, restoreSelection); } @@ -255,49 +256,51 @@ export class ListItemNode extends ParagraphNode { } insertNewAfter( - selection: RangeSelection, + _: RangeSelection, restoreSelection = true, ): ListItemNode | ParagraphNode { const newElement = $createListItemNode( this.__checked == null ? undefined : false, ); - - const format = selection.format; - newElement.setTextFormat(format); - - newElement.setFormat(this.getFormatType()); this.insertAfter(newElement, restoreSelection); return newElement; } - collapseAtStart(): boolean { - const selection = $getSelection(); - - if (!$isRangeSelection(selection)) { - return false; - } - + collapseAtStart(selection: RangeSelection): true { const paragraph = $createParagraphNode(); const children = this.getChildren(); children.forEach((child) => paragraph.append(child)); - const listNode = this.getParentOrThrow(); - const listNodeParent = listNode.getParent(); - - if (!$isListNode(listNode)) { - return false; - } + const listNodeParent = listNode.getParentOrThrow(); + const isIndented = $isListItemNode(listNodeParent); if (listNode.getChildrenSize() === 1) { - if ($isListItemNode(listNodeParent)) { + if (isIndented) { + // if the list node is nested, we just want to remove it, + // effectively unindenting it. listNode.remove(); listNodeParent.select(); } else { listNode.insertBefore(paragraph); listNode.remove(); - paragraph.select(); + // If we have selection on the list item, we'll need to move it + // to the paragraph + const anchor = selection.anchor; + const focus = selection.focus; + const key = paragraph.getKey(); + + if (anchor.type === 'element' && anchor.getNode().is(this)) { + anchor.set(key, anchor.offset, 'element'); + } + + if (focus.type === 'element' && focus.getNode().is(this)) { + focus.set(key, focus.offset, 'element'); + } } + } else { + listNode.insertBefore(paragraph); + this.remove(); } return true; diff --git a/packages/lexical-list/src/formatList.ts b/packages/lexical-list/src/formatList.ts index 469d623590a..46694253ebb 100644 --- a/packages/lexical-list/src/formatList.ts +++ b/packages/lexical-list/src/formatList.ts @@ -242,6 +242,7 @@ export function removeList(editor: LexicalEditor): void { if ($isLeafNode(node)) { const listItemNode = $getNearestNodeOfType(node, ListItemNode); + if (listItemNode != null) { listNodes.add($getTopListNode(listItemNode)); } @@ -477,13 +478,11 @@ export function $handleListInsertParagraph(): boolean { return false; } // Only run this code on empty list items - const anchor = selection.anchor.getNode(); if (!$isListItemNode(anchor) || anchor.getChildrenSize() !== 0) { return false; } - const topListNode = $getTopListNode(anchor); const parent = anchor.getParent(); @@ -493,6 +492,7 @@ export function $handleListInsertParagraph(): boolean { ); const grandparent = parent.getParent(); + let replacementNode: ParagraphNode | ListItemNode; if ($isRootOrShadowRoot(grandparent)) { @@ -506,10 +506,10 @@ export function $handleListInsertParagraph(): boolean { } else { return false; } - replacementNode.select(); const nextSiblings = anchor.getNextSiblings(); + if (nextSiblings.length > 0) { const newList = $createListNode(parent.getListType()); if ($isListItemNode(replacementNode)) { @@ -521,7 +521,9 @@ export function $handleListInsertParagraph(): boolean { } newList.append(...nextSiblings); } + // Don't leave hanging nested empty lists $removeHighestEmptyListParent(anchor); + return true; } diff --git a/packages/lexical-list/src/utils.ts b/packages/lexical-list/src/utils.ts index 9b443f76205..9c9b1bf9af1 100644 --- a/packages/lexical-list/src/utils.ts +++ b/packages/lexical-list/src/utils.ts @@ -6,8 +6,9 @@ * */ +import type {LexicalNode, Spread} from 'lexical'; + import {$findMatchingParent} from '@lexical/utils'; -import {type LexicalNode, type Spread} from 'lexical'; import invariant from 'shared/invariant'; import { diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index d29495ee708..df81baee7a2 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -1901,48 +1901,4 @@ test.describe.parallel('Nested List', () => { }); }, ); - test('new list item should preserve format from previous list item even after new list item is indented', async ({ - page, - }) => { - await focusEditor(page); - await toggleBulletList(page); - await toggleBold(page); - await page.keyboard.type('MLH Fellowship'); - await page.keyboard.press('Enter'); - await clickIndentButton(page); - await page.keyboard.type('Fall 2024'); - await assertHTML( - page, - html` -
    -
  • - - MLH Fellowship - -
  • -
  • -
      -
    • - - Fall 2024 - -
    • -
    -
  • -
- `, - ); - }); }); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 7f7be6d9c5f..3986f27806f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -1026,7 +1026,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,"textFormat":0,"textStyle":""}],"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); diff --git a/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts b/packages/lexical/src/__tests__/unit/LexicalSerialization.test.ts index 96820722dcd..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,"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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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}}`; + 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,"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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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,"textFormat":0,"textStyle":"","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}}`, + `{"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/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index 036799b3b71..c1250aeae16 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -120,7 +120,11 @@ export class ParagraphNode extends ElementNode { } return dom; } - updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { + updateDOM( + prevNode: ParagraphNode, + dom: HTMLElement, + config: EditorConfig, + ): boolean { return false; } @@ -160,9 +164,7 @@ export class ParagraphNode extends ElementNode { node.setFormat(serializedNode.format); node.setIndent(serializedNode.indent); node.setDirection(serializedNode.direction); - if (typeof serializedNode.textFormat === 'number') { - node.setTextFormat(serializedNode.textFormat); - } + node.setTextFormat(serializedNode.textFormat); return node; } @@ -188,8 +190,7 @@ export class ParagraphNode extends ElementNode { const direction = this.getDirection(); newElement.setDirection(direction); newElement.setFormat(this.getFormatType()); - newElement.setStyle(this.getStyle()); - + newElement.setStyle(this.getTextStyle()); this.insertAfter(newElement, restoreSelection); return newElement; } From f49f068b96c218d92d1ab719ffeebb5346707304 Mon Sep 17 00:00:00 2001 From: "Niels Y." Date: Thu, 12 Dec 2024 11:38:10 -0800 Subject: [PATCH 16/31] [lexical-onboarding] testing sev mitigation (#6952) --- packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow index d7ce0ef3f68..c6b84a047c2 100644 --- a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow +++ b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow @@ -40,3 +40,5 @@ declare export function registerLexicalCommandLogger( declare export function useLexicalCommandsLog( editor: LexicalEditor, ): LexicalCommandLog; + +// test comment \ No newline at end of file From f989d198043980ad62fb4ca2e55166a9ac0f87f5 Mon Sep 17 00:00:00 2001 From: bailey-meta Date: Thu, 12 Dec 2024 11:42:59 -0800 Subject: [PATCH 17/31] Test comment for pr testing (#6953) Co-authored-by: bailey-meta Co-authored-by: Niels Y. --- packages/lexical-dragon/flow/LexicalDragon.js.flow | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lexical-dragon/flow/LexicalDragon.js.flow b/packages/lexical-dragon/flow/LexicalDragon.js.flow index 179ab470c54..2db9d522ecc 100644 --- a/packages/lexical-dragon/flow/LexicalDragon.js.flow +++ b/packages/lexical-dragon/flow/LexicalDragon.js.flow @@ -11,3 +11,5 @@ import {$getSelection, $isRangeSelection, $isTextNode} from 'lexical'; declare export function registerDragonSupport( editor: LexicalEditor, ): () => void; + +// test comment for pr testing From b2d8c697d8987617a9e47ecc05b858a627104c8b Mon Sep 17 00:00:00 2001 From: Tranquiliz00 Date: Thu, 12 Dec 2024 12:08:43 -0800 Subject: [PATCH 18/31] Create a test PR (#6955) Co-authored-by: Niels Y. --- packages/lexical-utils/flow/LexicalUtils.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 958dd8acfa7..692edf9116c 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -135,4 +135,4 @@ declare export function $firstToLastIterator(node: ElementNode): Iterable; -declare export function $unwrapNode(node: ElementNode): void; +declare export function $unwrapNode(node: ElementNode): void; \ No newline at end of file From 89ca73099fe9cded1955d2d2ea7a17df8b11915e Mon Sep 17 00:00:00 2001 From: "Niels Y." Date: Thu, 12 Dec 2024 12:22:39 -0800 Subject: [PATCH 19/31] Revert "[lexical-onboarding] testing sev mitigation (#6952)" (#6956) --- packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow index c6b84a047c2..d7ce0ef3f68 100644 --- a/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow +++ b/packages/lexical-devtools-core/flow/LexicalDevtoolsCore.js.flow @@ -40,5 +40,3 @@ declare export function registerLexicalCommandLogger( declare export function useLexicalCommandsLog( editor: LexicalEditor, ): LexicalCommandLog; - -// test comment \ No newline at end of file From b72d8538a5c7984d648b3357e88c28886ce09d5e Mon Sep 17 00:00:00 2001 From: bailey-meta Date: Thu, 12 Dec 2024 12:22:54 -0800 Subject: [PATCH 20/31] Revert "Test comment for pr testing (#6953)" (#6957) Co-authored-by: bailey-meta --- packages/lexical-dragon/flow/LexicalDragon.js.flow | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/lexical-dragon/flow/LexicalDragon.js.flow b/packages/lexical-dragon/flow/LexicalDragon.js.flow index 2db9d522ecc..179ab470c54 100644 --- a/packages/lexical-dragon/flow/LexicalDragon.js.flow +++ b/packages/lexical-dragon/flow/LexicalDragon.js.flow @@ -11,5 +11,3 @@ import {$getSelection, $isRangeSelection, $isTextNode} from 'lexical'; declare export function registerDragonSupport( editor: LexicalEditor, ): () => void; - -// test comment for pr testing From 0f3ff1569ba3a8b1ed853d0278a44409f265f515 Mon Sep 17 00:00:00 2001 From: Tranquiliz00 Date: Thu, 12 Dec 2024 12:23:00 -0800 Subject: [PATCH 21/31] Test234 (#6958) --- packages/lexical-utils/flow/LexicalUtils.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-utils/flow/LexicalUtils.js.flow b/packages/lexical-utils/flow/LexicalUtils.js.flow index 692edf9116c..958dd8acfa7 100644 --- a/packages/lexical-utils/flow/LexicalUtils.js.flow +++ b/packages/lexical-utils/flow/LexicalUtils.js.flow @@ -135,4 +135,4 @@ declare export function $firstToLastIterator(node: ElementNode): Iterable; -declare export function $unwrapNode(node: ElementNode): void; \ No newline at end of file +declare export function $unwrapNode(node: ElementNode): void; From 66f805c41a9c72a4b92f8a5056938335c1b3d7b1 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 13 Dec 2024 10:31:26 -0800 Subject: [PATCH 22/31] [Breaking Change][lexical] Feature: New update tag: skip-dom-selection, $onUpdate now always called (#6894) --- .../lexical-code/flow/LexicalCode.js.flow | 7 -- .../lexical-code/src/CodeHighlightNode.ts | 6 +- packages/lexical-code/src/CodeNode.ts | 6 +- .../lexical-link/flow/LexicalLink.js.flow | 5 - packages/lexical-link/src/index.ts | 4 +- packages/lexical-list/src/LexicalListNode.ts | 6 +- packages/lexical-mark/src/MarkNode.ts | 2 +- .../flow/LexicalOverflow.js.flow | 1 - packages/lexical-overflow/src/index.ts | 2 +- .../__tests__/e2e/Autocomplete.spec.mjs | 35 ++++++- .../__tests__/e2e/Tables.spec.mjs | 4 +- .../src/nodes/AutocompleteNode.tsx | 18 ++-- .../src/nodes/EmojiNode.tsx | 6 +- .../src/nodes/EquationNode.tsx | 2 +- .../nodes/InlineImageNode/InlineImageNode.tsx | 6 +- .../src/nodes/LayoutContainerNode.ts | 2 +- .../src/nodes/SpecialTextNode.tsx | 6 +- .../CollapsibleContainerNode.ts | 5 +- .../CollapsibleContentNode.ts | 2 +- .../CollapsiblePlugin/CollapsibleTitleNode.ts | 2 +- .../flow/LexicalRichText.js.flow | 2 - packages/lexical-rich-text/src/index.ts | 4 +- .../lexical-table/flow/LexicalTable.js.flow | 4 - .../lexical-table/src/LexicalTableCellNode.ts | 2 +- .../lexical-table/src/LexicalTableNode.ts | 6 +- .../lexical-table/src/LexicalTableObserver.ts | 4 +- .../lexical-table/src/LexicalTableRowNode.ts | 2 +- packages/lexical-utils/src/markSelection.ts | 22 +++-- .../lexical-website/docs/concepts/commands.md | 6 ++ .../lexical-website/docs/concepts/nodes.md | 4 +- .../docs/concepts/selection.md | 52 ++++++++++- packages/lexical/flow/Lexical.js.flow | 10 +- packages/lexical/src/LexicalEditor.ts | 32 +++++-- packages/lexical/src/LexicalUpdates.ts | 4 +- packages/lexical/src/LexicalUtils.ts | 67 +++++++++----- .../src/__tests__/unit/LexicalEditor.test.tsx | 91 +++++++++++++++++++ .../src/__tests__/unit/LexicalUtils.test.ts | 30 ++++++ packages/lexical/src/index.ts | 1 + packages/lexical/src/nodes/LexicalRootNode.ts | 2 +- packages/lexical/src/nodes/LexicalTextNode.ts | 6 +- 40 files changed, 327 insertions(+), 151 deletions(-) diff --git a/packages/lexical-code/flow/LexicalCode.js.flow b/packages/lexical-code/flow/LexicalCode.js.flow index 9c5cf449acd..a3a0bac98f5 100644 --- a/packages/lexical-code/flow/LexicalCode.js.flow +++ b/packages/lexical-code/flow/LexicalCode.js.flow @@ -77,12 +77,6 @@ declare export class CodeHighlightNode extends TextNode { // $FlowFixMe static clone(node: CodeHighlightNode): CodeHighlightNode; createDOM(config: EditorConfig): HTMLElement; - updateDOM( - // $FlowFixMe - prevNode: CodeHighlightNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean; setFormat(format: number): this; } @@ -125,7 +119,6 @@ declare export class CodeNode extends ElementNode { static clone(node: CodeNode): CodeNode; constructor(language: ?string, key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: CodeNode, dom: HTMLElement): boolean; insertNewAfter( selection: RangeSelection, restoreSelection?: boolean, diff --git a/packages/lexical-code/src/CodeHighlightNode.ts b/packages/lexical-code/src/CodeHighlightNode.ts index 15f08d207ab..c9b43e486d3 100644 --- a/packages/lexical-code/src/CodeHighlightNode.ts +++ b/packages/lexical-code/src/CodeHighlightNode.ts @@ -136,11 +136,7 @@ export class CodeHighlightNode extends TextNode { return element; } - updateDOM( - prevNode: CodeHighlightNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { const update = super.updateDOM(prevNode, dom, config); const prevClassName = getHighlightThemeClass( config.theme, diff --git a/packages/lexical-code/src/CodeNode.ts b/packages/lexical-code/src/CodeNode.ts index 728e23e64b1..f2ae407c189 100644 --- a/packages/lexical-code/src/CodeNode.ts +++ b/packages/lexical-code/src/CodeNode.ts @@ -107,11 +107,7 @@ export class CodeNode extends ElementNode { } return element; } - updateDOM( - prevNode: CodeNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { const language = this.__language; const prevLanguage = prevNode.__language; diff --git a/packages/lexical-link/flow/LexicalLink.js.flow b/packages/lexical-link/flow/LexicalLink.js.flow index cab496485ac..5e755f8e8aa 100644 --- a/packages/lexical-link/flow/LexicalLink.js.flow +++ b/packages/lexical-link/flow/LexicalLink.js.flow @@ -39,11 +39,6 @@ declare export class LinkNode extends ElementNode { static clone(node: LinkNode): LinkNode; constructor(url: string, attributes?: LinkAttributes, key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM( - prevNode: LinkNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean; static importDOM(): DOMConversionMap | null; exportJSON(): SerializedLinkNode; getURL(): string; diff --git a/packages/lexical-link/src/index.ts b/packages/lexical-link/src/index.ts index b2cdaefc89c..47e8cde0fc8 100644 --- a/packages/lexical-link/src/index.ts +++ b/packages/lexical-link/src/index.ts @@ -113,7 +113,7 @@ export class LinkNode extends ElementNode { } updateDOM( - prevNode: LinkNode, + prevNode: this, anchor: LinkHTMLElementType, config: EditorConfig, ): boolean { @@ -393,7 +393,7 @@ export class AutoLinkNode extends LinkNode { } updateDOM( - prevNode: AutoLinkNode, + prevNode: this, anchor: LinkHTMLElementType, config: EditorConfig, ): boolean { diff --git a/packages/lexical-list/src/LexicalListNode.ts b/packages/lexical-list/src/LexicalListNode.ts index 2af911c7a8e..a82b342d28d 100644 --- a/packages/lexical-list/src/LexicalListNode.ts +++ b/packages/lexical-list/src/LexicalListNode.ts @@ -111,11 +111,7 @@ export class ListNode extends ElementNode { return dom; } - updateDOM( - prevNode: ListNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { if (prevNode.__tag !== this.__tag) { return true; } diff --git a/packages/lexical-mark/src/MarkNode.ts b/packages/lexical-mark/src/MarkNode.ts index a19bd626fc7..2bc9ab140f0 100644 --- a/packages/lexical-mark/src/MarkNode.ts +++ b/packages/lexical-mark/src/MarkNode.ts @@ -78,7 +78,7 @@ export class MarkNode extends ElementNode { } updateDOM( - prevNode: MarkNode, + prevNode: this, element: HTMLElement, config: EditorConfig, ): boolean { diff --git a/packages/lexical-overflow/flow/LexicalOverflow.js.flow b/packages/lexical-overflow/flow/LexicalOverflow.js.flow index 4ebd65ead94..d9e0a990aea 100644 --- a/packages/lexical-overflow/flow/LexicalOverflow.js.flow +++ b/packages/lexical-overflow/flow/LexicalOverflow.js.flow @@ -19,7 +19,6 @@ declare export class OverflowNode extends ElementNode { static clone(node: OverflowNode): OverflowNode; constructor(key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean; insertNewAfter(selection: RangeSelection): null | LexicalNode; excludeFromCopy(): boolean; static importJSON(serializedNode: SerializedOverflowNode): OverflowNode; diff --git a/packages/lexical-overflow/src/index.ts b/packages/lexical-overflow/src/index.ts index 2b1986a5d6e..60e77d21ecb 100644 --- a/packages/lexical-overflow/src/index.ts +++ b/packages/lexical-overflow/src/index.ts @@ -58,7 +58,7 @@ export class OverflowNode extends ElementNode { return div; } - updateDOM(prevNode: OverflowNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { return false; } diff --git a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs index 2a7c3156111..fe73b19f791 100644 --- a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs @@ -52,7 +52,12 @@ test.describe('Autocomplete', () => { class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" dir="ltr"> Sort by alpha - +

`, ); @@ -118,7 +123,12 @@ test.describe('Autocomplete', () => { data-lexical-text="true"> Test - +

`, ); @@ -204,7 +214,12 @@ test.describe('Autocomplete', () => { data-lexical-text="true"> Test - +

`, ); @@ -241,7 +256,12 @@ test.describe('Autocomplete', () => { data-lexical-text="true"> Test - +

`, ); @@ -278,7 +298,12 @@ test.describe('Autocomplete', () => { data-lexical-text="true"> Test - +

`, ); diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 1b1ba63cd8b..b6a683f022e 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -1657,7 +1657,7 @@ test.describe.parallel('Tables', () => { }); test( - 'Grid selection: can select multiple cells and insert an image', + 'Table selection: can select multiple cells and insert an image', { tag: '@flaky', }, @@ -1741,7 +1741,7 @@ test.describe.parallel('Tables', () => { }, ); - test('Grid selection: can backspace lines, backspacing empty cell does not destroy it #3278', async ({ + test('Table selection: can backspace lines, backspacing empty cell does not destroy it #3278', async ({ page, isPlainText, isCollab, diff --git a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx index 220add6396c..777f0d69ab6 100644 --- a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx +++ b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx @@ -73,11 +73,7 @@ export class AutocompleteNode extends TextNode { this.__uuid = uuid; } - updateDOM( - prevNode: unknown, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { return false; } @@ -85,12 +81,16 @@ export class AutocompleteNode extends TextNode { return {element: null}; } + excludeFromCopy() { + return true; + } + createDOM(config: EditorConfig): HTMLElement { - if (this.__uuid !== UUID) { - return document.createElement('span'); - } const dom = super.createDOM(config); dom.classList.add(config.theme.autocomplete); + if (this.__uuid !== UUID) { + dom.style.display = 'none'; + } return dom; } } @@ -99,5 +99,5 @@ export function $createAutocompleteNode( text: string, uuid: string, ): AutocompleteNode { - return new AutocompleteNode(text, uuid); + return new AutocompleteNode(text, uuid).setMode('token'); } diff --git a/packages/lexical-playground/src/nodes/EmojiNode.tsx b/packages/lexical-playground/src/nodes/EmojiNode.tsx index 3c1a56874b4..30b899666d1 100644 --- a/packages/lexical-playground/src/nodes/EmojiNode.tsx +++ b/packages/lexical-playground/src/nodes/EmojiNode.tsx @@ -48,11 +48,7 @@ export class EmojiNode extends TextNode { return dom; } - updateDOM( - prevNode: TextNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { const inner = dom.firstChild; if (inner === null) { return true; diff --git a/packages/lexical-playground/src/nodes/EquationNode.tsx b/packages/lexical-playground/src/nodes/EquationNode.tsx index 373b821f988..1ab7cce2128 100644 --- a/packages/lexical-playground/src/nodes/EquationNode.tsx +++ b/packages/lexical-playground/src/nodes/EquationNode.tsx @@ -128,7 +128,7 @@ export class EquationNode extends DecoratorNode { }; } - updateDOM(prevNode: EquationNode): boolean { + updateDOM(prevNode: this): boolean { // If the inline property changes, replace the element return this.__inline !== prevNode.__inline; } diff --git a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx index 3ed9eca084b..1a759e8cd0e 100644 --- a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx +++ b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx @@ -230,11 +230,7 @@ export class InlineImageNode extends DecoratorNode { return span; } - updateDOM( - prevNode: InlineImageNode, - dom: HTMLElement, - config: EditorConfig, - ): false { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): false { const position = this.__position; if (position !== prevNode.__position) { const className = `${config.theme.inlineImage} position-${position}`; diff --git a/packages/lexical-playground/src/nodes/LayoutContainerNode.ts b/packages/lexical-playground/src/nodes/LayoutContainerNode.ts index b89eed53b89..8bb7cddf47a 100644 --- a/packages/lexical-playground/src/nodes/LayoutContainerNode.ts +++ b/packages/lexical-playground/src/nodes/LayoutContainerNode.ts @@ -73,7 +73,7 @@ export class LayoutContainerNode extends ElementNode { return {element}; } - updateDOM(prevNode: LayoutContainerNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { if (prevNode.__templateColumns !== this.__templateColumns) { dom.style.gridTemplateColumns = this.__templateColumns; } diff --git a/packages/lexical-playground/src/nodes/SpecialTextNode.tsx b/packages/lexical-playground/src/nodes/SpecialTextNode.tsx index 474241d15d4..8c89106f7cf 100644 --- a/packages/lexical-playground/src/nodes/SpecialTextNode.tsx +++ b/packages/lexical-playground/src/nodes/SpecialTextNode.tsx @@ -37,11 +37,7 @@ export class SpecialTextNode extends TextNode { return dom; } - updateDOM( - prevNode: TextNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { if (prevNode.__text.startsWith('[') && prevNode.__text.endsWith(']')) { const strippedText = this.__text.substring(1, this.__text.length - 1); // Strip brackets again dom.textContent = strippedText; // Update the text content diff --git a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts index 1ade6a71cbc..6d4387e0a59 100644 --- a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts +++ b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts @@ -79,10 +79,7 @@ export class CollapsibleContainerNode extends ElementNode { return dom; } - updateDOM( - prevNode: CollapsibleContainerNode, - dom: HTMLDetailsElement, - ): boolean { + updateDOM(prevNode: this, dom: HTMLDetailsElement): boolean { const currentOpen = this.__open; if (prevNode.__open !== currentOpen) { // details is not well supported in Chrome #5582 diff --git a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContentNode.ts b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContentNode.ts index 427d22bfd72..f6f4ce07ddd 100644 --- a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContentNode.ts +++ b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContentNode.ts @@ -72,7 +72,7 @@ export class CollapsibleContentNode extends ElementNode { return dom; } - updateDOM(prevNode: CollapsibleContentNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { return false; } diff --git a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts index d2e0488e09e..3b6a39061b3 100644 --- a/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts +++ b/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleTitleNode.ts @@ -62,7 +62,7 @@ export class CollapsibleTitleNode extends ElementNode { return dom; } - updateDOM(prevNode: CollapsibleTitleNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { return false; } diff --git a/packages/lexical-rich-text/flow/LexicalRichText.js.flow b/packages/lexical-rich-text/flow/LexicalRichText.js.flow index 0751c17fc5f..9b7bbb0c803 100644 --- a/packages/lexical-rich-text/flow/LexicalRichText.js.flow +++ b/packages/lexical-rich-text/flow/LexicalRichText.js.flow @@ -25,7 +25,6 @@ declare export class QuoteNode extends ElementNode { static clone(node: QuoteNode): QuoteNode; constructor(key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean; insertNewAfter( selection: RangeSelection, restoreSelection?: boolean, @@ -44,7 +43,6 @@ declare export class HeadingNode extends ElementNode { constructor(tag: HeadingTagType, key?: NodeKey): void; getTag(): HeadingTagType; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean; static importDOM(): DOMConversionMap | null; insertNewAfter( selection: RangeSelection, diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index cec5da17fa7..639cab8ffa5 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -136,7 +136,7 @@ export class QuoteNode extends ElementNode { addClassNamesToElement(element, config.theme.quote); return element; } - updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { return false; } @@ -257,7 +257,7 @@ export class HeadingNode extends ElementNode { return element; } - updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement): boolean { return false; } diff --git a/packages/lexical-table/flow/LexicalTable.js.flow b/packages/lexical-table/flow/LexicalTable.js.flow index 0d3af559ed3..075314baf93 100644 --- a/packages/lexical-table/flow/LexicalTable.js.flow +++ b/packages/lexical-table/flow/LexicalTable.js.flow @@ -52,7 +52,6 @@ declare export class TableCellNode extends ElementNode { key?: NodeKey, ): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: TableCellNode, dom: HTMLElement): boolean; insertNewAfter( selection: RangeSelection, ): null | ParagraphNode | TableCellNode; @@ -70,7 +69,6 @@ declare export class TableCellNode extends ElementNode { setBackgroundColor(newBackgroundColor: null | string): TableCellNode; toggleHeaderStyle(headerState: TableCellHeaderState): TableCellNode; hasHeader(): boolean; - updateDOM(prevNode: TableCellNode): boolean; collapseAtStart(): true; canBeEmpty(): false; } @@ -99,7 +97,6 @@ declare export class TableNode extends ElementNode { static clone(node: TableNode): TableNode; constructor(key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: TableNode, dom: HTMLElement): boolean; insertNewAfter(selection: RangeSelection): null | ParagraphNode | TableNode; collapseAtStart(): true; getCordsFromCellNode( @@ -126,7 +123,6 @@ declare export class TableRowNode extends ElementNode { static clone(node: TableRowNode): TableRowNode; constructor(height?: ?number, key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: TableRowNode, dom: HTMLElement): boolean; setHeight(height: number): ?number; getHeight(): ?number; insertNewAfter( diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index 795779c4990..92b52bcf1f0 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -265,7 +265,7 @@ export class TableCellNode extends ElementNode { return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS; } - updateDOM(prevNode: TableCellNode): boolean { + updateDOM(prevNode: this): boolean { return ( prevNode.__headerState !== this.__headerState || prevNode.__width !== this.__width || diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 636613346b3..b31104a7cdb 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -225,11 +225,7 @@ export class TableNode extends ElementNode { return tableElement; } - updateDOM( - prevNode: TableNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { if (prevNode.__rowStriping !== this.__rowStriping) { setRowStriping(dom, config, this.__rowStriping); } diff --git a/packages/lexical-table/src/LexicalTableObserver.ts b/packages/lexical-table/src/LexicalTableObserver.ts index 059c471b96d..2f76ce0712a 100644 --- a/packages/lexical-table/src/LexicalTableObserver.ts +++ b/packages/lexical-table/src/LexicalTableObserver.ts @@ -462,9 +462,7 @@ export class TableObserver { const selection = $getSelection(); - if (!$isTableSelection(selection)) { - invariant(false, 'Expected grid selection'); - } + invariant($isTableSelection(selection), 'Expected TableSelection'); const selectedNodes = selection.getNodes().filter($isTableCellNode); diff --git a/packages/lexical-table/src/LexicalTableRowNode.ts b/packages/lexical-table/src/LexicalTableRowNode.ts index 9a7d5c99c88..4e216b865df 100644 --- a/packages/lexical-table/src/LexicalTableRowNode.ts +++ b/packages/lexical-table/src/LexicalTableRowNode.ts @@ -104,7 +104,7 @@ export class TableRowNode extends ElementNode { return this.getLatest().__height; } - updateDOM(prevNode: TableRowNode): boolean { + updateDOM(prevNode: this): boolean { return prevNode.__height !== this.__height; } diff --git a/packages/lexical-utils/src/markSelection.ts b/packages/lexical-utils/src/markSelection.ts index 382b57b4bb7..1c5a07911dc 100644 --- a/packages/lexical-utils/src/markSelection.ts +++ b/packages/lexical-utils/src/markSelection.ts @@ -67,11 +67,12 @@ export default function markSelection( currentAnchorNodeKey !== previousAnchorNode.getKey() || (currentAnchorNode !== previousAnchorNode && (!$isTextNode(previousAnchorNode) || - currentAnchorNode.updateDOM( - previousAnchorNode, - currentAnchorNodeDOM, - editor._config, - ))); + ($isTextNode(currentAnchorNode) && + currentAnchorNode.updateDOM( + previousAnchorNode, + currentAnchorNodeDOM, + editor._config, + )))); const differentFocusDOM = previousFocusNode === null || currentFocusNodeDOM === null || @@ -79,11 +80,12 @@ export default function markSelection( currentFocusNodeKey !== previousFocusNode.getKey() || (currentFocusNode !== previousFocusNode && (!$isTextNode(previousFocusNode) || - currentFocusNode.updateDOM( - previousFocusNode, - currentFocusNodeDOM, - editor._config, - ))); + ($isTextNode(currentFocusNode) && + currentFocusNode.updateDOM( + previousFocusNode, + currentFocusNodeDOM, + editor._config, + )))); if (differentAnchorDOM || differentFocusDOM) { const anchorHTMLElement = editor.getElementByKey( anchor.getNode().getKey(), diff --git a/packages/lexical-website/docs/concepts/commands.md b/packages/lexical-website/docs/concepts/commands.md index a773b640186..5456058c3cf 100644 --- a/packages/lexical-website/docs/concepts/commands.md +++ b/packages/lexical-website/docs/concepts/commands.md @@ -31,6 +31,8 @@ editor.registerCommand( Commands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or a Plugin, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts). +Calling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`. + ```js editor.dispatchCommand(command, payload); ``` @@ -70,6 +72,10 @@ editor.registerCommand( You can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed. +The command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use +`editor.update` (and *never* call `editor.read`) synchronously from within a command listener. It is safe to call +`editor.getEditorState().read` if you need to read the previous state after updates have already been made. + ```js const removeListener = editor.registerCommand( COMMAND, diff --git a/packages/lexical-website/docs/concepts/nodes.md b/packages/lexical-website/docs/concepts/nodes.md index baa60eb92c5..99526c6d082 100644 --- a/packages/lexical-website/docs/concepts/nodes.md +++ b/packages/lexical-website/docs/concepts/nodes.md @@ -183,7 +183,7 @@ export class CustomParagraph extends ElementNode { return dom; } - updateDOM(prevNode: CustomParagraph, dom: HTMLElement): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { // Returning false tells Lexical that this node does not need its // DOM element replacing with a new copy from createDOM. return false; @@ -231,7 +231,7 @@ export class ColoredNode extends TextNode { } updateDOM( - prevNode: ColoredNode, + prevNode: this, dom: HTMLElement, config: EditorConfig, ): boolean { diff --git a/packages/lexical-website/docs/concepts/selection.md b/packages/lexical-website/docs/concepts/selection.md index e1c1a152fd9..0923b3befa1 100644 --- a/packages/lexical-website/docs/concepts/selection.md +++ b/packages/lexical-website/docs/concepts/selection.md @@ -116,4 +116,54 @@ editor.update(() => { // You can also clear selection by setting it to `null`. $setSelection(null); }); -``` \ No newline at end of file +``` + +## Focus + +You may notice that when you issue an `editor.update` or +`editor.dispatchCommand` then the editor can "steal focus" if there is +a selection and the editor is editable. This is because the Lexical +selection is reconciled to the DOM selection during reconciliation, +and the browser's focus follows its DOM selection. + +If you want to make updates or dispatch commands to the editor without +changing the selection, can use the `'skip-dom-selection'` update tag +(added in v0.22.0): + +```js +// Call this from an editor.update or command listener +$addUpdateTag('skip-dom-selection'); +``` + +If you want to add this tag during processing of a `dispatchCommand`, +you can wrap it in an `editor.update`: + +```js +// NOTE: If you are already in a command listener or editor.update, +// do *not* nest a second editor.update! Nested updates have +// confusing semantics (dispatchCommand will re-use the +// current update without nesting) +editor.update(() => { + $addUpdateTag('skip-dom-selection'); + editor.dispatchCommand(/* … */); +}); +``` + +If you have to support older versions of Lexical, you can mark the editor +as not editable during the update or dispatch. + +```js +// NOTE: This code should be *outside* of your update or command listener, e.g. +// directly in the DOM event listener +const prevEditable = editor.isEditable(); +editor.setEditable(false); +editor.update( + () => { + // run your update code or editor.dispatchCommand in here + }, { + onUpdate: () => { + editor.setEditable(prevEditable); + }, + }, +); +``` diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index dccc5987079..a632cd35407 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -415,8 +415,7 @@ declare export class LexicalNode { getTextContentSize(includeDirectionless?: boolean): number; createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement; updateDOM( - // $FlowFixMe - prevNode: any, + prevNode: this, dom: HTMLElement, config: EditorConfig, ): boolean; @@ -611,11 +610,6 @@ declare export class TextNode extends LexicalNode { getTextContent(): string; getFormatFlags(type: TextFormatType, alignWithFormat: null | number): number; createDOM(config: EditorConfig): HTMLElement; - updateDOM( - prevNode: TextNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean; selectionTransform( prevSelection: null | BaseSelection, nextSelection: RangeSelection, @@ -707,7 +701,6 @@ declare export class RootNode extends ElementNode { replace(node: N): N; insertBefore(nodeToInsert: T): T; insertAfter(nodeToInsert: T): T; - updateDOM(prevNode: RootNode, dom: HTMLElement): false; append(...nodesToAppend: Array): this; canBeEmpty(): false; } @@ -850,7 +843,6 @@ declare export class ParagraphNode extends ElementNode { static clone(node: ParagraphNode): ParagraphNode; constructor(key?: NodeKey): void; createDOM(config: EditorConfig): HTMLElement; - updateDOM(prevNode: ParagraphNode, dom: HTMLElement): boolean; insertNewAfter( selection: RangeSelection, restoreSelection?: boolean, diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index dcc91e85658..7cc1280854b 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -37,7 +37,7 @@ import { getCachedTypeToNodeMap, getDefaultView, getDOMSelection, - markAllNodesAsDirty, + markNodesWithTypesAsDirty, } from './LexicalUtils'; import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode'; import {DecoratorNode} from './nodes/LexicalDecoratorNode'; @@ -279,11 +279,11 @@ export type LexicalCommand = { * * editor.registerCommand(MY_COMMAND, payload => { * // Type of `payload` is inferred here. But lets say we want to extract a function to delegate to - * handleMyCommand(editor, payload); + * $handleMyCommand(editor, payload); * return true; * }); * - * function handleMyCommand(editor: LexicalEditor, payload: CommandPayloadType) { + * function $handleMyCommand(editor: LexicalEditor, payload: CommandPayloadType) { * // `payload` is of type `SomeType`, extracted from the command. * } * ``` @@ -775,14 +775,24 @@ export class LexicalEditor { } /** * Registers a listener that will trigger anytime the provided command - * is dispatched, subject to priority. Listeners that run at a higher priority can "intercept" - * commands and prevent them from propagating to other handlers by returning true. + * is dispatched with {@link LexicalEditor.dispatch}, subject to priority. + * Listeners that run at a higher priority can "intercept" commands and + * prevent them from propagating to other handlers by returning true. * - * Listeners registered at the same priority level will run deterministically in the order of registration. + * Listeners are always invoked in an {@link LexicalEditor.update} and can + * call dollar functions. + * + * Listeners registered at the same priority level will run + * deterministically in the order of registration. * * @param command - the command that will trigger the callback. * @param listener - the function that will execute when the command is dispatched. * @param priority - the relative priority of the listener. 0 | 1 | 2 | 3 | 4 + * (or {@link COMMAND_PRIORITY_EDITOR} | + * {@link COMMAND_PRIORITY_LOW} | + * {@link COMMAND_PRIORITY_NORMAL} | + * {@link COMMAND_PRIORITY_HIGH} | + * {@link COMMAND_PRIORITY_CRITICAL}) * @returns a teardown function that can be used to cleanup the listener. */ registerCommand

( @@ -960,7 +970,10 @@ export class LexicalEditor { registeredNodes.push(registeredReplaceWithNode); } - markAllNodesAsDirty(this, klass.getType()); + markNodesWithTypesAsDirty( + this, + registeredNodes.map((node) => node.klass.getType()), + ); return () => { registeredNodes.forEach((node) => node.transforms.delete(listener as Transform), @@ -989,7 +1002,10 @@ export class LexicalEditor { /** * Dispatches a command of the specified type with the specified payload. * This triggers all command listeners (set by {@link LexicalEditor.registerCommand}) - * for this type, passing them the provided payload. + * for this type, passing them the provided payload. The command listeners + * will be triggered in an implicit {@link LexicalEditor.update}, unless + * this was invoked from inside an update in which case that update context + * will be re-used (as if this was a dollar function itself). * @param type - the type of command listeners to trigger. * @param payload - the data to pass as an argument to the command listeners. */ diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 11296a2be5a..8b129817a23 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -607,7 +607,8 @@ export function $commitPendingUpdates( editor._editable && // domSelection will be null in headless domSelection !== null && - (needsUpdate || pendingSelection === null || pendingSelection.dirty) + (needsUpdate || pendingSelection === null || pendingSelection.dirty) && + !tags.has('skip-dom-selection') ) { activeEditor = editor; activeEditorState = pendingEditorState; @@ -1005,6 +1006,7 @@ function $beginUpdate( const shouldUpdate = editor._dirtyType !== NO_DIRTY_NODES || + editor._deferred.length > 0 || editorStateHasDirtySelection(pendingEditorState, editor); if (shouldUpdate) { diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index d4bc2d1ea46..3a90936ba86 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -72,7 +72,6 @@ import { internalGetActiveEditorState, isCurrentlyReadOnlyMode, triggerCommandListeners, - updateEditor, } from './LexicalUpdates'; export const emptyFunction = () => { @@ -498,22 +497,31 @@ export function getEditorStateTextContent(editorState: EditorState): string { return editorState.read(() => $getRoot().getTextContent()); } -export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void { - // Mark all existing text nodes as dirty - updateEditor( - editor, +export function markNodesWithTypesAsDirty( + editor: LexicalEditor, + types: string[], +): void { + // We only need to mark nodes dirty if they were in the previous state. + // If they aren't, then they are by definition dirty already. + const cachedMap = getCachedTypeToNodeMap(editor.getEditorState()); + const dirtyNodeMaps: NodeMap[] = []; + for (const type of types) { + const nodeMap = cachedMap.get(type); + if (nodeMap) { + // By construction these are non-empty + dirtyNodeMaps.push(nodeMap); + } + } + // Nothing to mark dirty, no update necessary + if (dirtyNodeMaps.length === 0) { + return; + } + editor.update( () => { - const editorState = getActiveEditorState(); - if (editorState.isEmpty()) { - return; - } - if (type === 'root') { - $getRoot().markDirty(); - return; - } - const nodeMap = editorState._nodeMap; - for (const [, node] of nodeMap) { - node.markDirty(); + for (const nodeMap of dirtyNodeMaps) { + for (const node of nodeMap.values()) { + node.markDirty(); + } } }, editor._pendingEditorState === null @@ -1825,17 +1833,26 @@ export function getCachedTypeToNodeMap( ); let typeToNodeMap = cachedNodeMaps.get(editorState); if (!typeToNodeMap) { - typeToNodeMap = new Map(); + typeToNodeMap = computeTypeToNodeMap(editorState); 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; +} + +/** + * @internal + * Compute a Map of node type to nodes for an EditorState + */ +function computeTypeToNodeMap(editorState: EditorState): TypeToNodeMap { + const typeToNodeMap = new Map(); + 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 3986f27806f..af71d8f681c 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -39,6 +39,7 @@ import { createEditor, EditorState, ElementNode, + getDOMSelection, type Klass, type LexicalEditor, type LexicalNode, @@ -2893,4 +2894,94 @@ describe('LexicalEditor tests', () => { expect(onError).not.toHaveBeenCalled(); }); }); + + describe('selection', () => { + it('updates the DOM selection', async () => { + const onError = jest.fn(); + const newEditor = createTestEditor({ + onError: onError, + }); + const text = 'initial content'; + let textNode!: TextNode; + await newEditor.update( + () => { + textNode = $createTextNode(text); + $getRoot().append($createParagraphNode().append(textNode)); + textNode.select(); + }, + {tag: 'history-merge'}, + ); + await newEditor.setRootElement(container); + const domText = newEditor.getElementByKey(textNode.getKey()) + ?.firstChild as Text; + expect(domText).not.toBe(null); + let selection = getDOMSelection(newEditor._window || window) as Selection; + expect(selection).not.toBe(null); + expect(selection.rangeCount > 0); + let range = selection.getRangeAt(0); + expect(range.collapsed).toBe(true); + expect(range.startContainer).toBe(domText); + expect(range.endContainer).toBe(domText); + expect(range.startOffset).toBe(text.length); + expect(range.endOffset).toBe(text.length); + await newEditor.update(() => { + textNode.select(0); + }); + selection = getDOMSelection(newEditor._window || window) as Selection; + expect(selection).not.toBe(null); + expect(selection.rangeCount > 0); + range = selection.getRangeAt(0); + expect(range.collapsed).toBe(false); + expect(range.startContainer).toBe(domText); + expect(range.endContainer).toBe(domText); + expect(range.startOffset).toBe(0); + expect(range.endOffset).toBe(text.length); + expect(onError).not.toHaveBeenCalled(); + }); + it('does not update the Lexical->DOM selection with skip-dom-selection', async () => { + const onError = jest.fn(); + const newEditor = createTestEditor({ + onError: onError, + }); + const text = 'initial content'; + let textNode!: TextNode; + await newEditor.update( + () => { + textNode = $createTextNode(text); + $getRoot().append($createParagraphNode().append(textNode)); + textNode.select(); + }, + {tag: 'history-merge'}, + ); + await newEditor.setRootElement(container); + const domText = newEditor.getElementByKey(textNode.getKey()) + ?.firstChild as Text; + expect(domText).not.toBe(null); + let selection = getDOMSelection(newEditor._window || window) as Selection; + expect(selection).not.toBe(null); + expect(selection.rangeCount > 0); + let range = selection.getRangeAt(0); + expect(range.collapsed).toBe(true); + expect(range.startContainer).toBe(domText); + expect(range.endContainer).toBe(domText); + expect(range.startOffset).toBe(text.length); + expect(range.endOffset).toBe(text.length); + await newEditor.update( + () => { + textNode.select(0); + }, + {tag: 'skip-dom-selection'}, + ); + selection = getDOMSelection(newEditor._window || window) as Selection; + expect(selection).not.toBe(null); + expect(selection.rangeCount > 0); + range = selection.getRangeAt(0); + expect(range.collapsed).toBe(true); + expect(range.startContainer).toBe(domText); + expect(range.endContainer).toBe(domText); + expect(range.startOffset).toBe(text.length); + expect(range.endOffset).toBe(text.length); + expect(onError).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts index e360eac2486..6b7e913c1ba 100644 --- a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts @@ -244,6 +244,36 @@ describe('LexicalUtils tests', () => { }); describe('$onUpdate', () => { + test('deferred even when there are no dirty nodes', () => { + const {editor} = testEnv; + const runs: string[] = []; + + editor.update( + () => { + $onUpdate(() => { + runs.push('second'); + }); + }, + { + onUpdate: () => { + runs.push('first'); + }, + }, + ); + expect(runs).toEqual([]); + editor.update(() => { + $onUpdate(() => { + runs.push('third'); + }); + }); + expect(runs).toEqual([]); + + // Flush pending updates + editor.read(() => {}); + + expect(runs).toEqual(['first', 'second', 'third']); + }); + test('added fn runs after update, original onUpdate, and prior calls to $onUpdate', () => { const {editor} = testEnv; const runs: string[] = []; diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 562c7c4e387..3f6d1746ae8 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -29,6 +29,7 @@ export type { SerializedEditor, Spread, Transform, + UpdateListener, } from './LexicalEditor'; export type { EditorState, diff --git a/packages/lexical/src/nodes/LexicalRootNode.ts b/packages/lexical/src/nodes/LexicalRootNode.ts index b99576b8ea4..7e4782061f1 100644 --- a/packages/lexical/src/nodes/LexicalRootNode.ts +++ b/packages/lexical/src/nodes/LexicalRootNode.ts @@ -77,7 +77,7 @@ export class RootNode extends ElementNode { // View - updateDOM(prevNode: RootNode, dom: HTMLElement): false { + updateDOM(prevNode: this, dom: HTMLElement): false { return false; } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index fad639a1c72..694bff21a1b 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -490,11 +490,7 @@ export class TextNode extends LexicalNode { return dom; } - updateDOM( - prevNode: TextNode, - dom: HTMLElement, - config: EditorConfig, - ): boolean { + updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean { const nextText = this.__text; const prevFormat = prevNode.__format; const nextFormat = this.__format; From db1464c527c312f8d3b26525919c10fc94fc65d4 Mon Sep 17 00:00:00 2001 From: Vinay Kushwaha <51132814+iamvinayvk@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:25:45 +0530 Subject: [PATCH 23/31] [lexical-table][lexical-playground] Fix: Insertion of multiple rows (#6963) Co-authored-by: Bob Ippolito --- .../__tests__/e2e/Tables.spec.mjs | 405 ++++++++++++++++++ .../plugins/TableActionMenuPlugin/index.tsx | 6 +- .../lexical-table/src/LexicalTableUtils.ts | 48 ++- 3 files changed, 439 insertions(+), 20 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index b6a683f022e..670e78ff850 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -34,6 +34,7 @@ import { insertSampleImage, insertTable, insertTableColumnBefore, + insertTableRowAbove, insertTableRowBelow, IS_COLLAB, IS_LINUX, @@ -4674,4 +4675,408 @@ test.describe.parallel('Tables', () => { `, ); }); + + test('Can insert multiple rows above the selection', async ({ + page, + isCollab, + isPlainText, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 5, 5); + + await selectCellsFromTableCords( + page, + {x: 0, y: 1}, + {x: 4, y: 3}, + true, + false, + ); + + await insertTableRowAbove(page); + + await assertHTML( + page, + html` +


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }); + + test('Can insert multiple rows below the selection', async ({ + page, + isCollab, + isPlainText, + }) => { + await initialize({isCollab, page}); + test.skip(isPlainText); + test.skip(isCollab); + + await focusEditor(page); + + await insertTable(page, 5, 5); + + await selectCellsFromTableCords( + page, + {x: 0, y: 1}, + {x: 4, y: 3}, + true, + false, + ); + + await insertTableRowBelow(page); + + await assertHTML( + page, + html` +


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


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+
+


+ `, + ); + }); }); diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 2e17272cbf7..b5275c7cd22 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -309,11 +309,13 @@ function TableActionMenu({ const insertTableRowAtSelection = useCallback( (shouldInsertAfter: boolean) => { editor.update(() => { - $insertTableRow__EXPERIMENTAL(shouldInsertAfter); + for (let i = 0; i < selectionCounts.rows; i++) { + $insertTableRow__EXPERIMENTAL(shouldInsertAfter); + } onClose(); }); }, - [editor, onClose], + [editor, onClose, selectionCounts.rows], ); const insertTableColumnAtSelection = useCallback( diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index 6c354285ec4..84e4ab3f5a7 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -258,20 +258,31 @@ export function $insertTableRow__EXPERIMENTAL( $isRangeSelection(selection) || $isTableSelection(selection), 'Expected a RangeSelection or TableSelection', ); + const anchor = selection.anchor.getNode(); const focus = selection.focus.getNode(); + const [anchorCell] = $getNodeTriplet(anchor); const [focusCell, , grid] = $getNodeTriplet(focus); - const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell); + const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap( + grid, + focusCell, + anchorCell, + ); const columnCount = gridMap[0].length; + const {startRow: anchorStartRow} = anchorCellMap; const {startRow: focusStartRow} = focusCellMap; let insertedRow: TableRowNode | null = null; if (insertAfter) { - const focusEndRow = focusStartRow + focusCell.__rowSpan - 1; - const focusEndRowMap = gridMap[focusEndRow]; + const insertAfterEndRow = + Math.max( + focusStartRow + focusCell.__rowSpan, + anchorStartRow + anchorCell.__rowSpan, + ) - 1; + const insertAfterEndRowMap = gridMap[insertAfterEndRow]; const newRow = $createTableRowNode(); for (let i = 0; i < columnCount; i++) { - const {cell, startRow} = focusEndRowMap[i]; - if (startRow + cell.__rowSpan - 1 <= focusEndRow) { - const currentCell = focusEndRowMap[i].cell as TableCellNode; + const {cell, startRow} = insertAfterEndRowMap[i]; + if (startRow + cell.__rowSpan - 1 <= insertAfterEndRow) { + const currentCell = insertAfterEndRowMap[i].cell as TableCellNode; const currentCellHeaderState = currentCell.__headerState; const headerState = getHeaderState( @@ -286,20 +297,21 @@ export function $insertTableRow__EXPERIMENTAL( cell.setRowSpan(cell.__rowSpan + 1); } } - const focusEndRowNode = grid.getChildAtIndex(focusEndRow); + const insertAfterEndRowNode = grid.getChildAtIndex(insertAfterEndRow); invariant( - $isTableRowNode(focusEndRowNode), - 'focusEndRow is not a TableRowNode', + $isTableRowNode(insertAfterEndRowNode), + 'insertAfterEndRow is not a TableRowNode', ); - focusEndRowNode.insertAfter(newRow); + insertAfterEndRowNode.insertAfter(newRow); insertedRow = newRow; } else { - const focusStartRowMap = gridMap[focusStartRow]; + const insertBeforeStartRow = Math.min(focusStartRow, anchorStartRow); + const insertBeforeStartRowMap = gridMap[insertBeforeStartRow]; const newRow = $createTableRowNode(); for (let i = 0; i < columnCount; i++) { - const {cell, startRow} = focusStartRowMap[i]; - if (startRow === focusStartRow) { - const currentCell = focusStartRowMap[i].cell as TableCellNode; + const {cell, startRow} = insertBeforeStartRowMap[i]; + if (startRow === insertBeforeStartRow) { + const currentCell = insertBeforeStartRowMap[i].cell as TableCellNode; const currentCellHeaderState = currentCell.__headerState; const headerState = getHeaderState( @@ -314,12 +326,12 @@ export function $insertTableRow__EXPERIMENTAL( cell.setRowSpan(cell.__rowSpan + 1); } } - const focusStartRowNode = grid.getChildAtIndex(focusStartRow); + const insertBeforeStartRowNode = grid.getChildAtIndex(insertBeforeStartRow); invariant( - $isTableRowNode(focusStartRowNode), - 'focusEndRow is not a TableRowNode', + $isTableRowNode(insertBeforeStartRowNode), + 'insertBeforeStartRow is not a TableRowNode', ); - focusStartRowNode.insertBefore(newRow); + insertBeforeStartRowNode.insertBefore(newRow); insertedRow = newRow; } return insertedRow; From 70cfd2f12a055457f36f0cecf0f9a8f325c082c9 Mon Sep 17 00:00:00 2001 From: Parasaran <74203806+Parasaran-Python@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:45:19 +0530 Subject: [PATCH 24/31] [lexical-playground] Bug Fix: Allow scrolling if the table cell content overflows (#6966) --- .../src/themes/PlaygroundEditorTheme.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 2e985c4dc8d..d7041331a52 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -213,6 +213,14 @@ padding: 6px 8px; position: relative; outline: none; + overflow: auto; +} +/* + A firefox workaround to allow scrolling of overflowing table cell + ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1904159 +*/ +.PlaygroundEditorTheme__tableCell > * { + overflow: inherit; } .PlaygroundEditorTheme__tableCellResizer { position: absolute; From 8d9d945b6671f7a687a115465e6df1b7dd21bb7e Mon Sep 17 00:00:00 2001 From: Bedru Umer <63902795+bedre7@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:16:45 +0300 Subject: [PATCH 25/31] [lexical][lexical-rich-text][lexical-playground] Feature: Support capitalization format (#6897) Co-authored-by: Fadekemi Adebayo Co-authored-by: Bob Ippolito --- .../__tests__/e2e/KeyboardShortcuts.spec.mjs | 15 ++ .../__tests__/e2e/TextFormatting.spec.mjs | 142 ++++++++++++++++++ .../__tests__/keyboardShortcuts/index.mjs | 24 +++ .../src/context/ToolbarContext.tsx | 5 + .../src/images/icons/type-capitalize.svg | 1 + .../src/images/icons/type-lowercase.svg | 3 + .../src/images/icons/type-uppercase.svg | 3 + packages/lexical-playground/src/index.css | 12 ++ .../FloatingTextFormatToolbarPlugin/index.tsx | 52 +++++++ .../src/plugins/ShortcutsPlugin/index.tsx | 12 ++ .../src/plugins/ShortcutsPlugin/shortcuts.ts | 33 ++++ .../src/plugins/ToolbarPlugin/index.tsx | 48 ++++++ .../src/themes/PlaygroundEditorTheme.css | 9 ++ .../src/themes/PlaygroundEditorTheme.ts | 3 + packages/lexical-rich-text/src/index.ts | 44 ++++++ packages/lexical/flow/Lexical.js.flow | 14 +- packages/lexical/src/LexicalConstants.ts | 11 +- packages/lexical/src/LexicalEditor.ts | 3 + packages/lexical/src/LexicalUtils.ts | 9 ++ packages/lexical/src/nodes/LexicalTextNode.ts | 5 +- .../__tests__/unit/LexicalTextNode.test.tsx | 62 +++++++- 21 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 packages/lexical-playground/src/images/icons/type-capitalize.svg create mode 100644 packages/lexical-playground/src/images/icons/type-lowercase.svg create mode 100644 packages/lexical-playground/src/images/icons/type-uppercase.svg diff --git a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs index 989a4bbbeb8..60997995ceb 100644 --- a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs @@ -23,14 +23,17 @@ import { selectCharacters, toggleBold, toggleBulletList, + toggleCapitalize, toggleChecklist, toggleInsertCodeBlock, toggleItalic, + toggleLowercase, toggleNumberedList, toggleStrikethrough, toggleSubscript, toggleSuperscript, toggleUnderline, + toggleUppercase, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -112,6 +115,18 @@ const alignmentTestCases = [ ]; const additionalStylesTestCases = [ + { + applyShortcut: (page) => toggleLowercase(page), + style: 'Lowercase', + }, + { + applyShortcut: (page) => toggleUppercase(page), + style: 'Uppercase', + }, + { + applyShortcut: (page) => toggleCapitalize(page), + style: 'Capitalize', + }, { applyShortcut: (page) => toggleStrikethrough(page), style: 'Strikethrough', diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index 528cec14d12..2727f72b27c 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -13,8 +13,11 @@ import { moveToLineEnd, selectCharacters, toggleBold, + toggleCapitalize, toggleItalic, + toggleLowercase, toggleUnderline, + toggleUppercase, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -428,6 +431,145 @@ test.describe.parallel('TextFormatting', () => { }); }); + const capitalizationFormats = [ + { + applyCapitalization: toggleLowercase, + className: 'PlaygroundEditorTheme__textLowercase', + format: 'lowercase', + }, + { + applyCapitalization: toggleUppercase, + className: 'PlaygroundEditorTheme__textUppercase', + format: 'uppercase', + }, + { + applyCapitalization: toggleCapitalize, + className: 'PlaygroundEditorTheme__textCapitalize', + format: 'capitalize', + }, + ]; + + capitalizationFormats.forEach(({className, format, applyCapitalization}) => { + test(`Can select text and change it to ${format}`, async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + await page.keyboard.type('Hello world!'); + await moveLeft(page); + await selectCharacters(page, 'left', 5); + + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 0, 0], + focusOffset: 6, + focusPath: [0, 0, 0], + }); + + await applyCapitalization(page); + await assertHTML( + page, + html` +

+ Hello + world + ! +

+ `, + ); + + await assertSelection(page, { + anchorOffset: 5, + anchorPath: [0, 1, 0], + focusOffset: 0, + focusPath: [0, 1, 0], + }); + }); + }); + + const capitalizationResettingTestCases = [ + { + expectedFinalHTML: html` +

+ Hello + world! +

+ `, + key: 'Space', + }, + { + expectedFinalHTML: html` +

+ Hello + + world! +

+ `, + key: 'Tab', + }, + { + expectedFinalHTML: html` +

+ Hello +

+

+ world! +

+ `, + key: 'Enter', + }, + ]; + + capitalizationFormats.forEach(({format, className, applyCapitalization}) => { + capitalizationResettingTestCases.forEach(({key, expectedFinalHTML}) => { + test(`Pressing ${key} resets ${format} format`, async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + + await applyCapitalization(page); + await page.keyboard.type('Hello'); + + await assertHTML( + page, + html` +

+ Hello +

+ `, + ); + + // Pressing the key should reset the format + await page.keyboard.press(key); + await page.keyboard.type(' world!'); + + await assertHTML( + page, + expectedFinalHTML.replace('$formatClassName', className), + ); + }); + }); + }); + test(`Can select text and increase the font-size`, async ({ page, isPlainText, diff --git a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs index 41893cd7900..e4bef9db645 100644 --- a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs +++ b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs @@ -261,6 +261,30 @@ export async function toggleInsertCodeBlock(page) { await page.keyboard.up('Shift'); } +export async function toggleLowercase(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('1'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function toggleUppercase(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('2'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function toggleCapitalize(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('3'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + export async function toggleStrikethrough(page) { await keyDownCtrlOrMeta(page); await page.keyboard.down('Shift'); diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx index 266c584f7ef..f8b1c1f082b 100644 --- a/packages/lexical-playground/src/context/ToolbarContext.tsx +++ b/packages/lexical-playground/src/context/ToolbarContext.tsx @@ -40,6 +40,8 @@ export const blockTypeToBlockName = { quote: 'Quote', }; +//disable eslint sorting rule for quick reference to toolbar state +/* eslint-disable sort-keys-fix/sort-keys-fix */ const INITIAL_TOOLBAR_STATE = { bgColor: '#fff', blockType: 'paragraph' as keyof typeof blockTypeToBlockName, @@ -63,6 +65,9 @@ const INITIAL_TOOLBAR_STATE = { isSubscript: false, isSuperscript: false, isUnderline: false, + isLowercase: false, + isUppercase: false, + isCapitalize: false, rootType: 'root' as keyof typeof rootTypeToRootName, }; diff --git a/packages/lexical-playground/src/images/icons/type-capitalize.svg b/packages/lexical-playground/src/images/icons/type-capitalize.svg new file mode 100644 index 00000000000..359fcd0707c --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-capitalize.svg @@ -0,0 +1 @@ + diff --git a/packages/lexical-playground/src/images/icons/type-lowercase.svg b/packages/lexical-playground/src/images/icons/type-lowercase.svg new file mode 100644 index 00000000000..5d097d7a57b --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-lowercase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/images/icons/type-uppercase.svg b/packages/lexical-playground/src/images/icons/type-uppercase.svg new file mode 100644 index 00000000000..d0887b5d2f5 --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-uppercase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 8ad3159aeeb..d62dd871509 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -395,6 +395,18 @@ i.underline { background-image: url(images/icons/type-underline.svg); } +i.uppercase { + background-image: url(images/icons/type-uppercase.svg); +} + +i.lowercase { + background-image: url(images/icons/type-lowercase.svg); +} + +i.capitalize { + background-image: url(images/icons/type-capitalize.svg); +} + i.strikethrough { background-image: url(images/icons/type-strikethrough.svg); } diff --git a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx index 2404f88dca9..0cb2730b632 100644 --- a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -39,6 +39,9 @@ function TextFormatFloatingToolbar({ isBold, isItalic, isUnderline, + isUppercase, + isLowercase, + isCapitalize, isCode, isStrikethrough, isSubscript, @@ -51,6 +54,9 @@ function TextFormatFloatingToolbar({ isCode: boolean; isItalic: boolean; isLink: boolean; + isUppercase: boolean; + isLowercase: boolean; + isCapitalize: boolean; isStrikethrough: boolean; isSubscript: boolean; isSuperscript: boolean; @@ -193,6 +199,7 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); }} className={'popup-item spaced ' + (isBold ? 'active' : '')} + title="Bold" aria-label="Format text as bold"> @@ -202,6 +209,7 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic'); }} className={'popup-item spaced ' + (isItalic ? 'active' : '')} + title="Italic" aria-label="Format text as italics"> @@ -211,6 +219,7 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline'); }} className={'popup-item spaced ' + (isUnderline ? 'active' : '')} + title="Underline" aria-label="Format text to underlined"> @@ -220,6 +229,7 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); }} className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')} + title="Strikethrough" aria-label="Format text with a strikethrough"> @@ -243,12 +253,43 @@ function TextFormatFloatingToolbar({ aria-label="Format Superscript"> + + + @@ -256,6 +297,7 @@ function TextFormatFloatingToolbar({ type="button" onClick={insertLink} className={'popup-item spaced ' + (isLink ? 'active' : '')} + title="Insert link" aria-label="Insert link"> @@ -265,6 +307,7 @@ function TextFormatFloatingToolbar({ type="button" onClick={insertComment} className={'popup-item spaced insert-comment'} + title="Insert comment" aria-label="Insert comment"> @@ -282,6 +325,9 @@ function useFloatingTextFormatToolbar( const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); + const [isUppercase, setIsUppercase] = useState(false); + const [isLowercase, setIsLowercase] = useState(false); + const [isCapitalize, setIsCapitalize] = useState(false); const [isStrikethrough, setIsStrikethrough] = useState(false); const [isSubscript, setIsSubscript] = useState(false); const [isSuperscript, setIsSuperscript] = useState(false); @@ -317,6 +363,9 @@ function useFloatingTextFormatToolbar( setIsBold(selection.hasFormat('bold')); setIsItalic(selection.hasFormat('italic')); setIsUnderline(selection.hasFormat('underline')); + setIsUppercase(selection.hasFormat('uppercase')); + setIsLowercase(selection.hasFormat('lowercase')); + setIsCapitalize(selection.hasFormat('capitalize')); setIsStrikethrough(selection.hasFormat('strikethrough')); setIsSubscript(selection.hasFormat('subscript')); setIsSuperscript(selection.hasFormat('superscript')); @@ -378,6 +427,9 @@ function useFloatingTextFormatToolbar( isLink={isLink} isBold={isBold} isItalic={isItalic} + isUppercase={isUppercase} + isLowercase={isLowercase} + isCapitalize={isCapitalize} isStrikethrough={isStrikethrough} isSubscript={isSubscript} isSuperscript={isSuperscript} diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx index 4549d8a10e8..eff896fcafa 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx @@ -34,6 +34,7 @@ import { UpdateFontSizeType, } from '../ToolbarPlugin/utils'; import { + isCapitalize, isCenterAlign, isClearFormatting, isDecreaseFontSize, @@ -50,11 +51,13 @@ import { isInsertLink, isJustifyAlign, isLeftAlign, + isLowercase, isOutdent, isRightAlign, isStrikeThrough, isSubscript, isSuperscript, + isUppercase, } from './shortcuts'; export default function ShortcutsPlugin({ @@ -96,6 +99,15 @@ export default function ShortcutsPlugin({ } else if (isStrikeThrough(event)) { event.preventDefault(); editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); + } else if (isLowercase(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); + } else if (isUppercase(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + } else if (isCapitalize(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); } else if (isIndent(event)) { event.preventDefault(); editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts index 4a959f9dcac..5ea8514e98a 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -27,6 +27,9 @@ export const SHORTCUTS = Object.freeze({ DECREASE_FONT_SIZE: IS_APPLE ? '⌘+Shift+,' : 'Ctrl+Shift+,', INSERT_CODE_BLOCK: IS_APPLE ? '⌘+Shift+C' : 'Ctrl+Shift+C', STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S', + LOWERCASE: IS_APPLE ? '⌘+Shift+1' : 'Ctrl+Shift+1', + UPPERCASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2', + CAPITALIZE: IS_APPLE ? '⌘+Shift+3' : 'Ctrl+Shift+3', CENTER_ALIGN: IS_APPLE ? '⌘+Shift+E' : 'Ctrl+Shift+E', JUSTIFY_ALIGN: IS_APPLE ? '⌘+Shift+J' : 'Ctrl+Shift+J', LEFT_ALIGN: IS_APPLE ? '⌘+Shift+L' : 'Ctrl+Shift+L', @@ -117,6 +120,36 @@ export function isFormatQuote(event: KeyboardEvent): boolean { ); } +export function isLowercase(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad1' || code === 'Digit1') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isUppercase(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad2' || code === 'Digit2') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isCapitalize(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad3' || code === 'Digit3') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + export function isStrikeThrough(event: KeyboardEvent): boolean { const {code, shiftKey, altKey, metaKey, ctrlKey} = event; return ( diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index ed5da202bca..1dd6dc066d4 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -615,6 +615,9 @@ export default function ToolbarPlugin({ 'fontSize', $getSelectionStyleValueForProperty(selection, 'font-size', '15px'), ); + updateToolbarState('isLowercase', selection.hasFormat('lowercase')); + updateToolbarState('isUppercase', selection.hasFormat('uppercase')); + updateToolbarState('isCapitalize', selection.hasFormat('capitalize')); } }, [activeEditor, editor, updateToolbarState]); @@ -888,6 +891,51 @@ export default function ToolbarPlugin({ buttonLabel="" buttonAriaLabel="Formatting options for additional text styles" buttonIconClassName="icon dropdown-more"> + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isLowercase) + } + title="Lowercase" + aria-label="Format text to lowercase"> +
+ + Lowercase +
+ {SHORTCUTS.LOWERCASE} +
+ { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isUppercase) + } + title="Uppercase" + aria-label="Format text to uppercase"> +
+ + Uppercase +
+ {SHORTCUTS.UPPERCASE} +
+ { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isCapitalize) + } + title="Capitalize" + aria-label="Format text to capitalize"> +
+ + Capitalize +
+ {SHORTCUTS.CAPITALIZE} +
{ activeEditor.dispatchCommand( diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index d7041331a52..b5306f0b691 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -121,6 +121,15 @@ font-family: Menlo, Consolas, Monaco, monospace; font-size: 94%; } +.PlaygroundEditorTheme__textLowercase { + text-transform: lowercase; +} +.PlaygroundEditorTheme__textUppercase { + text-transform: uppercase; +} +.PlaygroundEditorTheme__textCapitalize { + text-transform: capitalize; +} .PlaygroundEditorTheme__hashtag { background-color: rgba(88, 144, 255, 0.15); border-bottom: 1px solid rgba(88, 144, 255, 0.3); diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index e473ada671c..e7c6a4aab7e 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -106,13 +106,16 @@ const theme: EditorThemeClasses = { tableSelection: 'PlaygroundEditorTheme__tableSelection', text: { bold: 'PlaygroundEditorTheme__textBold', + capitalize: 'PlaygroundEditorTheme__textCapitalize', code: 'PlaygroundEditorTheme__textCode', italic: 'PlaygroundEditorTheme__textItalic', + lowercase: 'PlaygroundEditorTheme__textLowercase', strikethrough: 'PlaygroundEditorTheme__textStrikethrough', subscript: 'PlaygroundEditorTheme__textSubscript', superscript: 'PlaygroundEditorTheme__textSuperscript', underline: 'PlaygroundEditorTheme__textUnderline', underlineStrikethrough: 'PlaygroundEditorTheme__textUnderlineStrikethrough', + uppercase: 'PlaygroundEditorTheme__textUppercase', }, }; diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 639cab8ffa5..dbc770b8b4e 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -88,6 +88,8 @@ import { KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, + KEY_SPACE_COMMAND, + KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, REMOVE_TEXT_COMMAND, @@ -549,6 +551,19 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize(); } +/** + * Resets the capitalization of the selection to default. + * Called when the user presses space, tab, or enter key. + * @param selection The selection to reset the capitalization of. + */ +function $resetCapitalization(selection: RangeSelection): void { + for (const format of ['lowercase', 'uppercase', 'capitalize'] as const) { + if (selection.hasFormat(format)) { + selection.toggleFormat(format); + } + } +} + export function registerRichText(editor: LexicalEditor): () => void { const removeListener = mergeRegister( editor.registerCommand( @@ -909,6 +924,9 @@ export function registerRichText(editor: LexicalEditor): () => void { if (!$isRangeSelection(selection)) { return false; } + + $resetCapitalization(selection); + if (event !== null) { // If we have beforeinput, then we can avoid blocking // the default behavior. This ensures that the iOS can @@ -1074,6 +1092,32 @@ export function registerRichText(editor: LexicalEditor): () => void { }, COMMAND_PRIORITY_EDITOR, ), + editor.registerCommand( + KEY_SPACE_COMMAND, + (_) => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $resetCapitalization(selection); + } + + return false; + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + KEY_TAB_COMMAND, + (_) => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $resetCapitalization(selection); + } + + return false; + }, + COMMAND_PRIORITY_EDITOR, + ), ); return removeListener; } diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index a632cd35407..1779c2f272a 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -80,7 +80,10 @@ declare export var IS_ITALIC: number; declare export var IS_STRIKETHROUGH: number; declare export var IS_SUBSCRIPT: number; declare export var IS_SUPERSCRIPT: number; -declare export var IS_UNDERLIN: number; +declare export var IS_UNDERLINE: number; +declare export var IS_UPPERCASE: number; +declare export var IS_LOWERCASE: number; +declare export var IS_CAPITALIZE: number; declare export var TEXT_TYPE_TO_FORMAT: Record; /** @@ -243,6 +246,9 @@ type TextNodeThemeClasses = { code?: EditorThemeClassName, subscript?: EditorThemeClassName, superscript?: EditorThemeClassName, + lowercase?: EditorThemeClassName, + uppercase?: EditorThemeClassName, + capitalize?: EditorThemeClassName, }; export type EditorThemeClasses = { characterLimit?: EditorThemeClassName, @@ -584,7 +590,11 @@ export type TextFormatType = | 'highlight' | 'code' | 'subscript' - | 'superscript'; + | 'superscript' + | 'lowercase' + | 'uppercase' + | 'capitalize'; + type TextModeType = 'normal' | 'token' | 'segmented'; declare export class TextNode extends LexicalNode { diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts index 81b86a372ef..aead3dbddff 100644 --- a/packages/lexical/src/LexicalConstants.ts +++ b/packages/lexical/src/LexicalConstants.ts @@ -44,6 +44,9 @@ export const IS_CODE = 1 << 4; export const IS_SUBSCRIPT = 1 << 5; export const IS_SUPERSCRIPT = 1 << 6; export const IS_HIGHLIGHT = 1 << 7; +export const IS_LOWERCASE = 1 << 8; +export const IS_UPPERCASE = 1 << 9; +export const IS_CAPITALIZE = 1 << 10; export const IS_ALL_FORMATTING = IS_BOLD | @@ -53,7 +56,10 @@ export const IS_ALL_FORMATTING = IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT | - IS_HIGHLIGHT; + IS_HIGHLIGHT | + IS_LOWERCASE | + IS_UPPERCASE | + IS_CAPITALIZE; // Text node details export const IS_DIRECTIONLESS = 1; @@ -97,13 +103,16 @@ export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']'); export const TEXT_TYPE_TO_FORMAT: Record = { bold: IS_BOLD, + capitalize: IS_CAPITALIZE, code: IS_CODE, highlight: IS_HIGHLIGHT, italic: IS_ITALIC, + lowercase: IS_LOWERCASE, strikethrough: IS_STRIKETHROUGH, subscript: IS_SUBSCRIPT, superscript: IS_SUPERSCRIPT, underline: IS_UNDERLINE, + uppercase: IS_UPPERCASE, }; export const DETAIL_TYPE_TO_DETAIL: Record = { diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 7cc1280854b..1961c8f4f34 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -69,6 +69,9 @@ export type TextNodeThemeClasses = { code?: EditorThemeClassName; highlight?: EditorThemeClassName; italic?: EditorThemeClassName; + lowercase?: EditorThemeClassName; + uppercase?: EditorThemeClassName; + capitalize?: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 3a90936ba86..6ac38ead20c 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -225,6 +225,15 @@ export function toggleTextFormatType( newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript; } else if (type === 'superscript') { newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript; + } else if (type === 'lowercase') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize; + } else if (type === 'uppercase') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize; + } else if (type === 'capitalize') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; } return newFormat; } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 694bff21a1b..de71f6942a0 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -90,7 +90,10 @@ export type TextFormatType = | 'highlight' | 'code' | 'subscript' - | 'superscript'; + | 'superscript' + | 'lowercase' + | 'uppercase' + | 'capitalize'; export type TextModeType = 'normal' | 'token' | 'segmented'; diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index 37191abc831..358d2b657dd 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -32,13 +32,16 @@ import { } from '../../../__tests__/utils'; import { IS_BOLD, + IS_CAPITALIZE, IS_CODE, IS_HIGHLIGHT, IS_ITALIC, + IS_LOWERCASE, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, + IS_UPPERCASE, } from '../../../LexicalConstants'; import { $getCompositionKey, @@ -51,12 +54,15 @@ const editorConfig = Object.freeze({ theme: { text: { bold: 'my-bold-class', + capitalize: 'my-capitalize-class', code: 'my-code-class', highlight: 'my-highlight-class', italic: 'my-italic-class', + lowercase: 'my-lowercase-class', strikethrough: 'my-strikethrough-class', underline: 'my-underline-class', underlineStrikethrough: 'my-underline-strikethrough-class', + uppercase: 'my-uppercase-class', }, }, }); @@ -210,6 +216,9 @@ describe('LexicalTextNode tests', () => { ['subscript', IS_SUBSCRIPT], ['superscript', IS_SUPERSCRIPT], ['highlight', IS_HIGHLIGHT], + ['lowercase', IS_LOWERCASE], + ['uppercase', IS_UPPERCASE], + ['capitalize', IS_CAPITALIZE], ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => { const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag); const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag); @@ -318,6 +327,34 @@ describe('LexicalTextNode tests', () => { }); }); + test('capitalization formats are mutually exclusive', async () => { + const capitalizationFormats: TextFormatType[] = [ + 'lowercase', + 'uppercase', + 'capitalize', + ]; + + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + + // Set each format and ensure that the other formats are cleared + capitalizationFormats.forEach((formatToSet) => { + textNode.toggleFormat(formatToSet as TextFormatType); + + capitalizationFormats + .filter((format) => format !== formatToSet) + .forEach((format) => + expect(textNode.hasFormat(format as TextFormatType)).toBe(false), + ); + + expect(textNode.hasFormat(formatToSet as TextFormatType)).toBe(true); + }); + }); + }); + test('selectPrevious()', async () => { await update(() => { const paragraphNode = $createParagraphNode(); @@ -636,6 +673,24 @@ describe('LexicalTextNode tests', () => { 'My text node', 'My text node', ], + [ + 'lowercase', + IS_LOWERCASE, + 'My text node', + 'My text node', + ], + [ + 'uppercase', + IS_UPPERCASE, + 'My text node', + 'My text node', + ], + [ + 'capitalize', + IS_CAPITALIZE, + 'My text node', + 'My text node', + ], [ 'underline + strikethrough', IS_UNDERLINE | IS_STRIKETHROUGH, @@ -669,15 +724,16 @@ describe('LexicalTextNode tests', () => { 'My text node', ], [ - 'code + underline + strikethrough + bold + italic + highlight', + 'code + underline + strikethrough + bold + italic + highlight + uppercase', IS_CODE | IS_UNDERLINE | IS_STRIKETHROUGH | IS_BOLD | IS_ITALIC | - IS_HIGHLIGHT, + IS_HIGHLIGHT | + IS_UPPERCASE, 'My text node', - 'My text node', + 'My text node', ], ])('%s text format type', async (_type, format, contents, expectedHTML) => { await update(() => { From b8bd3bb3f44ed590c3f1bd93ed0ea072f3939100 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sat, 14 Dec 2024 14:32:41 -0800 Subject: [PATCH 26/31] [lexical-website] Add Discord to the community section of the footer (#6967) --- packages/lexical-website/docusaurus.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lexical-website/docusaurus.config.js b/packages/lexical-website/docusaurus.config.js index 4559ce4ca2c..33c47913249 100644 --- a/packages/lexical-website/docusaurus.config.js +++ b/packages/lexical-website/docusaurus.config.js @@ -335,6 +335,10 @@ const config = { }, { items: [ + { + href: 'https://discord.gg/KmG4wQnnD9', + label: 'Discord', + }, { href: 'https://stackoverflow.com/questions/tagged/lexicaljs', label: 'Stack Overflow', From 26c6f26238ce92aa66fddde15156b242c73b0524 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sat, 14 Dec 2024 15:24:54 -0800 Subject: [PATCH 27/31] [lexical][lexical-table] Bug Fix: Allow TableSelection to be preserved during contextmenu events (#6964) --- examples/react-rich/src/App.tsx | 3 +- packages/lexical-link/src/index.ts | 2 +- packages/lexical-list/src/LexicalListNode.ts | 2 +- .../__tests__/e2e/Tables.spec.mjs | 309 ++++++++++++++---- .../ExcalidrawNode/ExcalidrawComponent.tsx | 7 +- .../nodes/InlineImageNode/InlineImageNode.tsx | 11 +- .../plugins/CodeActionMenuPlugin/index.tsx | 4 +- .../src/plugins/ImagesPlugin/index.tsx | 6 +- .../src/plugins/InlineImagePlugin/index.tsx | 6 +- .../plugins/TableActionMenuPlugin/index.tsx | 6 +- .../src/plugins/TableCellResizer/index.tsx | 11 +- .../plugins/TableHoverActionsPlugin/index.tsx | 4 +- .../lexical-playground/src/ui/DropDown.tsx | 11 +- .../src/ui/EquationEditor.tsx | 4 +- .../src/ui/ExcalidrawModal.tsx | 4 +- packages/lexical-playground/src/ui/Modal.tsx | 4 +- .../lexical-playground/src/utils/guard.ts | 10 - .../src/LexicalCheckListPlugin.tsx | 9 +- .../src/LexicalClickableLinkPlugin.tsx | 3 +- .../src/LexicalContextMenuPlugin.tsx | 4 +- .../src/LexicalDraggableBlockPlugin.tsx | 8 +- packages/lexical-rich-text/src/index.ts | 10 +- .../lexical-table/src/LexicalTableCellNode.ts | 2 +- .../lexical-table/src/LexicalTableNode.ts | 10 +- .../src/LexicalTableSelectionHelpers.ts | 20 +- .../lexical-utils/src/positionNodeOnRange.ts | 2 +- packages/lexical/src/LexicalEvents.ts | 3 +- packages/lexical/src/LexicalMutations.ts | 8 +- packages/lexical/src/LexicalUpdates.ts | 19 +- packages/lexical/src/LexicalUtils.ts | 37 ++- packages/lexical/src/index.ts | 1 + .../lexical/src/nodes/LexicalElementNode.ts | 2 +- .../lexical/src/nodes/LexicalParagraphNode.ts | 2 +- packages/lexical/src/nodes/LexicalTextNode.ts | 2 +- 34 files changed, 366 insertions(+), 180 deletions(-) delete mode 100644 packages/lexical-playground/src/utils/guard.ts diff --git a/examples/react-rich/src/App.tsx b/examples/react-rich/src/App.tsx index 8c16e827171..f71cc08adc7 100644 --- a/examples/react-rich/src/App.tsx +++ b/examples/react-rich/src/App.tsx @@ -17,6 +17,7 @@ import { DOMConversionMap, DOMExportOutput, DOMExportOutputMap, + isHTMLElement, Klass, LexicalEditor, LexicalNode, @@ -36,7 +37,7 @@ const removeStylesExportDOM = ( target: LexicalNode, ): DOMExportOutput => { const output = target.exportDOM(editor); - if (output && output.element instanceof HTMLElement) { + if (output && isHTMLElement(output.element)) { // Remove all inline styles and classes if the element is an HTMLElement // Children are checked as well since TextNode can be nested // in i, b, and strong tags. diff --git a/packages/lexical-link/src/index.ts b/packages/lexical-link/src/index.ts index 47e8cde0fc8..2149cf4a30c 100644 --- a/packages/lexical-link/src/index.ts +++ b/packages/lexical-link/src/index.ts @@ -117,7 +117,7 @@ export class LinkNode extends ElementNode { anchor: LinkHTMLElementType, config: EditorConfig, ): boolean { - if (anchor instanceof HTMLAnchorElement) { + if (isHTMLAnchorElement(anchor)) { const url = this.__url; const target = this.__target; const rel = this.__rel; diff --git a/packages/lexical-list/src/LexicalListNode.ts b/packages/lexical-list/src/LexicalListNode.ts index a82b342d28d..dfcaacc1d1f 100644 --- a/packages/lexical-list/src/LexicalListNode.ts +++ b/packages/lexical-list/src/LexicalListNode.ts @@ -152,7 +152,7 @@ export class ListNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const element = this.createDOM(editor._config, editor); - if (element && isHTMLElement(element)) { + if (isHTMLElement(element)) { if (this.__start !== 1) { element.setAttribute('start', String(this.__start)); } diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 670e78ff850..4bbc39f4678 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -106,8 +106,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -150,8 +150,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -198,8 +198,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -244,8 +244,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -289,8 +289,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -317,8 +317,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -349,8 +349,9 @@ test.describe.parallel('Tables', () => { browserName, legacyEvents, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); + // Table edge cursor doesn't show up in Firefox. test.fixme(browserName === 'firefox'); test.fixme( @@ -474,13 +475,14 @@ test.describe.parallel('Tables', () => { isCollab, browserName, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); // Table edge cursor doesn't show up in Firefox. test.fixme(browserName === 'firefox'); // After typing, the dom selection gets set back to the internal previous selection during the update. test.fixme(LEGACY_EVENTS); + await initialize({isCollab, page}); + await focusEditor(page); await insertTable(page, 2, 2); @@ -534,8 +536,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -563,8 +565,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -662,8 +664,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -697,8 +699,8 @@ test.describe.parallel('Tables', () => { isCollab, isPlainText, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 2); @@ -734,8 +736,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -828,8 +830,8 @@ test.describe.parallel('Tables', () => { isCollab, browserName, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 3, 3); @@ -838,7 +840,7 @@ test.describe.parallel('Tables', () => { let p = page; - if (IS_COLLAB) { + if (isCollab) { await focusEditor(page); p = await page.frame('left'); } @@ -959,8 +961,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -1057,8 +1059,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -1152,8 +1154,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -1257,8 +1259,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -1352,8 +1354,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -1416,8 +1418,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 1, 2); @@ -1474,8 +1476,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await page.keyboard.type('Hello World'); @@ -1561,8 +1563,8 @@ test.describe.parallel('Tables', () => { isCollab, isPlainText, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await page.keyboard.type('Text before'); await page.keyboard.press('Enter'); @@ -1622,8 +1624,8 @@ test.describe.parallel('Tables', () => { }); test(`Horizontal rule inside cell`, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 1, 2); @@ -1663,8 +1665,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab, browserName}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -1747,8 +1749,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -1834,8 +1836,8 @@ test.describe.parallel('Tables', () => { isCollab, browserName, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -1939,13 +1941,14 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.fixme( isCollab && IS_LINUX && browserName === 'firefox', 'Flaky on Linux + Collab', ); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2025,9 +2028,10 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2108,10 +2112,11 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); test.fixme(IS_COLLAB && IS_LINUX && browserName === 'firefox'); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2197,9 +2202,10 @@ test.describe.parallel('Tables', () => { }); test('Merge/unmerge cells (1)', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2296,9 +2302,10 @@ test.describe.parallel('Tables', () => { }); test('Merge/unmerge cells (2)', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2443,9 +2450,9 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); test.skip(isCollab); + await initialize({isCollab, page}); await focusEditor(page); @@ -2591,9 +2598,9 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); test.skip(isCollab); + await initialize({isCollab, page}); await focusEditor(page); @@ -2737,9 +2744,10 @@ test.describe.parallel('Tables', () => { }); test('Merge with content', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -2838,8 +2846,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -2975,8 +2983,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -3119,9 +3127,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3186,9 +3195,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3251,9 +3261,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3310,9 +3321,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3387,9 +3399,10 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3453,9 +3466,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3519,9 +3533,10 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3572,9 +3587,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3624,9 +3640,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3648,9 +3665,10 @@ test.describe.parallel('Tables', () => { }); test('Background color to cell', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3684,8 +3702,8 @@ test.describe.parallel('Tables', () => { }); test('Cell merge feature disabled', async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page, tableCellMerge: false}); test.skip(isPlainText); + await initialize({isCollab, page, tableCellMerge: false}); await focusEditor(page); await pasteFromClipboard(page, { @@ -3797,8 +3815,8 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page, tableCellBackgroundColor: false}); test.skip(isPlainText); + await initialize({isCollab, page, tableCellBackgroundColor: false}); await focusEditor(page); await pasteFromClipboard(page, { @@ -3841,9 +3859,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -3942,8 +3961,8 @@ test.describe.parallel('Tables', () => { tag: '@flaky', }, async ({page, isPlainText, isCollab}) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); await insertTable(page, 2, 3); @@ -4061,9 +4080,10 @@ test.describe.parallel('Tables', () => { isPlainText, isCollab, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); - if (IS_COLLAB) { + await initialize({isCollab, page}); + + if (isCollab) { // The contextual menu positioning needs fixing (it's hardcoded to show on the right side) page.setViewportSize({height: 1000, width: 3000}); } @@ -4238,8 +4258,8 @@ test.describe.parallel('Tables', () => { isCollab, isPlainText, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -4463,8 +4483,8 @@ test.describe.parallel('Tables', () => { isCollab, isPlainText, }) => { - await initialize({isCollab, page}); test.skip(isPlainText); + await initialize({isCollab, page}); await focusEditor(page); @@ -5079,4 +5099,149 @@ test.describe.parallel('Tables', () => { `, ); }); + test.describe('with context menu', () => { + test.use({shouldUseLexicalContextMenu: true}); + test(`Can select cells using Table selection and cut them with the context menu`, async ({ + page, + isPlainText, + isCollab, + shouldUseLexicalContextMenu, + browserName, + }) => { + test.skip(isPlainText); + // The way that the clicks happen in test doesn't work in firefox for some reason + // but it does seem to work when you do it by hand + test.fixme(browserName === 'firefox'); + await initialize({isCollab, page, shouldUseLexicalContextMenu}); + + 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 assertHTML( + page, + html` +


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

a

+
+

bb

+
+

cc

+
+

d

+
+

e

+
+

f

+
+


+ `, + html` +


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

a

+
+

bb

+
+

cc

+
+

d

+
+

e

+
+

f

+
+


+ `, + {ignoreClasses: true}, + ); + + await withExclusiveClipboardAccess(async () => { + await click(page, 'div[contenteditable] th p', { + button: 'right', + }); + await click(page, '#typeahead-menu [role="option"] :text("Cut")'); + }); + + await assertHTML( + page, + html` +


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


+
+


+
+

cc

+
+


+
+


+
+

f

+
+


+ `, + undefined, + {ignoreClasses: true}, + ); + }); + }); }); diff --git a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx index 5a45068469e..259935a41f6 100644 --- a/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +++ b/packages/lexical-playground/src/nodes/ExcalidrawNode/ExcalidrawComponent.tsx @@ -18,6 +18,7 @@ import { $getNodeByKey, CLICK_COMMAND, COMMAND_PRIORITY_LOW, + isDOMNode, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, } from 'lexical'; @@ -86,7 +87,11 @@ export default function ExcalidrawComponent({ return true; } - if (buttonElem !== null && buttonElem.contains(eventTarget as Node)) { + if ( + buttonElem !== null && + isDOMNode(eventTarget) && + buttonElem.contains(eventTarget) + ) { if (!event.shiftKey) { clearSelection(); } diff --git a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx index 1a759e8cd0e..df78864ca00 100644 --- a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx +++ b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageNode.tsx @@ -19,7 +19,12 @@ import type { Spread, } from 'lexical'; -import {$applyNodeReplacement, createEditor, DecoratorNode} from 'lexical'; +import { + $applyNodeReplacement, + createEditor, + DecoratorNode, + isHTMLElement, +} from 'lexical'; import * as React from 'react'; import {Suspense} from 'react'; @@ -45,8 +50,8 @@ export interface UpdateInlineImagePayload { } function $convertInlineImageElement(domNode: Node): null | DOMConversionOutput { - if (domNode instanceof HTMLImageElement) { - const {alt: altText, src, width, height} = domNode; + if (isHTMLElement(domNode) && domNode.nodeName === 'IMG') { + const {alt: altText, src, width, height} = domNode as HTMLImageElement; const node = $createInlineImageNode({altText, height, src, width}); return {node}; } diff --git a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx index b200b279e69..e0a795a6a05 100644 --- a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx @@ -15,7 +15,7 @@ import { normalizeCodeLang, } from '@lexical/code'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {$getNearestNodeFromDOMNode} from 'lexical'; +import {$getNearestNodeFromDOMNode, isHTMLElement} from 'lexical'; import {useEffect, useRef, useState} from 'react'; import * as React from 'react'; import {createPortal} from 'react-dom'; @@ -163,7 +163,7 @@ function getMouseInfo(event: MouseEvent): { } { const target = event.target; - if (target && target instanceof HTMLElement) { + if (isHTMLElement(target)) { const codeDOMNode = target.closest( 'code.PlaygroundEditorTheme__code', ); diff --git a/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx b/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx index b2c2120220e..4fb80b2fab2 100644 --- a/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx @@ -23,6 +23,7 @@ import { DRAGSTART_COMMAND, DROP_COMMAND, getDOMSelection, + isHTMLElement, LexicalCommand, LexicalEditor, } from 'lexical'; @@ -357,10 +358,9 @@ declare global { function canDropImage(event: DragEvent): boolean { const target = event.target; return !!( - target && - target instanceof HTMLElement && + isHTMLElement(target) && !target.closest('code, span.editor-image') && - target.parentElement && + isHTMLElement(target.parentElement) && target.parentElement.closest('div.ContentEditable__root') ); } diff --git a/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx b/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx index 98cea7f6888..4ba2f6b40d6 100644 --- a/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/InlineImagePlugin/index.tsx @@ -27,6 +27,7 @@ import { DRAGSTART_COMMAND, DROP_COMMAND, getDOMSelection, + isHTMLElement, LexicalCommand, LexicalEditor, } from 'lexical'; @@ -309,10 +310,9 @@ declare global { function canDropImage(event: DragEvent): boolean { const target = event.target; return !!( - target && - target instanceof HTMLElement && + isHTMLElement(target) && !target.closest('code, span.editor-image') && - target.parentElement && + isHTMLElement(target.parentElement) && target.parentElement.closest('div.ContentEditable__root') ); } diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index b5275c7cd22..7fdae2e20c1 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -43,6 +43,7 @@ import { $isTextNode, COMMAND_PRIORITY_CRITICAL, getDOMSelection, + isDOMNode, SELECTION_CHANGE_COMMAND, } from 'lexical'; import * as React from 'react'; @@ -219,8 +220,9 @@ function TableActionMenu({ if ( dropDownRef.current != null && contextRef.current != null && - !dropDownRef.current.contains(event.target as Node) && - !contextRef.current.contains(event.target as Node) + isDOMNode(event.target) && + !dropDownRef.current.contains(event.target) && + !contextRef.current.contains(event.target) ) { setIsMenuOpen(false); } diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index 23f8d1935ab..72f6fe694a8 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -23,7 +23,7 @@ import { TableNode, } from '@lexical/table'; import {calculateZoomLevel} from '@lexical/utils'; -import {$getNearestNodeFromDOMNode} from 'lexical'; +import {$getNearestNodeFromDOMNode, isHTMLElement} from 'lexical'; import * as React from 'react'; import { MouseEventHandler, @@ -89,6 +89,9 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { useEffect(() => { const onMouseMove = (event: MouseEvent) => { const target = event.target; + if (!isHTMLElement(target)) { + return; + } if (draggingDirection) { updateMouseCurrentPos({ @@ -98,13 +101,13 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { return; } updateIsMouseDown(isMouseDownOnEvent(event)); - if (resizerRef.current && resizerRef.current.contains(target as Node)) { + if (resizerRef.current && resizerRef.current.contains(target)) { return; } if (targetRef.current !== target) { - targetRef.current = target as HTMLElement; - const cell = getDOMCellFromTarget(target as HTMLElement); + targetRef.current = target; + const cell = getDOMCellFromTarget(target); if (cell && activeCell !== cell) { editor.getEditorState().read( diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx index 2116d21179a..00b1169f06e 100644 --- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx @@ -22,7 +22,7 @@ import { TableRowNode, } from '@lexical/table'; import {$findMatchingParent, mergeRegister} from '@lexical/utils'; -import {$getNearestNodeFromDOMNode, NodeKey} from 'lexical'; +import {$getNearestNodeFromDOMNode, isHTMLElement, NodeKey} from 'lexical'; import {useEffect, useMemo, useRef, useState} from 'react'; import * as React from 'react'; import {createPortal} from 'react-dom'; @@ -257,7 +257,7 @@ function getMouseInfo(event: MouseEvent): { } { const target = event.target; - if (target && target instanceof HTMLElement) { + if (isHTMLElement(target)) { const tableDOMNode = target.closest( 'td.PlaygroundEditorTheme__tableCell, th.PlaygroundEditorTheme__tableCell', ); diff --git a/packages/lexical-playground/src/ui/DropDown.tsx b/packages/lexical-playground/src/ui/DropDown.tsx index 012d4674b9e..19d3065df0e 100644 --- a/packages/lexical-playground/src/ui/DropDown.tsx +++ b/packages/lexical-playground/src/ui/DropDown.tsx @@ -6,6 +6,7 @@ * */ +import {isDOMNode} from 'lexical'; import * as React from 'react'; import { ReactNode, @@ -189,15 +190,15 @@ export default function DropDown({ if (button !== null && showDropDown) { const handle = (event: MouseEvent) => { const target = event.target; + if (!isDOMNode(target)) { + return; + } if (stopCloseOnClickSelf) { - if ( - dropDownRef.current && - dropDownRef.current.contains(target as Node) - ) { + if (dropDownRef.current && dropDownRef.current.contains(target)) { return; } } - if (!button.contains(target as Node)) { + if (!button.contains(target)) { setShowDropDown(false); } }; diff --git a/packages/lexical-playground/src/ui/EquationEditor.tsx b/packages/lexical-playground/src/ui/EquationEditor.tsx index d8f9aec527c..ce512aed010 100644 --- a/packages/lexical-playground/src/ui/EquationEditor.tsx +++ b/packages/lexical-playground/src/ui/EquationEditor.tsx @@ -10,7 +10,7 @@ import type {Ref, RefObject} from 'react'; import './EquationEditor.css'; -import * as React from 'react'; +import {isHTMLElement} from 'lexical'; import {ChangeEvent, forwardRef} from 'react'; type BaseEquationEditorProps = { @@ -27,7 +27,7 @@ function EquationEditor( setEquation((event.target as HTMLInputElement).value); }; - return inline && forwardedRef instanceof HTMLInputElement ? ( + return inline && isHTMLElement(forwardedRef) ? ( $ void) { const target = event.target; - if (target === null || !isHTMLElement(target)) { + if (!isHTMLElement(target)) { return; } @@ -173,7 +173,6 @@ function handleCheckItemEvent(event: PointerEvent, callback: () => void) { const firstChild = target.firstChild; if ( - firstChild != null && isHTMLElement(firstChild) && (firstChild.tagName === 'UL' || firstChild.tagName === 'OL') ) { @@ -200,7 +199,7 @@ function handleCheckItemEvent(event: PointerEvent, callback: () => void) { function handleClick(event: Event) { handleCheckItemEvent(event as PointerEvent, () => { - if (event.target instanceof HTMLElement) { + if (isHTMLElement(event.target)) { const domNode = event.target; const editor = getNearestEditorFromDOMNode(domNode); @@ -226,9 +225,9 @@ function handlePointerDown(event: PointerEvent) { } function getActiveCheckListItem(): HTMLElement | null { - const activeElement = document.activeElement as HTMLElement; + const activeElement = document.activeElement; - return activeElement != null && + return isHTMLElement(activeElement) && activeElement.tagName === 'LI' && activeElement.parentNode != null && // @ts-ignore internal field diff --git a/packages/lexical-react/src/LexicalClickableLinkPlugin.tsx b/packages/lexical-react/src/LexicalClickableLinkPlugin.tsx index 3c5e56c36e2..dd76a0b30a5 100644 --- a/packages/lexical-react/src/LexicalClickableLinkPlugin.tsx +++ b/packages/lexical-react/src/LexicalClickableLinkPlugin.tsx @@ -15,6 +15,7 @@ import { $isElementNode, $isRangeSelection, getNearestEditorFromDOMNode, + isDOMNode, } from 'lexical'; import {useEffect} from 'react'; @@ -44,7 +45,7 @@ export function ClickableLinkPlugin({ useEffect(() => { const onClick = (event: MouseEvent) => { const target = event.target; - if (!(target instanceof Node)) { + if (!isDOMNode(target)) { return; } const nearestEditor = getNearestEditorFromDOMNode(target); diff --git a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx index c105d48b8f1..7f1676f3cf9 100644 --- a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx @@ -12,6 +12,7 @@ import {calculateZoomLevel} from '@lexical/utils'; import { COMMAND_PRIORITY_LOW, CommandListenerPriority, + isDOMNode, LexicalNode, } from 'lexical'; import { @@ -122,7 +123,8 @@ export function LexicalContextMenuPlugin({ resolution !== null && menuRef.current != null && event.target != null && - !menuRef.current.contains(event.target as Node) + isDOMNode(event.target) && + !menuRef.current.contains(event.target) ) { closeNodeMenu(); } diff --git a/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx index ef7303ee281..be695b76b23 100644 --- a/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx +++ b/packages/lexical-react/src/LexicalDraggableBlockPlugin.tsx @@ -277,12 +277,12 @@ function useDraggableBlockMenu( useEffect(() => { function onMouseMove(event: MouseEvent) { const target = event.target; - if (target != null && !isHTMLElement(target)) { + if (!isHTMLElement(target)) { setDraggableBlockElem(null); return; } - if (target != null && isOnMenu(target as HTMLElement)) { + if (isOnMenu(target as HTMLElement)) { return; } @@ -324,7 +324,7 @@ function useDraggableBlockMenu( return false; } const {pageY, target} = event; - if (target != null && !isHTMLElement(target)) { + if (!isHTMLElement(target)) { return false; } const targetBlockElem = getBlockElement(anchorElem, editor, event, true); @@ -358,7 +358,7 @@ function useDraggableBlockMenu( if (!draggedNode) { return false; } - if (target != null && !isHTMLElement(target)) { + if (!isHTMLElement(target)) { return false; } const targetBlockElem = getBlockElement(anchorElem, editor, event, true); diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index dbc770b8b4e..7e39f5631dd 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -79,6 +79,7 @@ import { INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, + isDOMNode, isSelectionCapturedInDecoratorInput, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, @@ -154,7 +155,7 @@ export class QuoteNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); - if (element && isHTMLElement(element)) { + if (isHTMLElement(element)) { if (this.isEmpty()) { element.append(document.createElement('br')); } @@ -320,7 +321,7 @@ export class HeadingNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); - if (element && isHTMLElement(element)) { + if (isHTMLElement(element)) { if (this.isEmpty()) { element.append(document.createElement('br')); } @@ -1078,7 +1079,10 @@ export function registerRichText(editor: LexicalEditor): () => void { } // if inputs then paste within the input ignore creating a new node on paste event - if (isSelectionCapturedInDecoratorInput(event.target as Node)) { + if ( + isDOMNode(event.target) && + isSelectionCapturedInDecoratorInput(event.target) + ) { return false; } diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index 92b52bcf1f0..396fc6564d7 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -151,7 +151,7 @@ export class TableCellNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const output = super.exportDOM(editor); - if (output.element && isHTMLElement(output.element)) { + if (isHTMLElement(output.element)) { const element = output.element as HTMLTableCellElement; element.setAttribute( 'data-temporary-table-cell-lexical-key', diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index b31104a7cdb..c404e822f32 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -241,14 +241,10 @@ export class TableNode extends ElementNode { if (superExport.after) { tableElement = superExport.after(tableElement); } - if ( - tableElement && - isHTMLElement(tableElement) && - tableElement.nodeName !== 'TABLE' - ) { + if (isHTMLElement(tableElement) && tableElement.nodeName !== 'TABLE') { tableElement = tableElement.querySelector('table'); } - if (!tableElement || !isHTMLElement(tableElement)) { + if (!isHTMLElement(tableElement)) { return null; } @@ -312,7 +308,7 @@ export class TableNode extends ElementNode { return tableElement; }, element: - element && isHTMLElement(element) && element.nodeName !== 'TABLE' + isHTMLElement(element) && element.nodeName !== 'TABLE' ? element.querySelector('table') : element, }; diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 7959f1cfd75..5d010cc0bc2 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -60,6 +60,7 @@ import { FORMAT_TEXT_COMMAND, getDOMSelection, INSERT_PARAGRAPH_COMMAND, + isDOMNode, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, @@ -185,16 +186,19 @@ export function applyTableHandlers( }; const onMouseMove = (moveEvent: MouseEvent) => { + if (!isDOMNode(moveEvent.target)) { + return; + } if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) { tableObserver.isSelecting = false; editorWindow.removeEventListener('mouseup', onMouseUp); editorWindow.removeEventListener('mousemove', onMouseMove); return; } - const override = !tableElement.contains(moveEvent.target as Node); + const override = !tableElement.contains(moveEvent.target); let focusCell: null | TableDOMCell = null; if (!override) { - focusCell = getDOMCellFromTarget(moveEvent.target as Node); + focusCell = getDOMCellFromTarget(moveEvent.target); } else { for (const el of document.elementsFromPoint( moveEvent.clientX, @@ -231,15 +235,11 @@ export function applyTableHandlers( }; const onMouseDown = (event: MouseEvent) => { - if (event.button !== 0) { - return; - } - - if (!editorWindow) { + if (event.button !== 0 || !isDOMNode(event.target) || !editorWindow) { return; } - const targetCell = getDOMCellFromTarget(event.target as Node); + const targetCell = getDOMCellFromTarget(event.target); if (targetCell !== null) { editor.update(() => { const prevSelection = $getPreviousSelection(); @@ -292,13 +292,13 @@ export function applyTableHandlers( // Clear selection when clicking outside of dom. const mouseDownCallback = (event: MouseEvent) => { - if (event.button !== 0) { + const target = event.target; + if (event.button !== 0 || !isDOMNode(target)) { return; } editor.update(() => { const selection = $getSelection(); - const target = event.target as Node; if ( $isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && diff --git a/packages/lexical-utils/src/positionNodeOnRange.ts b/packages/lexical-utils/src/positionNodeOnRange.ts index 2ae7df5e35e..4202aa51a03 100644 --- a/packages/lexical-utils/src/positionNodeOnRange.ts +++ b/packages/lexical-utils/src/positionNodeOnRange.ts @@ -121,7 +121,7 @@ export default function mlcPositionNodeOnRange( return stop(); } const currentParentDOMNode = currentRootDOMNode.parentElement; - if (currentParentDOMNode === null || !isHTMLElement(currentParentDOMNode)) { + if (!isHTMLElement(currentParentDOMNode)) { return stop(); } stop(); diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 663fbb236a4..662cd81fe2d 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -108,6 +108,7 @@ import { isDeleteLineForward, isDeleteWordBackward, isDeleteWordForward, + isDOMNode, isEscape, isFirefoxClipboardEvents, isItalic, @@ -481,7 +482,7 @@ function onPointerDown(event: PointerEvent, editor: LexicalEditor) { // TODO implement text drag & drop const target = event.target; const pointerType = event.pointerType; - if (target instanceof Node && pointerType !== 'touch') { + if (isDOMNode(target) && pointerType !== 'touch' && event.button === 0) { updateEditor(editor, () => { // Drag & drop should not recompute selection until mouse up; otherwise the initially // selected content is lost. diff --git a/packages/lexical/src/LexicalMutations.ts b/packages/lexical/src/LexicalMutations.ts index fa58ebde193..f4864f60f67 100644 --- a/packages/lexical/src/LexicalMutations.ts +++ b/packages/lexical/src/LexicalMutations.ts @@ -34,6 +34,7 @@ import { internalGetRoot, isDOMUnmanaged, isFirefoxClipboardEvents, + isHTMLElement, } from './LexicalUtils'; // The time between a text entry event and the mutation observer firing. const TEXT_MUTATION_VARIANCE = 100; @@ -130,7 +131,9 @@ function $getNearestManagedNodePairFromDOMNode( const node = $getNodeByKey(key, editorState); if (node) { // All decorator nodes are unmanaged - return $isDecoratorNode(node) ? undefined : [dom as HTMLElement, node]; + return $isDecoratorNode(node) || !isHTMLElement(dom) + ? undefined + : [dom, node]; } } else if (dom === rootElement) { return [rootElement, internalGetRoot(editorState)]; @@ -209,7 +212,8 @@ export function $flushMutations( ) { if (IS_FIREFOX) { const possibleText = - (addedDOM as HTMLElement).innerText || addedDOM.nodeValue; + (isHTMLElement(addedDOM) ? addedDOM.innerText : null) || + addedDOM.nodeValue; if (possibleText) { possibleTextForFirefoxPaste += possibleText; diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 8b129817a23..1b2a83070dc 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -558,7 +558,7 @@ export function $commitPendingUpdates( return; } finally { - observer.observe(rootElement as Node, observerOptions); + observer.observe(rootElement, observerOptions); editor._updating = previouslyUpdating; activeEditorState = previousActiveEditorState; isReadOnlyMode = previousReadOnlyMode; @@ -608,6 +608,7 @@ export function $commitPendingUpdates( // domSelection will be null in headless domSelection !== null && (needsUpdate || pendingSelection === null || pendingSelection.dirty) && + rootElement !== null && !tags.has('skip-dom-selection') ) { activeEditor = editor; @@ -619,11 +620,7 @@ export function $commitPendingUpdates( if (needsUpdate || pendingSelection === null || pendingSelection.dirty) { const blockCursorElement = editor._blockCursorElement; if (blockCursorElement !== null) { - removeDOMBlockCursorElement( - blockCursorElement, - editor, - rootElement as HTMLElement, - ); + removeDOMBlockCursorElement(blockCursorElement, editor, rootElement); } updateDOMSelection( currentSelection, @@ -631,17 +628,13 @@ export function $commitPendingUpdates( editor, domSelection, tags, - rootElement as HTMLElement, + rootElement, nodeCount, ); } - updateDOMBlockCursorElement( - editor, - rootElement as HTMLElement, - pendingSelection, - ); + updateDOMBlockCursorElement(editor, rootElement, pendingSelection); if (observer !== null) { - observer.observe(rootElement as Node, observerOptions); + observer.observe(rootElement, observerOptions); } } finally { activeEditor = previousActiveEditor; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 6ac38ead20c..de2e019dd20 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -114,9 +114,9 @@ export function $isSelectionCapturedInDecorator(node: Node): boolean { } export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean { - const activeElement = document.activeElement as HTMLElement; + const activeElement = document.activeElement; - if (activeElement === null) { + if (!isHTMLElement(activeElement)) { return false; } const nodeName = activeElement.nodeName; @@ -143,7 +143,7 @@ export function isSelectionWithinEditor( rootElement.contains(focusDOM) && // Ignore if selection is within nested editor anchorDOM !== null && - !isSelectionCapturedInDecoratorInput(anchorDOM as Node) && + !isSelectionCapturedInDecoratorInput(anchorDOM) && getNearestEditorFromDOMNode(anchorDOM) === editor ); } catch (error) { @@ -1299,7 +1299,7 @@ export function getElementByKeyOrThrow( export function getParentElement(node: Node): HTMLElement | null { const parentElement = (node as HTMLSlotElement).assignedSlot || node.parentElement; - return parentElement !== null && parentElement.nodeType === 11 + return isDocumentFragment(parentElement) ? ((parentElement as unknown as ShadowRoot).host as HTMLElement) : parentElement; } @@ -1727,28 +1727,37 @@ export function $findMatchingParent( * @param x - The element being tested * @returns Returns true if x is an HTML anchor tag, false otherwise */ -export function isHTMLAnchorElement(x: Node): x is HTMLAnchorElement { +export function isHTMLAnchorElement(x: unknown): x is HTMLAnchorElement { return isHTMLElement(x) && x.tagName === 'A'; } /** - * @param x - The element being testing + * @param x - The element being tested * @returns Returns true if x is an HTML element, false otherwise. */ -export function isHTMLElement(x: Node | EventTarget): x is HTMLElement { - // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors - return x.nodeType === 1; +export function isHTMLElement(x: unknown): x is HTMLElement { + return isDOMNode(x) && x.nodeType === 1; +} + +/** + * @param x - The element being tested + * @returns Returns true if x is a DOM Node, false otherwise. + */ +export function isDOMNode(x: unknown): x is Node { + return ( + typeof x === 'object' && + x !== null && + 'nodeType' in x && + typeof x.nodeType === 'number' + ); } /** * @param x - The element being testing * @returns Returns true if x is a document fragment, false otherwise. */ -export function isDocumentFragment( - x: Node | EventTarget, -): x is DocumentFragment { - // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors - return x.nodeType === 11; +export function isDocumentFragment(x: unknown): x is DocumentFragment { + return isDOMNode(x) && x.nodeType === 11; } /** diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 3f6d1746ae8..0c7e1d7318b 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -186,6 +186,7 @@ export { getNearestEditorFromDOMNode, isBlockDomNode, isDocumentFragment, + isDOMNode, isDOMTextNode, isDOMUnmanaged, isHTMLAnchorElement, diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index 285a546545a..19e54b0cfa6 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -769,7 +769,7 @@ export class ElementNode extends LexicalNode { } exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); - if (element && isHTMLElement(element)) { + if (isHTMLElement(element)) { const indent = this.getIndent(); if (indent > 0) { // padding-inline-start is not widely supported in email HTML diff --git a/packages/lexical/src/nodes/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts index c1250aeae16..1fec9c8b257 100644 --- a/packages/lexical/src/nodes/LexicalParagraphNode.ts +++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts @@ -140,7 +140,7 @@ export class ParagraphNode extends ElementNode { exportDOM(editor: LexicalEditor): DOMExportOutput { const {element} = super.exportDOM(editor); - if (element && isHTMLElement(element)) { + if (isHTMLElement(element)) { if (this.isEmpty()) { element.append(document.createElement('br')); } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index de71f6942a0..ed57345627f 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -620,7 +620,7 @@ export class TextNode extends LexicalNode { exportDOM(editor: LexicalEditor): DOMExportOutput { let {element} = super.exportDOM(editor); invariant( - element !== null && isHTMLElement(element), + isHTMLElement(element), 'Expected TextNode createDOM to always return a HTMLElement', ); element.style.whiteSpace = 'pre-wrap'; From bf445306d714ef503b1ac878f669a1980d1217f9 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sat, 14 Dec 2024 21:33:13 -0800 Subject: [PATCH 28/31] [lexical-utils] Bug Fix: Refactor markSelection for getDOMSlot and not using updateDOM (#6961) --- packages/lexical-utils/src/markSelection.ts | 162 ++++++++++---------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/packages/lexical-utils/src/markSelection.ts b/packages/lexical-utils/src/markSelection.ts index 1c5a07911dc..458aa230fae 100644 --- a/packages/lexical-utils/src/markSelection.ts +++ b/packages/lexical-utils/src/markSelection.ts @@ -8,12 +8,13 @@ import { $getSelection, + $isElementNode, $isRangeSelection, - $isTextNode, type EditorState, ElementNode, getDOMTextNode, type LexicalEditor, + Point, TextNode, } from 'lexical'; @@ -21,6 +22,40 @@ import mergeRegister from './mergeRegister'; import positionNodeOnRange from './positionNodeOnRange'; import px from './px'; +function rangeTargetFromPoint( + point: Point, + node: ElementNode | TextNode, + dom: HTMLElement, +): [HTMLElement | Text, number] { + if (point.type === 'text' || !$isElementNode(node)) { + const textDOM = getDOMTextNode(dom) || dom; + return [textDOM, point.offset]; + } else { + const slot = node.getDOMSlot(dom); + return [slot.element, slot.getFirstChildOffset() + point.offset]; + } +} + +function rangeFromPoints( + editor: LexicalEditor, + anchor: Point, + anchorNode: ElementNode | TextNode, + anchorDOM: HTMLElement, + focus: Point, + focusNode: ElementNode | TextNode, + focusDOM: HTMLElement, +): Range { + const editorDocument = editor._window ? editor._window.document : document; + const range = editorDocument.createRange(); + if (focusNode.isBefore(anchorNode)) { + range.setStart(...rangeTargetFromPoint(focus, focusNode, focusDOM)); + range.setEnd(...rangeTargetFromPoint(anchor, anchorNode, anchorDOM)); + } else { + range.setStart(...rangeTargetFromPoint(anchor, anchorNode, anchorDOM)); + range.setEnd(...rangeTargetFromPoint(focus, focusNode, focusDOM)); + } + return range; +} /** * Place one or multiple newly created Nodes at the current selection. Multiple * nodes will only be created when the selection spans multiple lines (aka @@ -34,8 +69,10 @@ export default function markSelection( onReposition?: (node: Array) => void, ): () => void { let previousAnchorNode: null | TextNode | ElementNode = null; + let previousAnchorNodeDOM: null | HTMLElement = null; let previousAnchorOffset: null | number = null; let previousFocusNode: null | TextNode | ElementNode = null; + let previousFocusNodeDOM: null | HTMLElement = null; let previousFocusOffset: null | number = null; let removeRangeListener: () => void = () => {}; function compute(editorState: EditorState) { @@ -62,103 +99,66 @@ export default function markSelection( const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey); const differentAnchorDOM = previousAnchorNode === null || - currentAnchorNodeDOM === null || + currentAnchorNodeDOM !== previousAnchorNodeDOM || currentAnchorOffset !== previousAnchorOffset || - currentAnchorNodeKey !== previousAnchorNode.getKey() || - (currentAnchorNode !== previousAnchorNode && - (!$isTextNode(previousAnchorNode) || - ($isTextNode(currentAnchorNode) && - currentAnchorNode.updateDOM( - previousAnchorNode, - currentAnchorNodeDOM, - editor._config, - )))); + currentAnchorNodeKey !== previousAnchorNode.getKey(); const differentFocusDOM = previousFocusNode === null || - currentFocusNodeDOM === null || + currentFocusNodeDOM !== previousFocusNodeDOM || currentFocusOffset !== previousFocusOffset || - currentFocusNodeKey !== previousFocusNode.getKey() || - (currentFocusNode !== previousFocusNode && - (!$isTextNode(previousFocusNode) || - ($isTextNode(currentFocusNode) && - currentFocusNode.updateDOM( - previousFocusNode, - currentFocusNodeDOM, - editor._config, - )))); - if (differentAnchorDOM || differentFocusDOM) { - const anchorHTMLElement = editor.getElementByKey( - anchor.getNode().getKey(), + currentFocusNodeKey !== previousFocusNode.getKey(); + if ( + (differentAnchorDOM || differentFocusDOM) && + currentAnchorNodeDOM !== null && + currentFocusNodeDOM !== null + ) { + const range = rangeFromPoints( + editor, + anchor, + currentAnchorNode, + currentAnchorNodeDOM, + focus, + currentFocusNode, + currentFocusNodeDOM, ); - const focusHTMLElement = editor.getElementByKey( - focus.getNode().getKey(), - ); - if (anchorHTMLElement !== null && focusHTMLElement !== null) { - const range = document.createRange(); - let firstHTMLElement; - let firstOffset; - let lastHTMLElement; - let lastOffset; - if (focus.isBefore(anchor)) { - firstHTMLElement = focusHTMLElement; - firstOffset = focus.offset; - lastHTMLElement = anchorHTMLElement; - lastOffset = anchor.offset; - } else { - firstHTMLElement = anchorHTMLElement; - firstOffset = anchor.offset; - lastHTMLElement = focusHTMLElement; - lastOffset = focus.offset; - } - const firstHTMLElementTextChild = getDOMTextNode(firstHTMLElement); - const lastHTMLElementtextChild = getDOMTextNode(lastHTMLElement); - range.setStart( - firstHTMLElementTextChild || firstHTMLElement, - firstOffset, - ); - range.setEnd(lastHTMLElementtextChild || lastHTMLElement, lastOffset); - removeRangeListener(); - removeRangeListener = positionNodeOnRange( - editor, - range, - (domNodes) => { - if (onReposition === undefined) { - for (const domNode of domNodes) { - const domNodeStyle = domNode.style; + removeRangeListener(); + removeRangeListener = positionNodeOnRange(editor, range, (domNodes) => { + if (onReposition === undefined) { + for (const domNode of domNodes) { + const domNodeStyle = domNode.style; - if (domNodeStyle.background !== 'Highlight') { - domNodeStyle.background = 'Highlight'; - } - if (domNodeStyle.color !== 'HighlightText') { - domNodeStyle.color = 'HighlightText'; - } - if (domNodeStyle.marginTop !== px(-1.5)) { - domNodeStyle.marginTop = px(-1.5); - } - if (domNodeStyle.paddingTop !== px(4)) { - domNodeStyle.paddingTop = px(4); - } - if (domNodeStyle.paddingBottom !== px(0)) { - domNodeStyle.paddingBottom = px(0); - } - } - } else { - onReposition(domNodes); + if (domNodeStyle.background !== 'Highlight') { + domNodeStyle.background = 'Highlight'; + } + if (domNodeStyle.color !== 'HighlightText') { + domNodeStyle.color = 'HighlightText'; + } + if (domNodeStyle.marginTop !== px(-1.5)) { + domNodeStyle.marginTop = px(-1.5); } - }, - ); - } + if (domNodeStyle.paddingTop !== px(4)) { + domNodeStyle.paddingTop = px(4); + } + if (domNodeStyle.paddingBottom !== px(0)) { + domNodeStyle.paddingBottom = px(0); + } + } + } else { + onReposition(domNodes); + } + }); } previousAnchorNode = currentAnchorNode; + previousAnchorNodeDOM = currentAnchorNodeDOM; previousAnchorOffset = currentAnchorOffset; previousFocusNode = currentFocusNode; + previousFocusNodeDOM = currentFocusNodeDOM; previousFocusOffset = currentFocusOffset; }); } compute(editor.getEditorState()); return mergeRegister( editor.registerUpdateListener(({editorState}) => compute(editorState)), - removeRangeListener, () => { removeRangeListener(); }, From ddd09034b1cbdc09b2e7a1ae8cd9b086009ba45c Mon Sep 17 00:00:00 2001 From: yhw5 <140376617+yhw5@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:29:28 -0800 Subject: [PATCH 29/31] [lexical-markdown] Bug Fix: preserve the order of markdown tags for markdown combinations, and close the tags when the outmost tag is closed (#5758) Co-authored-by: yhw5 Co-authored-by: Bob Ippolito --- .../lexical-markdown/src/MarkdownExport.ts | 56 +++++++++++++++---- .../__tests__/unit/LexicalMarkdown.test.ts | 10 +++- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownExport.ts b/packages/lexical-markdown/src/MarkdownExport.ts index 0bdf3b71b8b..e26ce19db33 100644 --- a/packages/lexical-markdown/src/MarkdownExport.ts +++ b/packages/lexical-markdown/src/MarkdownExport.ts @@ -108,6 +108,8 @@ function exportChildren( ): string { const output = []; const children = node.getChildren(); + // keep track of unclosed tags from the very beginning + const unclosedTags: {format: TextFormatType; tag: string}[] = []; mainLoop: for (const child of children) { for (const transformer of textMatchTransformers) { @@ -124,7 +126,12 @@ function exportChildren( textMatchTransformers, ), (textNode, textContent) => - exportTextFormat(textNode, textContent, textTransformersIndex), + exportTextFormat( + textNode, + textContent, + textTransformersIndex, + unclosedTags, + ), ); if (result != null) { @@ -137,7 +144,12 @@ function exportChildren( output.push('\n'); } else if ($isTextNode(child)) { output.push( - exportTextFormat(child, child.getTextContent(), textTransformersIndex), + exportTextFormat( + child, + child.getTextContent(), + textTransformersIndex, + unclosedTags, + ), ); } else if ($isElementNode(child)) { // empty paragraph returns "" @@ -156,6 +168,8 @@ function exportTextFormat( node: TextNode, textContent: string, textTransformers: Array, + // unclosed tags include the markdown tags that haven't been closed yet, and their associated formats + unclosedTags: Array<{format: TextFormatType; tag: string}>, ): string { // This function handles the case of a string looking like this: " foo " // Where it would be invalid markdown to generate: "** foo **" @@ -163,6 +177,13 @@ function exportTextFormat( // bring the whitespace back. So our returned string looks like this: " **foo** " const frozenString = textContent.trim(); let output = frozenString; + // the opening tags to be added to the result + let openingTags = ''; + // the closing tags to be added to the result + let closingTags = ''; + + const prevNode = getTextSibling(node, true); + const nextNode = getTextSibling(node, false); const applied = new Set(); @@ -170,25 +191,40 @@ function exportTextFormat( const format = transformer.format[0]; const tag = transformer.tag; + // dedup applied formats if (hasFormat(node, format) && !applied.has(format)) { // Multiple tags might be used for the same format (*, _) applied.add(format); - // Prevent adding opening tag is already opened by the previous sibling - const previousNode = getTextSibling(node, true); - if (!hasFormat(previousNode, format)) { - output = tag + output; + // append the tag to openningTags, if it's not applied to the previous nodes, + // or the nodes before that (which would result in an unclosed tag) + if ( + !hasFormat(prevNode, format) || + !unclosedTags.find((element) => element.tag === tag) + ) { + unclosedTags.push({format, tag}); + openingTags += tag; } + } + } - // Prevent adding closing tag if next sibling will do it - const nextNode = getTextSibling(node, false); + // close any tags in the same order they were applied, if necessary + for (let i = 0; i < unclosedTags.length; i++) { + // prevent adding closing tag if next sibling will do it + if (hasFormat(nextNode, unclosedTags[i].format)) { + continue; + } - if (!hasFormat(nextNode, format)) { - output += tag; + while (unclosedTags.length > i) { + const unclosedTag = unclosedTags.pop(); + if (unclosedTag && typeof unclosedTag.tag === 'string') { + closingTags += unclosedTag.tag; } } + break; } + output = openingTags + output + closingTags; // Replace trimmed version of textContent ensuring surrounding whitespace is not modified return textContent.replace(frozenString, () => output); } diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index f78fc4a3056..0557bd09a10 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -354,7 +354,15 @@ describe('Markdown', () => { }, { html: '

Hello world!

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

Hello world!

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

Hello world!

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

Hello world!

', From ad7187dff11a57192cdfbad74b696500e8c782d2 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 16 Dec 2024 15:30:14 -0800 Subject: [PATCH 30/31] [lexical-table] Feature: Support google docs colgroup import (via deprecated col width attribute) (#6971) --- .../html/TablesHTMLCopyAndPaste.spec.mjs | 78 ++++++++++++++++++- .../lexical-table/src/LexicalTableNode.ts | 12 ++- .../__tests__/unit/LexicalTableNode.test.tsx | 6 +- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs index a2c9e0bf610..c0ef411ca34 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs @@ -106,6 +106,78 @@ test.describe('HTML Tables CopyAndPaste', () => { ); }); + test('Copy + paste (Table - Google Docs with custom widths)', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText || isCollab); + await focusEditor(page); + const clipboard = { + 'text/html': `

short

wide

default

a

b

c


`, + }; + await pasteFromClipboard(page, clipboard); + await assertHTML( + page, + html` + + + + + + + + + + + + + + + + +
+

+ short +

+
+

+ wide +

+
+

+ default +

+
+

+ a +

+
+

+ b +

+
+

+ c +

+
+


+ `, + ); + }); + test('Copy + paste (Table - Quip)', async ({page, isPlainText}) => { test.skip(isPlainText); @@ -204,9 +276,9 @@ test.describe('HTML Tables CopyAndPaste', () => { html` - - - + + +
diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index c404e822f32..fa7aa3d4d04 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -479,10 +479,14 @@ export function $convertTableElement( if (colGroup) { let columns: number[] | undefined = []; for (const col of colGroup.querySelectorAll(':scope > col')) { - const width = (col as HTMLElement).style.width; - if (!width || !PIXEL_VALUE_REG_EXP.test(width)) { - columns = undefined; - break; + let width = (col as HTMLElement).style.width || ''; + if (!PIXEL_VALUE_REG_EXP.test(width)) { + // Also support deprecated width attribute for google docs + width = col.getAttribute('width') || ''; + if (!/^\d+$/.test(width)) { + columns = undefined; + break; + } } columns.push(parseFloat(width)); } diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx index 38755a841e2..c5f208c51f8 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableNode.test.tsx @@ -650,9 +650,9 @@ describe('LexicalTableNode tests', () => { html` - - - + + +
From 62b37130366bd37328413e2b1f19839f860db168 Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:20:59 +0100 Subject: [PATCH 31/31] [Documentation][lexical-website]: Documentation for useLexical node selection hook (#6976) --- .../src/useLexicalNodeSelection.ts | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/lexical-react/src/useLexicalNodeSelection.ts b/packages/lexical-react/src/useLexicalNodeSelection.ts index 2b6c0621d4c..1ee2a9adf89 100644 --- a/packages/lexical-react/src/useLexicalNodeSelection.ts +++ b/packages/lexical-react/src/useLexicalNodeSelection.ts @@ -18,23 +18,48 @@ import { } from 'lexical'; import {useCallback, useEffect, useState} from 'react'; +/** + * A helper function to determine if a specific node is selected in a Lexical editor. + * + * @param {LexicalEditor} editor - The LexicalEditor instance. + * @param {NodeKey} key - The key of the node to check. + * @returns {boolean} Whether the node is selected. + */ + function isNodeSelected(editor: LexicalEditor, key: NodeKey): boolean { return editor.getEditorState().read(() => { const node = $getNodeByKey(key); if (node === null) { - return false; + return false; // Node doesn't exist, so it's not selected. } - return node.isSelected(); + return node.isSelected(); // Check if the node is selected. }); } +/** + * A custom hook to manage the selection state of a specific node in a Lexical editor. + * + * This hook provides utilities to: + * - Check if a node is selected. + * - Update its selection state. + * - Clear the selection. + * + * @param {NodeKey} key - The key of the node to track selection for. + * @returns {[boolean, (selected: boolean) => void, () => void]} A tuple containing: + * - `isSelected` (boolean): Whether the node is currently selected. + * - `setSelected` (function): A function to set the selection state of the node. + * - `clearSelected` (function): A function to clear the selection of the node. + * + */ + export function useLexicalNodeSelection( key: NodeKey, -): [boolean, (arg0: boolean) => void, () => void] { +): [boolean, (selected: boolean) => void, () => void] { const [editor] = useLexicalComposerContext(); + // State to track whether the node is currently selected. const [isSelected, setIsSelected] = useState(() => isNodeSelected(editor, key), ); @@ -48,7 +73,7 @@ export function useLexicalNodeSelection( }); return () => { - isMounted = false; + isMounted = false; // Prevent updates after component unmount. unregister(); }; }, [editor, key]); @@ -62,6 +87,7 @@ export function useLexicalNodeSelection( selection = $createNodeSelection(); $setSelection(selection); } + if ($isNodeSelection(selection)) { if (selected) { selection.add(key);