diff --git a/packages/lexical-table/src/LexicalTableSelection.js b/packages/lexical-table/src/LexicalTableSelection.js index 286879e7d75..9a776b9bc96 100644 --- a/packages/lexical-table/src/LexicalTableSelection.js +++ b/packages/lexical-table/src/LexicalTableSelection.js @@ -27,6 +27,7 @@ import { $setSelection, SELECTION_CHANGE_COMMAND, } from 'lexical'; +import {CAN_USE_DOM} from 'shared/canUseDOM'; import getDOMSelection from 'shared/getDOMSelection'; import invariant from 'shared/invariant'; @@ -52,21 +53,29 @@ export type Grid = { rows: number, }; -let removeHighlightStyle; - -function createSelectionStyleReset() { - removeHighlightStyle = document.createElement('style'); - removeHighlightStyle.appendChild( - document.createTextNode('::selection{background-color: transparent}'), - ); -} - -function removeSelectionStyleReset() { - const parent = removeHighlightStyle - ? removeHighlightStyle.parentNode - : undefined; - if (parent != null) { - parent.removeChild(removeHighlightStyle); +if (CAN_USE_DOM) { + const disableNativeSelectionUi = document.createElement('style'); + + disableNativeSelectionUi.innerHTML = ` + 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; + } + + .disable-selection span::selection{ + background-color: transparent; + } + .disable-selection br::selection{ + background-color: transparent; + } + `; + + if (document.body) { + document.body.append(disableNativeSelectionUi); } } @@ -185,7 +194,29 @@ export class TableSelection { $setSelection(null); this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND); - removeSelectionStyleReset(); + this.enableHighlightStyle(); + }); + } + + enableHighlightStyle() { + this.editor.update(() => { + const tableElement = this.editor.getElementByKey(this.tableNodeKey); + if (!tableElement) { + throw new Error('Expected to find TableElement in DOM'); + } + + tableElement.classList.remove('disable-selection'); + }); + } + + disableHighlightStyle() { + this.editor.update(() => { + const tableElement = this.editor.getElementByKey(this.tableNodeKey); + if (!tableElement) { + throw new Error('Expected to find TableElement in DOM'); + } + + tableElement.classList.add('disable-selection'); }); } @@ -217,12 +248,7 @@ export class TableSelection { (this.startX !== cellX || this.startY !== cellY || ignoreStart) ) { this.isHighlightingCells = true; - if (document.body) { - if (removeHighlightStyle === undefined) { - createSelectionStyleReset(); - } - document.body.appendChild(removeHighlightStyle); - } + this.disableHighlightStyle(); } else if (cellX === this.currentX && cellY === this.currentY) { return; } diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.js b/packages/lexical-table/src/LexicalTableSelectionHelpers.js index 41d4bbafdcb..6ca5e765361 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.js +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.js @@ -13,16 +13,21 @@ import type { CommandListenerCriticalPriority, GridSelection, LexicalEditor, + LexicalNode, + RangeSelection, } from 'lexical'; import {$findMatchingParent} from '@lexical/utils'; import { + $createRangeSelection, $getNearestNodeFromDOMNode, $getSelection, $isElementNode, $isGridSelection, $isParagraphNode, $isRangeSelection, + $isTextNode, + $setSelection, DELETE_CHARACTER_COMMAND, FORMAT_TEXT_COMMAND, INSERT_TEXT_COMMAND, @@ -32,6 +37,7 @@ import { KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_TAB_COMMAND, + SELECTION_CHANGE_COMMAND, } from 'lexical'; import {$isTableCellNode} from './LexicalTableCellNode'; @@ -57,6 +63,7 @@ export function applyTableHandlers( attachTableSelectionToTableElement(tableElement, tableSelection); let isMouseDown = false; + let isRangeSelectionHijacked = false; tableElement.addEventListener('dblclick', (event: MouseEvent) => { // $FlowFixMe: event.target is always a Node on the DOM @@ -81,7 +88,6 @@ export function applyTableHandlers( // $FlowFixMe: event.target is always a Node on the DOM const cell = getCellFromTarget(event.target); if (cell !== null) { - isMouseDown = true; tableSelection.setAnchorCellForSelection(cell); document.addEventListener( @@ -100,6 +106,12 @@ export function applyTableHandlers( // This is adjusting the focus of the selection. tableElement.addEventListener('mousemove', (event: MouseEvent) => { + if (isRangeSelectionHijacked) { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + } + if (isMouseDown) { // $FlowFixMe: event.target is always a Node on the DOM const cell = getCellFromTarget(event.target); @@ -117,6 +129,7 @@ export function applyTableHandlers( tableSelection.adjustFocusCellForSelection(cell); } } + } else { } }); @@ -135,6 +148,8 @@ export function applyTableHandlers( // Clear selection when clicking outside of dom. const mouseDownCallback = (event) => { + isMouseDown = true; + if (event.button !== 0) { return; } @@ -158,6 +173,16 @@ export function applyTableHandlers( window.removeEventListener('mousedown', mouseDownCallback), ); + const mouseUpCallback = (event) => { + isMouseDown = false; + }; + + window.addEventListener('mouseup', mouseUpCallback); + + tableSelection.listenersToRemove.add(() => + window.removeEventListener('mouseup', mouseUpCallback), + ); + tableSelection.listenersToRemove.add( editor.registerCommand( KEY_ARROW_DOWN_COMMAND, @@ -716,6 +741,80 @@ export function applyTableHandlers( CriticalPriority, ), ); + + tableSelection.listenersToRemove.add( + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (payload) => { + const selection = $getSelection(); + 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 containsPartialTable = + (isAnchorInside && !isFocusInside) || + (isFocusInside && !isAnchorInside); + if (containsPartialTable) { + const isBackward = selection.isBackward(); + const startNode = isBackward ? focusNode : anchorNode; + const modifiedSelection = $createRangeSelection(); + const tableIndex = tableNode.getIndexWithinParent(); + const parentKey = tableNode.getParentOrThrow().getKey(); + isRangeSelectionHijacked = true; + tableSelection.disableHighlightStyle(); + (isBackward + ? modifiedSelection.focus + : modifiedSelection.anchor + ).set( + startNode.getKey(), + (isBackward ? selection.focus : selection.anchor).offset, + $isTextNode(startNode) ? 'text' : 'element', + ); + (isBackward + ? modifiedSelection.anchor + : modifiedSelection.focus + ).set( + parentKey, + isBackward ? tableIndex - 1 : tableIndex + 1, + 'element', + ); + $setSelection(modifiedSelection); + $forEachGridCell(tableSelection.grid, (cell) => { + const elem = cell.elem; + cell.highlighted = true; + elem.style.setProperty('background-color', 'rgb(172, 206, 247)'); + elem.style.setProperty('caret-color', 'transparent'); + }); + return true; + } + } + + if (isRangeSelectionHijacked && !tableNode.isSelected()) { + tableSelection.enableHighlightStyle(); + $forEachGridCell(tableSelection.grid, (cell) => { + const elem = cell.elem; + cell.highlighted = false; + elem.style.removeProperty('background-color'); + elem.style.removeProperty('caret-color'); + + if (!elem.getAttribute('style')) { + elem.removeAttribute('style'); + } + }); + isRangeSelectionHijacked = false; + return true; + } + + return false; + }, + CriticalPriority, + ), + ); return tableSelection; } @@ -814,40 +913,53 @@ export function getTableGrid(tableElement: HTMLElement): Grid { export function $updateDOMForSelection( grid: Grid, - gridSelection: GridSelection | null, + selection: GridSelection | RangeSelection | null, ): Array { const highlightedCells = []; - const {cells} = grid; + const selectedCellNodes = new Set(selection ? selection.getNodes() : []); - const selectedCellNodes = new Set( - gridSelection ? gridSelection.getNodes() : [], - ); + $forEachGridCell(grid, (cell, lexicalNode) => { + const elem = cell.elem; + + if (selectedCellNodes.has(lexicalNode)) { + cell.highlighted = true; + elem.style.setProperty('background-color', 'rgb(172, 206, 247)'); + elem.style.setProperty('caret-color', 'transparent'); + highlightedCells.push(cell); + } else { + cell.highlighted = false; + elem.style.removeProperty('background-color'); + elem.style.removeProperty('caret-color'); + + if (!elem.getAttribute('style')) { + elem.removeAttribute('style'); + } + } + }); + + return highlightedCells; +} +export function $forEachGridCell( + grid: Grid, + cb: ( + cell: Cell, + lexicalNode: LexicalNode, + cords: {x: number, y: number}, + ) => void, +): Array { + const highlightedCells = []; + const {cells} = grid; for (let y = 0; y < cells.length; y++) { const row = cells[y]; - for (let x = 0; x < row.length; x++) { const cell = row[x]; - const elemStyle = cell.elem.style; const lexicalNode = $getNearestNodeFromDOMNode(cell.elem); - - if (lexicalNode && selectedCellNodes.has(lexicalNode)) { - cell.highlighted = true; - elemStyle.setProperty('background-color', 'rgb(172, 206, 247)'); - elemStyle.setProperty('caret-color', 'transparent'); - highlightedCells.push(cell); - } else { - cell.highlighted = false; - elemStyle.removeProperty('background-color'); - elemStyle.removeProperty('caret-color'); - - if (!cell.elem.getAttribute('style')) { - cell.elem.removeAttribute('style'); - } + if (lexicalNode !== null) { + cb(cell, lexicalNode, {x, y}); } } } - return highlightedCells; }