Skip to content

Commit

Permalink
Handle RangeSelection Containing Partial Table Selection (v2) (facebo…
Browse files Browse the repository at this point in the history
…ok#1654)

* Move changes from old branch.

* Revert bad merge.

* Remove LexicalSelection changes

* Clean-up solution
  • Loading branch information
tylerjbainbridge authored Apr 11, 2022
1 parent 760f088 commit 988127c
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 45 deletions.
70 changes: 48 additions & 22 deletions packages/lexical-table/src/LexicalTableSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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');
});
}

Expand Down Expand Up @@ -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;
}
Expand Down
158 changes: 135 additions & 23 deletions packages/lexical-table/src/LexicalTableSelectionHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +37,7 @@ import {
KEY_ARROW_UP_COMMAND,
KEY_BACKSPACE_COMMAND,
KEY_TAB_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';

import {$isTableCellNode} from './LexicalTableCellNode';
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -117,6 +129,7 @@ export function applyTableHandlers(
tableSelection.adjustFocusCellForSelection(cell);
}
}
} else {
}
});

Expand All @@ -135,6 +148,8 @@ export function applyTableHandlers(

// Clear selection when clicking outside of dom.
const mouseDownCallback = (event) => {
isMouseDown = true;

if (event.button !== 0) {
return;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -814,40 +913,53 @@ export function getTableGrid(tableElement: HTMLElement): Grid {

export function $updateDOMForSelection(
grid: Grid,
gridSelection: GridSelection | null,
selection: GridSelection | RangeSelection | null,
): Array<Cell> {
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<Cell> {
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;
}

Expand Down

0 comments on commit 988127c

Please sign in to comment.