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 1a40c41c7ca..574e14eef68 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/TablesHTMLCopyAndPaste.spec.mjs @@ -264,7 +264,8 @@ test.describe('HTML Tables CopyAndPaste', () => { page, html`


- +
{ code


- +
{ style="padding-inline-start: calc(40px)">

- +
{ style="padding-inline-start: calc(80px)">

- +
{ style="padding-inline-start: calc(40px)">

- +
{ code


- +
{ page, isPlainText, isCollab, + browserName, }) => { await initialize({isCollab, page}); test.skip(isPlainText); @@ -308,6 +309,10 @@ test.describe('Tables', () => { await page.keyboard.down('Shift'); await page.keyboard.press('ArrowRight'); + // Firefox range selection spans across cells after two arrow key press + if (browserName === 'firefox') { + await page.keyboard.press('ArrowRight'); + } await page.keyboard.press('ArrowDown'); await page.keyboard.up('Shift'); @@ -801,6 +806,7 @@ test.describe('Tables', () => { page, isPlainText, isCollab, + browserName, }) => { await initialize({isCollab, page}); test.skip(isPlainText); @@ -814,6 +820,10 @@ test.describe('Tables', () => { await page.keyboard.down('Shift'); await page.keyboard.press('ArrowRight'); + // Firefox range selection spans across cells after two arrow key press + if (browserName === 'firefox') { + await page.keyboard.press('ArrowRight'); + } await page.keyboard.press('ArrowDown'); await page.keyboard.up('Shift'); @@ -1301,7 +1311,8 @@ test.describe('Tables', () => { page, html`


- +
{ false, ); - await assertIsGridSelection(page); - await assertGridSelectionCoordinates(page, { anchor: {x: 2, y: 1}, focus: {x: 2, y: 2}, diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 64d4182b633..d8ffa4f4ba4 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -36,6 +36,13 @@ export const LEXICAL_IMAGE_BASE64 = export const YOUTUBE_SAMPLE_URL = 'https://www.youtube-nocookie.com/embed/jNQXAC9IVRw'; +function wrapAndSlowDown(method, delay) { + return async function () { + await new Promise((resolve) => setTimeout(resolve, delay)); + return method.apply(this, arguments); + }; +} + export async function initialize({ page, isCollab, @@ -47,6 +54,13 @@ export async function initialize({ tableCellMerge, tableCellBackgroundColor, }) { + // Tests with legacy events often fail to register keypress, so + // slowing it down to reduce flakiness + if (LEGACY_EVENTS) { + page.keyboard.type = wrapAndSlowDown(page.keyboard.type, 50); + page.keyboard.press = wrapAndSlowDown(page.keyboard.press, 50); + } + const appSettings = {}; appSettings.isRichText = IS_RICH_TEXT; appSettings.emptyEditor = true; @@ -90,7 +104,7 @@ async function exposeLexicalEditor(page) { await leftFrame.waitForSelector('.tree-view-output pre'); await leftFrame.evaluate(() => { window.lexicalEditor = document.querySelector( - '.tree-view-output pre', + '[data-lexical-editor="true"]', ).__lexicalEditor; }); } @@ -202,22 +216,16 @@ async function retryAsync(page, fn, attempts) { } } -export async function assertIsGridSelection(page, coordinates) { - const gridKey = await page.evaluate(() => { - const editor = window.lexicalEditor; - const editorState = editor.getEditorState(); - const selection = editorState._selection; - return selection.gridKey; - }); - - expect(gridKey).not.toBeUndefined(); -} - export async function assertGridSelectionCoordinates(page, coordinates) { - const {_anchor, _focus} = await page.evaluate(() => { + const pageOrFrame = IS_COLLAB ? await page.frame('left') : page; + + const {_anchor, _focus} = await pageOrFrame.evaluate(() => { const editor = window.lexicalEditor; const editorState = editor.getEditorState(); const selection = editorState._selection; + if (!selection.gridKey) { + throw new Error('Expected grid selection'); + } const anchorElement = editor.getElementByKey(selection.anchor.key); const focusElement = editor.getElementByKey(selection.focus.key); return { @@ -826,11 +834,7 @@ export async function selectCellsFromTableCords( ); // Focus on inside the iFrame or the boundingBox() below returns null. - await firstRowFirstColumnCell.click( - // This is a test runner quirk. Chrome seems to need two clicks to focus on the - // content editable cell before dragging, but Firefox treats it as a double click event. - E2E_BROWSER === 'chromium' ? {clickCount: 2} : {}, - ); + await firstRowFirstColumnCell.click(); await dragMouse( page, diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index c8fe2c44425..8c3ef1915c9 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -1246,23 +1246,6 @@ i.prettier-error { background-image: url(images/icons/plug-fill.svg); } -table.disable-selection { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -table.disable-selection span::selection { - background-color: transparent; -} - -table.disable-selection br::selection { - background-color: transparent; -} - .table-cell-action-button-container { position: absolute; top: 0; diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 28328c86241..11a29e171d9 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -125,6 +125,9 @@ width: calc(100% - 25px); margin: 30px 0; } +.PlaygroundEditorTheme__tableSelection *::selection { + background-color: transparent; +} .PlaygroundEditorTheme__tableSelected { outline: 2px solid rgb(60, 132, 244); } @@ -135,7 +138,6 @@ text-align: start; padding: 6px 8px; position: relative; - cursor: default; outline: none; } .PlaygroundEditorTheme__tableCellSortedIndicator { diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index a4e9cb9378b..48c43eecd06 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -100,6 +100,7 @@ const theme: EditorThemeClasses = { tableCellSortedIndicator: 'PlaygroundEditorTheme__tableCellSortedIndicator', tableResizeRuler: 'PlaygroundEditorTheme__tableCellResizeRuler', tableSelected: 'PlaygroundEditorTheme__tableSelected', + tableSelection: 'PlaygroundEditorTheme__tableSelection', text: { bold: 'PlaygroundEditorTheme__textBold', code: 'PlaygroundEditorTheme__textCode', diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts index c877ff77d3f..d23d564e167 100644 --- a/packages/lexical-react/src/LexicalTablePlugin.ts +++ b/packages/lexical-react/src/LexicalTablePlugin.ts @@ -55,9 +55,11 @@ function $insertFirst(parent: ElementNode, node: LexicalNode): void { export function TablePlugin({ hasCellMerge = true, hasCellBackgroundColor = true, + hasTabHandler = true, }: { hasCellMerge?: boolean; hasCellBackgroundColor?: boolean; + hasTabHandler?: boolean; }): JSX.Element | null { const [editor] = useLexicalComposerContext(); @@ -103,6 +105,7 @@ export function TablePlugin({ tableNode, tableElement, editor, + hasTabHandler, ); tableSelections.set(nodeKey, tableSelection); } @@ -150,7 +153,7 @@ export function TablePlugin({ tableSelection.removeListeners(); } }; - }, [editor]); + }, [editor, hasTabHandler]); // Unmerge cells when the feature isn't enabled useEffect(() => { diff --git a/packages/lexical-table/src/LexicalTableSelection.ts b/packages/lexical-table/src/LexicalTableSelection.ts index a6c1c27bab8..c1cc1f90899 100644 --- a/packages/lexical-table/src/LexicalTableSelection.ts +++ b/packages/lexical-table/src/LexicalTableSelection.ts @@ -13,6 +13,10 @@ import type { TextFormatType, } from 'lexical'; +import { + addClassNamesToElement, + removeClassNamesFromElement, +} from '@lexical/utils'; import { $createParagraphNode, $createRangeSelection, @@ -37,8 +41,6 @@ import { getTableGrid, } from './LexicalTableSelectionHelpers'; -export const BACKGROUND_COLOR = 'background-color'; -export const BACKGROUND_IMAGE = 'background-image'; export type Cell = { elem: HTMLElement; highlighted: boolean; @@ -197,6 +199,10 @@ export class TableSelection { throw new Error('Expected to find TableElement in DOM'); } + removeClassNamesFromElement( + tableElement, + editor._config.theme.tableSelection, + ); tableElement.classList.remove('disable-selection'); this.hasHijackedSelectionStyles = false; }); @@ -211,7 +217,7 @@ export class TableSelection { throw new Error('Expected to find TableElement in DOM'); } - tableElement.classList.add('disable-selection'); + addClassNamesToElement(tableElement, editor._config.theme.tableSelection); this.hasHijackedSelectionStyles = true; }); } diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index 726b071315c..2d268917e60 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -18,16 +18,14 @@ import type { TextFormatType, } from 'lexical'; -import {TableCellNode} from '@lexical/table'; +import {$isTableNode, TableCellNode} from '@lexical/table'; import {$findMatchingParent} from '@lexical/utils'; import { $createParagraphNode, - $createRangeSelection, $getNearestNodeFromDOMNode, $getPreviousSelection, $getSelection, $isElementNode, - $isParagraphNode, $isRangeSelection, $setSelection, COMMAND_PRIORITY_CRITICAL, @@ -36,8 +34,6 @@ import { DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, - DEPRECATED_$createGridSelection, - DEPRECATED_$isGridNode, DEPRECATED_$isGridSelection, FOCUS_COMMAND, FORMAT_TEXT_COMMAND, @@ -47,6 +43,7 @@ import { KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, + KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical'; @@ -61,6 +58,7 @@ export function applyTableHandlers( tableNode: TableNode, tableElement: HTMLTableElementWithWithTableSelectionState, editor: LexicalEditor, + hasTabHandler: boolean, ): TableSelection { const rootElement = editor.getRootElement(); @@ -69,76 +67,46 @@ export function applyTableHandlers( } const tableSelection = new TableSelection(editor, tableNode.getKey()); + const editorWindow = editor._window || window; attachTableSelectionToTableElement(tableElement, tableSelection); - let isMouseDown = false; - let isRangeSelectionHijacked = false; - - tableElement.addEventListener('dblclick', (event: MouseEvent) => { - const cell = getCellFromTarget(event.target as Node); - - if (cell !== null) { - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - tableSelection.setAnchorCellForSelection(cell); - tableSelection.setFocusCellForSelection(cell, true); - isMouseDown = false; - } - }); - - // This is the anchor of the selection. tableElement.addEventListener('mousedown', (event: MouseEvent) => { setTimeout(() => { if (event.button !== 0) { return; } - const cell = getCellFromTarget(event.target as Node); - - if (cell !== null) { - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - tableSelection.setAnchorCellForSelection(cell); + if (!editorWindow) { + return; } - }, 0); - }); - - // This is adjusting the focus of the selection. - tableElement.addEventListener('mousemove', (event: MouseEvent) => { - if (isRangeSelectionHijacked) { - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - } - if (isMouseDown) { - const cell = getCellFromTarget(event.target as Node); + const anchorCell = getCellFromTarget(event.target as Node); + if (anchorCell !== null) { + stopEvent(event); + tableSelection.setAnchorCellForSelection(anchorCell); + } - if (cell !== null) { - const cellX = cell.x; - const cellY = cell.y; + const onMouseUp = () => { + editorWindow.removeEventListener('mouseup', onMouseUp); + editorWindow.removeEventListener('mousemove', onMouseMove); + }; + const onMouseMove = (moveEvent: MouseEvent) => { + const focusCell = getCellFromTarget(moveEvent.target as Node); if ( - isMouseDown && - (tableSelection.anchorX !== cellX || - tableSelection.anchorY !== cellY || - tableSelection.isHighlightingCells) + focusCell !== null && + (tableSelection.anchorX !== focusCell.x || + tableSelection.anchorY !== focusCell.y) ) { - event.preventDefault(); - tableSelection.setFocusCellForSelection(cell); + moveEvent.preventDefault(); + tableSelection.setFocusCellForSelection(focusCell); } - } - } - }); + }; - // Select entire table at this point, when grid selection is ready. - tableElement.addEventListener('mouseleave', () => { - if (isMouseDown) { - return; - } + editorWindow.addEventListener('mouseup', onMouseUp); + editorWindow.addEventListener('mousemove', onMouseMove); + }, 0); }); // Clear selection when clicking outside of dom. @@ -149,153 +117,28 @@ export function applyTableHandlers( editor.update(() => { const selection = $getSelection(); - const target = event.target; - if (target instanceof Node) { - if ( - DEPRECATED_$isGridSelection(selection) && - selection.gridKey === tableSelection.tableNodeKey && - rootElement.contains(target) - ) { - tableSelection.clearHighlight(); - } - // TODO Revise this logic; the UX selection boundaries and nested editors - const node = $getNearestNodeFromDOMNode(target); - if ( - node !== null && - $findMatchingParent(node, DEPRECATED_$isGridNode) - ) { - isMouseDown = true; - } + const target = event.target as Node; + if ( + DEPRECATED_$isGridSelection(selection) && + selection.gridKey === tableSelection.tableNodeKey && + rootElement.contains(target) + ) { + tableSelection.clearHighlight(); } }); }; - window.addEventListener('mousedown', mouseDownCallback); - - tableSelection.listenersToRemove.add(() => - window.removeEventListener('mousedown', mouseDownCallback), - ); - - const mouseUpCallback = (event: MouseEvent) => { - if (isMouseDown && !doesTargetContainText(event.target as Node)) { - event.preventDefault(); - event.stopPropagation(); - } - - isMouseDown = false; - }; - - window.addEventListener('mouseup', mouseUpCallback); - tableSelection.listenersToRemove.add(() => - window.removeEventListener('mouseup', mouseUpCallback), - ); + editorWindow.addEventListener('mousedown', mouseDownCallback); - tableElement.addEventListener('mouseup', mouseUpCallback); tableSelection.listenersToRemove.add(() => - tableElement.removeEventListener('mouseup', mouseUpCallback), + editorWindow.removeEventListener('mousedown', mouseDownCallback), ); tableSelection.listenersToRemove.add( editor.registerCommand( KEY_ARROW_DOWN_COMMAND, - (event) => { - const selection = $getSelection(); - - if (!$isSelectionInTable(selection, tableNode)) { - return false; - } - - const direction = 'down'; - - if ($isRangeSelection(selection)) { - if (selection.isCollapsed()) { - const tableCellNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isTableCellNode(n), - ); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - const elementParentNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isElementNode(n), - ); - - if (elementParentNode == null) { - throw new Error('Expected BlockNode Parent'); - } - - const lastChild = tableCellNode.getLastChild(); - const isSelectionInLastBlock = - (lastChild && elementParentNode.isParentOf(lastChild)) || - elementParentNode === lastChild; - - if (isSelectionInLastBlock || event.shiftKey) { - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - // Start Selection - if (event.shiftKey) { - tableSelection.setAnchorCellForSelection( - tableNode.getCellFromCordsOrThrow( - currentCords.x, - currentCords.y, - tableSelection.grid, - ), - ); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return selectGridNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - } - } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { - const tableCellNode = selection.focus.getNode(); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return false; - }, + (event) => + $handleArrowKey(editor, event, 'down', tableNode, tableSelection), COMMAND_PRIORITY_HIGH, ), ); @@ -303,104 +146,8 @@ export function applyTableHandlers( tableSelection.listenersToRemove.add( editor.registerCommand( KEY_ARROW_UP_COMMAND, - (event) => { - const selection = $getSelection(); - - if (!$isSelectionInTable(selection, tableNode)) { - return false; - } - - const direction = 'up'; - - if ($isRangeSelection(selection)) { - if (selection.isCollapsed()) { - const tableCellNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isTableCellNode(n), - ); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - const elementParentNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isElementNode(n), - ); - - if (elementParentNode == null) { - throw new Error('Expected BlockNode Parent'); - } - - const lastChild = tableCellNode.getLastChild(); - const isSelectionInLastBlock = - (lastChild && elementParentNode.isParentOf(lastChild)) || - elementParentNode === lastChild; - - if (isSelectionInLastBlock || event.shiftKey) { - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - // Start Selection - if (event.shiftKey) { - tableSelection.setAnchorCellForSelection( - tableNode.getCellFromCordsOrThrow( - currentCords.x, - currentCords.y, - tableSelection.grid, - ), - ); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return selectGridNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - } - } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { - const tableCellNode = selection.focus.getNode(); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return false; - }, + (event) => + $handleArrowKey(editor, event, 'up', tableNode, tableSelection), COMMAND_PRIORITY_HIGH, ), ); @@ -408,99 +155,8 @@ export function applyTableHandlers( tableSelection.listenersToRemove.add( editor.registerCommand( KEY_ARROW_LEFT_COMMAND, - (event) => { - const selection = $getSelection(); - - if (!$isSelectionInTable(selection, tableNode)) { - return false; - } - - const direction = 'backward'; - - if ($isRangeSelection(selection)) { - if (selection.isCollapsed()) { - const tableCellNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isTableCellNode(n), - ); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - const elementParentNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isElementNode(n), - ); - - if (elementParentNode == null) { - throw new Error('Expected BlockNode Parent'); - } - - if (selection.anchor.offset === 0 || event.shiftKey) { - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - // Start Selection - if (event.shiftKey) { - tableSelection.setAnchorCellForSelection( - tableNode.getCellFromCordsOrThrow( - currentCords.x, - currentCords.y, - tableSelection.grid, - ), - ); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return selectGridNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - } - } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { - const tableCellNode = selection.focus.getNode(); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return false; - }, + (event) => + $handleArrowKey(editor, event, 'backward', tableNode, tableSelection), COMMAND_PRIORITY_HIGH, ), ); @@ -508,99 +164,27 @@ export function applyTableHandlers( tableSelection.listenersToRemove.add( editor.registerCommand( KEY_ARROW_RIGHT_COMMAND, + (event) => + $handleArrowKey(editor, event, 'forward', tableNode, tableSelection), + COMMAND_PRIORITY_HIGH, + ), + ); + + tableSelection.listenersToRemove.add( + editor.registerCommand( + KEY_ESCAPE_COMMAND, (event) => { const selection = $getSelection(); - - if (!$isSelectionInTable(selection, tableNode)) { - return false; - } - - const direction = 'forward'; - - if ($isRangeSelection(selection)) { - if (selection.isCollapsed()) { - const tableCellNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isTableCellNode(n), - ); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - const elementParentNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isElementNode(n), - ); - - if (elementParentNode == null) { - throw new Error('Expected BlockNode Parent'); - } - - if ( - selection.anchor.offset === - selection.anchor.getNode().getTextContentSize() || - event.shiftKey - ) { - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - // Start Selection - if (event.shiftKey) { - tableSelection.setAnchorCellForSelection( - tableNode.getCellFromCordsOrThrow( - currentCords.x, - currentCords.y, - tableSelection.grid, - ), - ); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - - return selectGridNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, - ); - } - } - } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { - const tableCellNode = selection.focus.getNode(); - - if (!$isTableCellNode(tableCellNode)) { - return false; - } - - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); - - event.preventDefault(); - event.stopImmediatePropagation(); - event.stopPropagation(); - - return adjustFocusNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - direction, + if (DEPRECATED_$isGridSelection(selection)) { + const focusCellNode = $findMatchingParent( + selection.focus.getNode(), + $isTableCellNode, ); + if ($isTableCellNode(focusCellNode)) { + stopEvent(event); + focusCellNode.selectEnd(); + return true; + } } return false; @@ -808,50 +392,46 @@ export function applyTableHandlers( ), ); - tableSelection.listenersToRemove.add( - editor.registerCommand( - KEY_TAB_COMMAND, - (event) => { - const selection = $getSelection(); - - if (!$isSelectionInTable(selection, tableNode)) { - return false; - } - - if ($isRangeSelection(selection)) { - const tableCellNode = $findMatchingParent( - selection.anchor.getNode(), - (n) => $isTableCellNode(n), - ); + if (hasTabHandler) { + tableSelection.listenersToRemove.add( + editor.registerCommand( + KEY_TAB_COMMAND, + (event) => { + const selection = $getSelection(); + if ( + !$isRangeSelection(selection) || + !selection.isCollapsed() || + !$isSelectionInTable(selection, tableNode) + ) { + return false; + } - if (!$isTableCellNode(tableCellNode)) { + const tableCellNode = $findCellNode(selection.anchor.getNode()); + if (tableCellNode === null) { return false; } - if (selection.isCollapsed()) { - const currentCords = tableNode.getCordsFromCellNode( - tableCellNode, - tableSelection.grid, - ); + stopEvent(event); - event.preventDefault(); - selectGridNodeInDirection( - tableSelection, - tableNode, - currentCords.x, - currentCords.y, - !event.shiftKey ? 'forward' : 'backward', - ); + const currentCords = tableNode.getCordsFromCellNode( + tableCellNode, + tableSelection.grid, + ); - return true; - } - } + selectGridNodeInDirection( + tableSelection, + tableNode, + currentCords.x, + currentCords.y, + !event.shiftKey ? 'forward' : 'backward', + ); - return false; - }, - COMMAND_PRIORITY_HIGH, - ), - ); + return true; + }, + COMMAND_PRIORITY_CRITICAL, + ), + ); + } tableSelection.listenersToRemove.add( editor.registerCommand( @@ -863,80 +443,61 @@ export function applyTableHandlers( ), ); + function getCellFromCellNode(tableCellNode: TableCellNode): Cell { + const currentCords = tableNode.getCordsFromCellNode( + tableCellNode, + tableSelection.grid, + ); + return tableNode.getCellFromCordsOrThrow( + currentCords.x, + currentCords.y, + tableSelection.grid, + ); + } + tableSelection.listenersToRemove.add( editor.registerCommand( SELECTION_CHANGE_COMMAND, - (payload) => { + () => { const selection = $getSelection(); const prevSelection = $getPreviousSelection(); - if ( - selection && - $isRangeSelection(selection) && - !selection.isCollapsed() - ) { - const anchorNode = selection.anchor.getNode(); - const focusNode = selection.focus.getNode(); - const isAnchorInside = tableNode.isParentOf(anchorNode); - const isFocusInside = tableNode.isParentOf(focusNode); - - const selectionContainsPartialTable = - (isAnchorInside && !isFocusInside) || - (isFocusInside && !isAnchorInside); - - const selectionIsInsideTable = - isAnchorInside && isFocusInside && !tableNode.isSelected(); - - if (selectionContainsPartialTable) { - const isBackward = selection.isBackward(); - const modifiedSelection = $createRangeSelection(); - const tableKey = tableNode.getKey(); - - modifiedSelection.anchor.set( - selection.anchor.key, - selection.anchor.offset, - selection.anchor.type, - ); - - modifiedSelection.focus.set( - tableKey, + if ($isRangeSelection(selection)) { + const {anchor, focus} = selection; + const anchorNode = anchor.getNode(); + const focusNode = focus.getNode(); + // Using explicit comparison with table node to ensure it's not a nested table + // as in that case we'll leave selection resolving to that table + const anchorCellNode = $findCellNode(anchorNode); + const focusCellNode = $findCellNode(focusNode); + const isAnchorInside = + anchorCellNode && tableNode.is($findTableNode(anchorCellNode)); + const isFocusInside = + focusCellNode && tableNode.is($findTableNode(focusCellNode)); + const isPartialyWithinTable = isAnchorInside !== isFocusInside; + const isWithinTable = isAnchorInside && isFocusInside; + const isBackward = selection.isBackward(); + + if (isPartialyWithinTable) { + const newSelection = selection.clone(); + newSelection.focus.set( + tableNode.getKey(), isBackward ? 0 : tableNode.getChildrenSize(), 'element', ); - - isRangeSelectionHijacked = true; - $setSelection(modifiedSelection); + $setSelection(newSelection); $addHighlightStyleToTable(editor, tableSelection); - - return true; - } else if (selectionIsInsideTable) { - const {grid} = tableSelection; - - if ( - selection.getNodes().filter($isTableCellNode).length === - grid.rows * grid.columns - ) { - const gridSelection = DEPRECATED_$createGridSelection(); - const tableKey = tableNode.getKey(); - - const firstCell = tableNode - .getFirstChildOrThrow() - .getFirstChild(); - - const lastCell = tableNode.getLastChildOrThrow().getLastChild(); - - if (firstCell != null && lastCell != null) { - gridSelection.set( - tableKey, - firstCell.getKey(), - lastCell.getKey(), - ); - - $setSelection(gridSelection); - tableSelection.updateTableGridSelection(gridSelection); - - return true; - } + } else if (isWithinTable) { + // Handle case when selection spans across multiple cells but still + // has range selection, then we convert it into grid selection + if (!anchorCellNode.is(focusCellNode)) { + tableSelection.setAnchorCellForSelection( + getCellFromCellNode(anchorCellNode), + ); + tableSelection.setFocusCellForSelection( + getCellFromCellNode(focusCellNode), + true, + ); } } } @@ -969,7 +530,6 @@ export function applyTableHandlers( !tableNode.isSelected() ) { $removeHighlightStyleToTable(editor, tableSelection); - isRangeSelectionHijacked = false; } else if ( !tableSelection.hasHijackedSelectionStyles && tableNode.isSelected() @@ -1113,8 +673,7 @@ export function $updateDOMForSelection( editor: LexicalEditor, grid: Grid, selection: GridSelection | RangeSelection | null, -): Array { - const highlightedCells: Array = []; +) { const selectedCellNodes = new Set(selection ? selection.getNodes() : []); $forEachGridCell(grid, (cell, lexicalNode) => { const elem = cell.elem; @@ -1122,7 +681,6 @@ export function $updateDOMForSelection( if (selectedCellNodes.has(lexicalNode)) { cell.highlighted = true; $addHighlightToDOM(editor, cell); - highlightedCells.push(cell); } else { cell.highlighted = false; $removeHighlightFromDOM(editor, cell); @@ -1131,8 +689,6 @@ export function $updateDOMForSelection( } } }); - - return highlightedCells; } export function $forEachGridCell( @@ -1192,12 +748,14 @@ export function $removeHighlightStyleToTable( }); } +type Direction = 'backward' | 'forward' | 'up' | 'down'; + const selectGridNodeInDirection = ( tableSelection: TableSelection, tableNode: TableNode, x: number, y: number, - direction: 'backward' | 'forward' | 'up' | 'down', + direction: Direction, ): boolean => { const isForward = direction === 'forward'; @@ -1211,6 +769,7 @@ const selectGridNodeInDirection = ( y, tableSelection.grid, ), + isForward, ); } else { if (y !== (isForward ? tableSelection.grid.rows - 1 : 0)) { @@ -1220,6 +779,7 @@ const selectGridNodeInDirection = ( y + (isForward ? 1 : -1), tableSelection.grid, ), + isForward, ); } else if (!isForward) { tableNode.selectPrevious(); @@ -1234,6 +794,7 @@ const selectGridNodeInDirection = ( if (y !== 0) { selectTableCellNode( tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableSelection.grid), + false, ); } else { tableNode.selectPrevious(); @@ -1245,6 +806,7 @@ const selectGridNodeInDirection = ( if (y !== tableSelection.grid.rows - 1) { selectTableCellNode( tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableSelection.grid), + true, ); } else { tableNode.selectNext(); @@ -1261,7 +823,7 @@ const adjustFocusNodeInDirection = ( tableNode: TableNode, x: number, y: number, - direction: 'backward' | 'forward' | 'up' | 'down', + direction: Direction, ): boolean => { const isForward = direction === 'forward'; @@ -1318,13 +880,9 @@ function $isSelectionInTable( return false; } -function selectTableCellNode(tableCell: TableCellNode) { - const possibleParagraph = tableCell - .getChildren() - .find((n) => $isParagraphNode(n)); - - if ($isParagraphNode(possibleParagraph)) { - possibleParagraph.selectEnd(); +function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) { + if (fromStart) { + tableCell.selectStart(); } else { tableCell.selectEnd(); } @@ -1364,3 +922,160 @@ function $removeHighlightFromDOM(editor: LexicalEditor, cell: Cell): void { element.style.removeProperty('background-image'); element.style.removeProperty('caret-color'); } + +function $findCellNode(node: LexicalNode): null | TableCellNode { + const cellNode = $findMatchingParent(node, $isTableCellNode); + return $isTableCellNode(cellNode) ? cellNode : null; +} + +function $findTableNode(node: LexicalNode): null | TableNode { + const tableNode = $findMatchingParent(node, $isTableNode); + return $isTableNode(tableNode) ? tableNode : null; +} + +function $handleArrowKey( + editor: LexicalEditor, + event: KeyboardEvent, + direction: Direction, + tableNode: TableNode, + tableSelection: TableSelection, +): boolean { + const selection = $getSelection(); + + if (!$isSelectionInTable(selection, tableNode)) { + return false; + } + + if ($isRangeSelection(selection) && selection.isCollapsed()) { + // Horizontal move between cels seem to work well without interruption + // so just exit early, and handle vertical moves + if (direction === 'backward' || direction === 'forward') { + return false; + } + + const {anchor, focus} = selection; + const anchorCellNode = $findMatchingParent( + anchor.getNode(), + $isTableCellNode, + ); + const focusCellNode = $findMatchingParent( + focus.getNode(), + $isTableCellNode, + ); + if ( + !$isTableCellNode(anchorCellNode) || + !anchorCellNode.is(focusCellNode) + ) { + return false; + } + + const anchorCellDom = editor.getElementByKey(anchorCellNode.__key); + const anchorDOM = editor.getElementByKey(anchor.key); + if (anchorDOM == null || anchorCellDom == null) { + return false; + } + + let edgeSelectionRect; + if (anchor.type === 'element') { + edgeSelectionRect = anchorDOM.getBoundingClientRect(); + } else { + const domSelection = window.getSelection(); + if (domSelection === null || domSelection.rangeCount === 0) { + return false; + } + + const range = domSelection.getRangeAt(0); + edgeSelectionRect = range.getBoundingClientRect(); + } + + const edgeChild = + direction === 'up' + ? anchorCellNode.getFirstChild() + : anchorCellNode.getLastChild(); + if (edgeChild == null) { + return false; + } + + const edgeChildDOM = editor.getElementByKey(edgeChild.__key); + + if (edgeChildDOM == null) { + return false; + } + + const edgeRect = edgeChildDOM.getBoundingClientRect(); + const isExiting = + direction === 'up' + ? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height + : edgeSelectionRect.bottom + edgeSelectionRect.height > edgeRect.bottom; + + if (isExiting) { + stopEvent(event); + + const cords = tableNode.getCordsFromCellNode( + anchorCellNode, + tableSelection.grid, + ); + + if (event.shiftKey) { + const cell = tableNode.getCellFromCordsOrThrow( + cords.x, + cords.y, + tableSelection.grid, + ); + tableSelection.setAnchorCellForSelection(cell); + tableSelection.setFocusCellForSelection(cell, true); + } else { + return selectGridNodeInDirection( + tableSelection, + tableNode, + cords.x, + cords.y, + direction, + ); + } + + return true; + } + } else if (DEPRECATED_$isGridSelection(selection)) { + const {anchor, focus} = selection; + const anchorCellNode = $findMatchingParent( + anchor.getNode(), + $isTableCellNode, + ); + const focusCellNode = $findMatchingParent( + focus.getNode(), + $isTableCellNode, + ); + if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode)) { + return false; + } + + stopEvent(event); + + if (event.shiftKey) { + const cords = tableNode.getCordsFromCellNode( + focusCellNode, + tableSelection.grid, + ); + return adjustFocusNodeInDirection( + tableSelection, + tableNode, + cords.x, + cords.y, + direction, + ); + } else { + focusCellNode.selectEnd(); + } + + return true; + } + + return false; +} + +function stopEvent(event: Event) { + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); +}