From 0e2ae869d58a114d65b9924178845c76b6043745 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 9 Mar 2023 12:10:49 -0500 Subject: [PATCH] Remove rows --- .../__tests__/e2e/Tables.spec.mjs | 57 +++++++++++ .../__tests__/keyboardShortcuts/index.mjs | 9 ++ .../__tests__/utils/index.mjs | 15 +++ .../plugins/TableActionMenuPlugin/index.tsx | 26 ++--- .../lexical-table/src/LexicalTableUtils.ts | 95 +++++++++++++++++++ packages/lexical-table/src/index.ts | 2 + 6 files changed, 193 insertions(+), 11 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index f9f665ad079..afa369c28c5 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -19,6 +19,7 @@ import { click, clickSelectors, copyToClipboard, + deleteTableRows, focusEditor, html, initialize, @@ -1218,4 +1219,60 @@ test.describe('Tables', () => { `, ); }); + + test('Delete rows (with conflicting merged cell)', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + + await insertTable(page, 4, 2); + + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 1, y: 3}, + false, + false, + ); + await mergeTableCells(page); + + await page.pause(); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + + await deleteTableRows(page); + + await assertHTML( + page, + html` +


+ + + + + + + + +
+


+
+


+
+


+
+


+ `, + ); + }); }); diff --git a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs index 8cd41d514de..9eedc464c9f 100644 --- a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs +++ b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs @@ -179,6 +179,15 @@ export async function moveRight(page, numCharacters = 1, delayMs) { } } +export async function moveUp(page, numCharacters = 1, delayMs) { + for (let i = 0; i < numCharacters; i++) { + if (delayMs !== undefined) { + await sleep(delayMs); + } + await page.keyboard.press('ArrowUp'); + } +} + export async function moveDown(page, numCharacters = 1, delayMs) { for (let i = 0; i < numCharacters; i++) { if (delayMs !== undefined) { diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 42c89803683..480b340e6b8 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -797,6 +797,21 @@ export async function mergeTableCells(page) { await click(page, '.item[data-test-id="table-merge-cells"]'); } +export async function deleteTableRows(page) { + await click(page, '.table-cell-action-button-container'); + await click(page, '.item[data-test-id="table-delete-rows"]'); +} + +export async function deleteTableColumns(page) { + await click(page, '.table-cell-action-button-container'); + await click(page, '.item[data-test-id="table-delete-columns"]'); +} + +export async function deleteTable(page) { + await click(page, '.table-cell-action-button-container'); + await click(page, '.item[data-test-id="table-delete"]'); +} + export async function enableCompositionKeyEvents(page) { const targetPage = IS_COLLAB ? await page.frame('left') : page; await targetPage.evaluate(() => { diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 8b139251c6b..3682c5cb6b0 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -10,6 +10,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import useLexicalEditable from '@lexical/react/useLexicalEditable'; import { $deleteTableColumn, + $deleteTableRow__EXPERIMENTAL, $getTableCellNodeFromLexicalNode, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, @@ -18,7 +19,6 @@ import { $insertTableRow__EXPERIMENTAL, $isTableCellNode, $isTableRowNode, - $removeTableRowAtIndex, getTableSelectionFromTableElement, HTMLTableElementWithWithTableSelectionState, TableCellHeaderStates, @@ -251,15 +251,10 @@ function TableActionMenu({ const deleteTableRowAtSelection = useCallback(() => { editor.update(() => { - const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode); - - $removeTableRowAtIndex(tableNode, tableRowIndex); - - clearTableSelection(); + $deleteTableRow__EXPERIMENTAL(); onClose(); }); - }, [editor, tableCellNode, clearTableSelection, onClose]); + }, [editor, onClose]); const deleteTableAtSelection = useCallback(() => { editor.update(() => { @@ -419,13 +414,22 @@ function TableActionMenu({
- - -
diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index b1ba9f56483..f890ef439e7 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -7,6 +7,7 @@ */ import type {Grid} from './LexicalTableSelection'; +import type {ElementNode} from 'lexical'; import {$findMatchingParent} from '@lexical/utils'; import { @@ -18,6 +19,7 @@ import { DEPRECATED_$getNodeTriplet, DEPRECATED_$isGridRowNode, DEPRECATED_$isGridSelection, + DEPRECATED_GridCellNode, LexicalNode, } from 'lexical'; import invariant from 'shared/invariant'; @@ -394,3 +396,96 @@ export function $deleteTableColumn( return tableNode; } + +export function $deleteTableRow__EXPERIMENTAL(): void { + const selection = $getSelection(); + invariant( + $isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection), + 'Expected a RangeSelection or GridSelection', + ); + const anchor = selection.anchor.getNode(); + const focus = selection.focus.getNode(); + const [anchorCell, , grid] = DEPRECATED_$getNodeTriplet(anchor); + const [focusCell] = DEPRECATED_$getNodeTriplet(focus); + const [gridMap, anchorCellMap, focusCellMap] = DEPRECATED_$computeGridMap( + grid, + anchorCell, + focusCell, + ); + const {startRow: anchorStartRow} = anchorCellMap; + const {startRow: focusStartRow} = focusCellMap; + const focusEndRow = focusStartRow + focusCell.__rowSpan - 1; + if (gridMap.length === focusEndRow - anchorStartRow + 1) { + // Empty grid + grid.remove(); + return; + } + const columnCount = gridMap[0].length; + const nextRow = gridMap[focusEndRow + 1]; + const nextRowNode = grid.getChildAtIndex(focusEndRow + 1); + invariant( + DEPRECATED_$isGridRowNode(nextRowNode), + 'Expected GridNode childAtIndex(%s) to be RowNode', + String(focusEndRow + 1), + ); + for (let row = focusEndRow; row >= anchorStartRow; row--) { + for (let column = columnCount - 1; column >= 0; column--) { + const { + cell, + startRow: cellStartRow, + startColumn: cellStartColumn, + } = gridMap[row][column]; + if (cellStartColumn !== column) { + // Don't repeat work for the same Cell + continue; + } + // Rows overflowing top have to be trimmed + if (row === anchorStartRow && cellStartRow < anchorStartRow) { + cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow)); + } + // Rows overflowing bottom have to be trimmed and moved to the next row + if ( + cellStartRow >= anchorStartRow && + cellStartRow + cell.__rowSpan - 1 > focusEndRow + ) { + cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1)); + if (column === 0) { + $insertFirst(nextRowNode, cell); + } else { + const {cell: previousCell} = nextRow[column - 1]; + previousCell.insertAfter(cell); + } + } + } + const rowNode = grid.getChildAtIndex(row); + invariant( + DEPRECATED_$isGridRowNode(rowNode), + 'Expected GridNode childAtIndex(%s) to be RowNode', + String(row), + ); + rowNode.remove(); + } + if (nextRow !== undefined) { + const {cell} = nextRow[0]; + $moveSelectionToCell(cell); + } else { + const previousRow = gridMap[anchorStartRow - 1]; + const {cell} = previousRow[0]; + $moveSelectionToCell(cell); + } +} + +function $moveSelectionToCell(cell: DEPRECATED_GridCellNode): void { + const firstDescendant = cell.getFirstDescendant(); + invariant(firstDescendant !== null, 'Unexpected empty cell'); + firstDescendant.getParentOrThrow().selectStart(); +} + +function $insertFirst(parent: ElementNode, node: LexicalNode): void { + const firstChild = parent.getFirstChild(); + if (firstChild !== null) { + parent.insertBefore(firstChild); + } else { + parent.append(node); + } +} diff --git a/packages/lexical-table/src/index.ts b/packages/lexical-table/src/index.ts index 6c11bd728cf..c8f53921641 100644 --- a/packages/lexical-table/src/index.ts +++ b/packages/lexical-table/src/index.ts @@ -42,6 +42,7 @@ import { import { $createTableNodeWithDimensions, $deleteTableColumn, + $deleteTableRow__EXPERIMENTAL, $getTableCellNodeFromLexicalNode, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, @@ -60,6 +61,7 @@ export { $createTableNodeWithDimensions, $createTableRowNode, $deleteTableColumn, + $deleteTableRow__EXPERIMENTAL, $getElementGridForTableNode, $getTableCellNodeFromLexicalNode, $getTableColumnIndexFromTableCellNode,