2427 lines
85 KiB
JavaScript
2427 lines
85 KiB
JavaScript
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
var utils = require('@lexical/utils');
|
||
|
var lexical = require('lexical');
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
const TableCellHeaderStates = {
|
||
|
BOTH: 3,
|
||
|
COLUMN: 2,
|
||
|
NO_STATUS: 0,
|
||
|
ROW: 1
|
||
|
};
|
||
|
/** @noInheritDoc */
|
||
|
class TableCellNode extends lexical.ElementNode {
|
||
|
/** @internal */
|
||
|
|
||
|
/** @internal */
|
||
|
|
||
|
/** @internal */
|
||
|
|
||
|
/** @internal */
|
||
|
|
||
|
/** @internal */
|
||
|
|
||
|
static getType() {
|
||
|
return 'tablecell';
|
||
|
}
|
||
|
static clone(node) {
|
||
|
const cellNode = new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
|
||
|
cellNode.__rowSpan = node.__rowSpan;
|
||
|
cellNode.__backgroundColor = node.__backgroundColor;
|
||
|
return cellNode;
|
||
|
}
|
||
|
static importDOM() {
|
||
|
return {
|
||
|
td: node => ({
|
||
|
conversion: convertTableCellNodeElement,
|
||
|
priority: 0
|
||
|
}),
|
||
|
th: node => ({
|
||
|
conversion: convertTableCellNodeElement,
|
||
|
priority: 0
|
||
|
})
|
||
|
};
|
||
|
}
|
||
|
static importJSON(serializedNode) {
|
||
|
const colSpan = serializedNode.colSpan || 1;
|
||
|
const rowSpan = serializedNode.rowSpan || 1;
|
||
|
const cellNode = $createTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined);
|
||
|
cellNode.__rowSpan = rowSpan;
|
||
|
cellNode.__backgroundColor = serializedNode.backgroundColor || null;
|
||
|
return cellNode;
|
||
|
}
|
||
|
constructor(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width, key) {
|
||
|
super(key);
|
||
|
this.__colSpan = colSpan;
|
||
|
this.__rowSpan = 1;
|
||
|
this.__headerState = headerState;
|
||
|
this.__width = width;
|
||
|
this.__backgroundColor = null;
|
||
|
}
|
||
|
createDOM(config) {
|
||
|
const element = document.createElement(this.getTag());
|
||
|
if (this.__width) {
|
||
|
element.style.width = `${this.__width}px`;
|
||
|
}
|
||
|
if (this.__colSpan > 1) {
|
||
|
element.colSpan = this.__colSpan;
|
||
|
}
|
||
|
if (this.__rowSpan > 1) {
|
||
|
element.rowSpan = this.__rowSpan;
|
||
|
}
|
||
|
if (this.__backgroundColor !== null) {
|
||
|
element.style.backgroundColor = this.__backgroundColor;
|
||
|
}
|
||
|
utils.addClassNamesToElement(element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader);
|
||
|
return element;
|
||
|
}
|
||
|
exportDOM(editor) {
|
||
|
const {
|
||
|
element
|
||
|
} = super.exportDOM(editor);
|
||
|
if (element) {
|
||
|
const element_ = element;
|
||
|
const maxWidth = 700;
|
||
|
const colCount = this.getParentOrThrow().getChildrenSize();
|
||
|
element_.style.border = '1px solid black';
|
||
|
if (this.__colSpan > 1) {
|
||
|
element_.colSpan = this.__colSpan;
|
||
|
}
|
||
|
if (this.__rowSpan > 1) {
|
||
|
element_.rowSpan = this.__rowSpan;
|
||
|
}
|
||
|
element_.style.width = `${this.getWidth() || Math.max(90, maxWidth / colCount)}px`;
|
||
|
element_.style.verticalAlign = 'top';
|
||
|
element_.style.textAlign = 'start';
|
||
|
const backgroundColor = this.getBackgroundColor();
|
||
|
if (backgroundColor !== null) {
|
||
|
element_.style.backgroundColor = backgroundColor;
|
||
|
} else if (this.hasHeader()) {
|
||
|
element_.style.backgroundColor = '#f2f3f5';
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
element
|
||
|
};
|
||
|
}
|
||
|
exportJSON() {
|
||
|
return {
|
||
|
...super.exportJSON(),
|
||
|
backgroundColor: this.getBackgroundColor(),
|
||
|
colSpan: this.__colSpan,
|
||
|
headerState: this.__headerState,
|
||
|
rowSpan: this.__rowSpan,
|
||
|
type: 'tablecell',
|
||
|
width: this.getWidth()
|
||
|
};
|
||
|
}
|
||
|
getColSpan() {
|
||
|
return this.__colSpan;
|
||
|
}
|
||
|
setColSpan(colSpan) {
|
||
|
this.getWritable().__colSpan = colSpan;
|
||
|
return this;
|
||
|
}
|
||
|
getRowSpan() {
|
||
|
return this.__rowSpan;
|
||
|
}
|
||
|
setRowSpan(rowSpan) {
|
||
|
this.getWritable().__rowSpan = rowSpan;
|
||
|
return this;
|
||
|
}
|
||
|
getTag() {
|
||
|
return this.hasHeader() ? 'th' : 'td';
|
||
|
}
|
||
|
setHeaderStyles(headerState) {
|
||
|
const self = this.getWritable();
|
||
|
self.__headerState = headerState;
|
||
|
return this.__headerState;
|
||
|
}
|
||
|
getHeaderStyles() {
|
||
|
return this.getLatest().__headerState;
|
||
|
}
|
||
|
setWidth(width) {
|
||
|
const self = this.getWritable();
|
||
|
self.__width = width;
|
||
|
return this.__width;
|
||
|
}
|
||
|
getWidth() {
|
||
|
return this.getLatest().__width;
|
||
|
}
|
||
|
getBackgroundColor() {
|
||
|
return this.getLatest().__backgroundColor;
|
||
|
}
|
||
|
setBackgroundColor(newBackgroundColor) {
|
||
|
this.getWritable().__backgroundColor = newBackgroundColor;
|
||
|
}
|
||
|
toggleHeaderStyle(headerStateToToggle) {
|
||
|
const self = this.getWritable();
|
||
|
if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
|
||
|
self.__headerState -= headerStateToToggle;
|
||
|
} else {
|
||
|
self.__headerState += headerStateToToggle;
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
hasHeaderState(headerState) {
|
||
|
return (this.getHeaderStyles() & headerState) === headerState;
|
||
|
}
|
||
|
hasHeader() {
|
||
|
return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
|
||
|
}
|
||
|
updateDOM(prevNode) {
|
||
|
return prevNode.__headerState !== this.__headerState || prevNode.__width !== this.__width || prevNode.__colSpan !== this.__colSpan || prevNode.__rowSpan !== this.__rowSpan || prevNode.__backgroundColor !== this.__backgroundColor;
|
||
|
}
|
||
|
isShadowRoot() {
|
||
|
return true;
|
||
|
}
|
||
|
collapseAtStart() {
|
||
|
return true;
|
||
|
}
|
||
|
canBeEmpty() {
|
||
|
return false;
|
||
|
}
|
||
|
canIndent() {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
function convertTableCellNodeElement(domNode) {
|
||
|
const domNode_ = domNode;
|
||
|
const nodeName = domNode.nodeName.toLowerCase();
|
||
|
let width = undefined;
|
||
|
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
|
||
|
width = parseFloat(domNode_.style.width);
|
||
|
}
|
||
|
const tableCellNode = $createTableCellNode(nodeName === 'th' ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width);
|
||
|
tableCellNode.__rowSpan = domNode_.rowSpan;
|
||
|
const backgroundColor = domNode_.style.backgroundColor;
|
||
|
if (backgroundColor !== '') {
|
||
|
tableCellNode.__backgroundColor = backgroundColor;
|
||
|
}
|
||
|
return {
|
||
|
forChild: (lexicalNode, parentLexicalNode) => {
|
||
|
if ($isTableCellNode(parentLexicalNode) && !lexical.$isElementNode(lexicalNode)) {
|
||
|
const paragraphNode = lexical.$createParagraphNode();
|
||
|
if (lexical.$isLineBreakNode(lexicalNode) && lexicalNode.getTextContent() === '\n') {
|
||
|
return null;
|
||
|
}
|
||
|
paragraphNode.append(lexicalNode);
|
||
|
return paragraphNode;
|
||
|
}
|
||
|
return lexicalNode;
|
||
|
},
|
||
|
node: tableCellNode
|
||
|
};
|
||
|
}
|
||
|
function $createTableCellNode(headerState, colSpan = 1, width) {
|
||
|
return lexical.$applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
|
||
|
}
|
||
|
function $isTableCellNode(node) {
|
||
|
return node instanceof TableCellNode;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
const INSERT_TABLE_COMMAND = lexical.createCommand('INSERT_TABLE_COMMAND');
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
/** @noInheritDoc */
|
||
|
class TableRowNode extends lexical.ElementNode {
|
||
|
/** @internal */
|
||
|
|
||
|
static getType() {
|
||
|
return 'tablerow';
|
||
|
}
|
||
|
static clone(node) {
|
||
|
return new TableRowNode(node.__height, node.__key);
|
||
|
}
|
||
|
static importDOM() {
|
||
|
return {
|
||
|
tr: node => ({
|
||
|
conversion: convertTableRowElement,
|
||
|
priority: 0
|
||
|
})
|
||
|
};
|
||
|
}
|
||
|
static importJSON(serializedNode) {
|
||
|
return $createTableRowNode(serializedNode.height);
|
||
|
}
|
||
|
constructor(height, key) {
|
||
|
super(key);
|
||
|
this.__height = height;
|
||
|
}
|
||
|
exportJSON() {
|
||
|
return {
|
||
|
...super.exportJSON(),
|
||
|
type: 'tablerow',
|
||
|
version: 1
|
||
|
};
|
||
|
}
|
||
|
createDOM(config) {
|
||
|
const element = document.createElement('tr');
|
||
|
if (this.__height) {
|
||
|
element.style.height = `${this.__height}px`;
|
||
|
}
|
||
|
utils.addClassNamesToElement(element, config.theme.tableRow);
|
||
|
return element;
|
||
|
}
|
||
|
isShadowRoot() {
|
||
|
return true;
|
||
|
}
|
||
|
setHeight(height) {
|
||
|
const self = this.getWritable();
|
||
|
self.__height = height;
|
||
|
return this.__height;
|
||
|
}
|
||
|
getHeight() {
|
||
|
return this.getLatest().__height;
|
||
|
}
|
||
|
updateDOM(prevNode) {
|
||
|
return prevNode.__height !== this.__height;
|
||
|
}
|
||
|
canBeEmpty() {
|
||
|
return false;
|
||
|
}
|
||
|
canIndent() {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
function convertTableRowElement(domNode) {
|
||
|
const domNode_ = domNode;
|
||
|
let height = undefined;
|
||
|
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
|
||
|
height = parseFloat(domNode_.style.height);
|
||
|
}
|
||
|
return {
|
||
|
node: $createTableRowNode(height)
|
||
|
};
|
||
|
}
|
||
|
function $createTableRowNode(height) {
|
||
|
return lexical.$applyNodeReplacement(new TableRowNode(height));
|
||
|
}
|
||
|
function $isTableRowNode(node) {
|
||
|
return node instanceof TableRowNode;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
function $createTableNodeWithDimensions(rowCount, columnCount, includeHeaders = true) {
|
||
|
const tableNode = $createTableNode();
|
||
|
for (let iRow = 0; iRow < rowCount; iRow++) {
|
||
|
const tableRowNode = $createTableRowNode();
|
||
|
for (let iColumn = 0; iColumn < columnCount; iColumn++) {
|
||
|
let headerState = TableCellHeaderStates.NO_STATUS;
|
||
|
if (typeof includeHeaders === 'object') {
|
||
|
if (iRow === 0 && includeHeaders.rows) headerState |= TableCellHeaderStates.ROW;
|
||
|
if (iColumn === 0 && includeHeaders.columns) headerState |= TableCellHeaderStates.COLUMN;
|
||
|
} else if (includeHeaders) {
|
||
|
if (iRow === 0) headerState |= TableCellHeaderStates.ROW;
|
||
|
if (iColumn === 0) headerState |= TableCellHeaderStates.COLUMN;
|
||
|
}
|
||
|
const tableCellNode = $createTableCellNode(headerState);
|
||
|
const paragraphNode = lexical.$createParagraphNode();
|
||
|
paragraphNode.append(lexical.$createTextNode());
|
||
|
tableCellNode.append(paragraphNode);
|
||
|
tableRowNode.append(tableCellNode);
|
||
|
}
|
||
|
tableNode.append(tableRowNode);
|
||
|
}
|
||
|
return tableNode;
|
||
|
}
|
||
|
function $getTableCellNodeFromLexicalNode(startingNode) {
|
||
|
const node = utils.$findMatchingParent(startingNode, n => $isTableCellNode(n));
|
||
|
if ($isTableCellNode(node)) {
|
||
|
return node;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
function $getTableRowNodeFromTableCellNodeOrThrow(startingNode) {
|
||
|
const node = utils.$findMatchingParent(startingNode, n => $isTableRowNode(n));
|
||
|
if ($isTableRowNode(node)) {
|
||
|
return node;
|
||
|
}
|
||
|
throw new Error('Expected table cell to be inside of table row.');
|
||
|
}
|
||
|
function $getTableNodeFromLexicalNodeOrThrow(startingNode) {
|
||
|
const node = utils.$findMatchingParent(startingNode, n => $isTableNode(n));
|
||
|
if ($isTableNode(node)) {
|
||
|
return node;
|
||
|
}
|
||
|
throw new Error('Expected table cell to be inside of table.');
|
||
|
}
|
||
|
function $getTableRowIndexFromTableCellNode(tableCellNode) {
|
||
|
const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
|
||
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
|
||
|
return tableNode.getChildren().findIndex(n => n.is(tableRowNode));
|
||
|
}
|
||
|
function $getTableColumnIndexFromTableCellNode(tableCellNode) {
|
||
|
const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
|
||
|
return tableRowNode.getChildren().findIndex(n => n.is(tableCellNode));
|
||
|
}
|
||
|
function $getTableCellSiblingsFromTableCellNode(tableCellNode, table) {
|
||
|
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
||
|
const {
|
||
|
x,
|
||
|
y
|
||
|
} = tableNode.getCordsFromCellNode(tableCellNode, table);
|
||
|
return {
|
||
|
above: tableNode.getCellNodeFromCords(x, y - 1, table),
|
||
|
below: tableNode.getCellNodeFromCords(x, y + 1, table),
|
||
|
left: tableNode.getCellNodeFromCords(x - 1, y, table),
|
||
|
right: tableNode.getCellNodeFromCords(x + 1, y, table)
|
||
|
};
|
||
|
}
|
||
|
function $removeTableRowAtIndex(tableNode, indexToDelete) {
|
||
|
const tableRows = tableNode.getChildren();
|
||
|
if (indexToDelete >= tableRows.length || indexToDelete < 0) {
|
||
|
throw new Error('Expected table cell to be inside of table row.');
|
||
|
}
|
||
|
const targetRowNode = tableRows[indexToDelete];
|
||
|
targetRowNode.remove();
|
||
|
return tableNode;
|
||
|
}
|
||
|
function $insertTableRow(tableNode, targetIndex, shouldInsertAfter = true, rowCount, table) {
|
||
|
const tableRows = tableNode.getChildren();
|
||
|
if (targetIndex >= tableRows.length || targetIndex < 0) {
|
||
|
throw new Error('Table row target index out of range');
|
||
|
}
|
||
|
const targetRowNode = tableRows[targetIndex];
|
||
|
if ($isTableRowNode(targetRowNode)) {
|
||
|
for (let r = 0; r < rowCount; r++) {
|
||
|
const tableRowCells = targetRowNode.getChildren();
|
||
|
const tableColumnCount = tableRowCells.length;
|
||
|
const newTableRowNode = $createTableRowNode();
|
||
|
for (let c = 0; c < tableColumnCount; c++) {
|
||
|
const tableCellFromTargetRow = tableRowCells[c];
|
||
|
if (!$isTableCellNode(tableCellFromTargetRow)) {
|
||
|
throw Error(`Expected table cell`);
|
||
|
}
|
||
|
const {
|
||
|
above,
|
||
|
below
|
||
|
} = $getTableCellSiblingsFromTableCellNode(tableCellFromTargetRow, table);
|
||
|
let headerState = TableCellHeaderStates.NO_STATUS;
|
||
|
const width = above && above.getWidth() || below && below.getWidth() || undefined;
|
||
|
if (above && above.hasHeaderState(TableCellHeaderStates.COLUMN) || below && below.hasHeaderState(TableCellHeaderStates.COLUMN)) {
|
||
|
headerState |= TableCellHeaderStates.COLUMN;
|
||
|
}
|
||
|
const tableCellNode = $createTableCellNode(headerState, 1, width);
|
||
|
tableCellNode.append(lexical.$createParagraphNode());
|
||
|
newTableRowNode.append(tableCellNode);
|
||
|
}
|
||
|
if (shouldInsertAfter) {
|
||
|
targetRowNode.insertAfter(newTableRowNode);
|
||
|
} else {
|
||
|
targetRowNode.insertBefore(newTableRowNode);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error('Row before insertion index does not exist.');
|
||
|
}
|
||
|
return tableNode;
|
||
|
}
|
||
|
const getHeaderState = (currentState, possibleState) => {
|
||
|
if (currentState === TableCellHeaderStates.BOTH || currentState === possibleState) {
|
||
|
return possibleState;
|
||
|
}
|
||
|
return TableCellHeaderStates.NO_STATUS;
|
||
|
};
|
||
|
function $insertTableRow__EXPERIMENTAL(insertAfter = true) {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
|
||
|
throw Error(`Expected a RangeSelection or GridSelection`);
|
||
|
}
|
||
|
const focus = selection.focus.getNode();
|
||
|
const [focusCell,, grid] = $getNodeTriplet(focus);
|
||
|
const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell);
|
||
|
const columnCount = gridMap[0].length;
|
||
|
const {
|
||
|
startRow: focusStartRow
|
||
|
} = focusCellMap;
|
||
|
if (insertAfter) {
|
||
|
const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
|
||
|
const focusEndRowMap = gridMap[focusEndRow];
|
||
|
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;
|
||
|
const currentCellHeaderState = currentCell.__headerState;
|
||
|
const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
|
||
|
newRow.append($createTableCellNode(headerState).append(lexical.$createParagraphNode()));
|
||
|
} else {
|
||
|
cell.setRowSpan(cell.__rowSpan + 1);
|
||
|
}
|
||
|
}
|
||
|
const focusEndRowNode = grid.getChildAtIndex(focusEndRow);
|
||
|
if (!$isTableRowNode(focusEndRowNode)) {
|
||
|
throw Error(`focusEndRow is not a TableRowNode`);
|
||
|
}
|
||
|
focusEndRowNode.insertAfter(newRow);
|
||
|
} else {
|
||
|
const focusStartRowMap = gridMap[focusStartRow];
|
||
|
const newRow = $createTableRowNode();
|
||
|
for (let i = 0; i < columnCount; i++) {
|
||
|
const {
|
||
|
cell,
|
||
|
startRow
|
||
|
} = focusStartRowMap[i];
|
||
|
if (startRow === focusStartRow) {
|
||
|
const currentCell = focusStartRowMap[i].cell;
|
||
|
const currentCellHeaderState = currentCell.__headerState;
|
||
|
const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
|
||
|
newRow.append($createTableCellNode(headerState).append(lexical.$createParagraphNode()));
|
||
|
} else {
|
||
|
cell.setRowSpan(cell.__rowSpan + 1);
|
||
|
}
|
||
|
}
|
||
|
const focusStartRowNode = grid.getChildAtIndex(focusStartRow);
|
||
|
if (!$isTableRowNode(focusStartRowNode)) {
|
||
|
throw Error(`focusEndRow is not a TableRowNode`);
|
||
|
}
|
||
|
focusStartRowNode.insertBefore(newRow);
|
||
|
}
|
||
|
}
|
||
|
function $insertTableColumn(tableNode, targetIndex, shouldInsertAfter = true, columnCount, table) {
|
||
|
const tableRows = tableNode.getChildren();
|
||
|
const tableCellsToBeInserted = [];
|
||
|
for (let r = 0; r < tableRows.length; r++) {
|
||
|
const currentTableRowNode = tableRows[r];
|
||
|
if ($isTableRowNode(currentTableRowNode)) {
|
||
|
for (let c = 0; c < columnCount; c++) {
|
||
|
const tableRowChildren = currentTableRowNode.getChildren();
|
||
|
if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
|
||
|
throw new Error('Table column target index out of range');
|
||
|
}
|
||
|
const targetCell = tableRowChildren[targetIndex];
|
||
|
if (!$isTableCellNode(targetCell)) {
|
||
|
throw Error(`Expected table cell`);
|
||
|
}
|
||
|
const {
|
||
|
left,
|
||
|
right
|
||
|
} = $getTableCellSiblingsFromTableCellNode(targetCell, table);
|
||
|
let headerState = TableCellHeaderStates.NO_STATUS;
|
||
|
if (left && left.hasHeaderState(TableCellHeaderStates.ROW) || right && right.hasHeaderState(TableCellHeaderStates.ROW)) {
|
||
|
headerState |= TableCellHeaderStates.ROW;
|
||
|
}
|
||
|
const newTableCell = $createTableCellNode(headerState);
|
||
|
newTableCell.append(lexical.$createParagraphNode());
|
||
|
tableCellsToBeInserted.push({
|
||
|
newTableCell,
|
||
|
targetCell
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
tableCellsToBeInserted.forEach(({
|
||
|
newTableCell,
|
||
|
targetCell
|
||
|
}) => {
|
||
|
if (shouldInsertAfter) {
|
||
|
targetCell.insertAfter(newTableCell);
|
||
|
} else {
|
||
|
targetCell.insertBefore(newTableCell);
|
||
|
}
|
||
|
});
|
||
|
return tableNode;
|
||
|
}
|
||
|
function $insertTableColumn__EXPERIMENTAL(insertAfter = true) {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
|
||
|
throw Error(`Expected a RangeSelection or GridSelection`);
|
||
|
}
|
||
|
const anchor = selection.anchor.getNode();
|
||
|
const focus = selection.focus.getNode();
|
||
|
const [anchorCell] = $getNodeTriplet(anchor);
|
||
|
const [focusCell,, grid] = $getNodeTriplet(focus);
|
||
|
const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
|
||
|
const rowCount = gridMap.length;
|
||
|
const startColumn = insertAfter ? Math.max(focusCellMap.startColumn, anchorCellMap.startColumn) : Math.min(focusCellMap.startColumn, anchorCellMap.startColumn);
|
||
|
const insertAfterColumn = insertAfter ? startColumn + focusCell.__colSpan - 1 : startColumn - 1;
|
||
|
const gridFirstChild = grid.getFirstChild();
|
||
|
if (!$isTableRowNode(gridFirstChild)) {
|
||
|
throw Error(`Expected firstTable child to be a row`);
|
||
|
}
|
||
|
let firstInsertedCell = null;
|
||
|
function $createTableCellNodeForInsertTableColumn(headerState = TableCellHeaderStates.NO_STATUS) {
|
||
|
const cell = $createTableCellNode(headerState).append(lexical.$createParagraphNode());
|
||
|
if (firstInsertedCell === null) {
|
||
|
firstInsertedCell = cell;
|
||
|
}
|
||
|
return cell;
|
||
|
}
|
||
|
let loopRow = gridFirstChild;
|
||
|
rowLoop: for (let i = 0; i < rowCount; i++) {
|
||
|
if (i !== 0) {
|
||
|
const currentRow = loopRow.getNextSibling();
|
||
|
if (!$isTableRowNode(currentRow)) {
|
||
|
throw Error(`Expected row nextSibling to be a row`);
|
||
|
}
|
||
|
loopRow = currentRow;
|
||
|
}
|
||
|
const rowMap = gridMap[i];
|
||
|
const currentCellHeaderState = rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn].cell.__headerState;
|
||
|
const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.ROW);
|
||
|
if (insertAfterColumn < 0) {
|
||
|
$insertFirst(loopRow, $createTableCellNodeForInsertTableColumn(headerState));
|
||
|
continue;
|
||
|
}
|
||
|
const {
|
||
|
cell: currentCell,
|
||
|
startColumn: currentStartColumn,
|
||
|
startRow: currentStartRow
|
||
|
} = rowMap[insertAfterColumn];
|
||
|
if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
|
||
|
let insertAfterCell = currentCell;
|
||
|
let insertAfterCellRowStart = currentStartRow;
|
||
|
let prevCellIndex = insertAfterColumn;
|
||
|
while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
|
||
|
prevCellIndex -= currentCell.__colSpan;
|
||
|
if (prevCellIndex >= 0) {
|
||
|
const {
|
||
|
cell: cell_,
|
||
|
startRow: startRow_
|
||
|
} = rowMap[prevCellIndex];
|
||
|
insertAfterCell = cell_;
|
||
|
insertAfterCellRowStart = startRow_;
|
||
|
} else {
|
||
|
loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
|
||
|
continue rowLoop;
|
||
|
}
|
||
|
}
|
||
|
insertAfterCell.insertAfter($createTableCellNodeForInsertTableColumn(headerState));
|
||
|
} else {
|
||
|
currentCell.setColSpan(currentCell.__colSpan + 1);
|
||
|
}
|
||
|
}
|
||
|
if (firstInsertedCell !== null) {
|
||
|
$moveSelectionToCell(firstInsertedCell);
|
||
|
}
|
||
|
}
|
||
|
function $deleteTableColumn(tableNode, targetIndex) {
|
||
|
const tableRows = tableNode.getChildren();
|
||
|
for (let i = 0; i < tableRows.length; i++) {
|
||
|
const currentTableRowNode = tableRows[i];
|
||
|
if ($isTableRowNode(currentTableRowNode)) {
|
||
|
const tableRowChildren = currentTableRowNode.getChildren();
|
||
|
if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
|
||
|
throw new Error('Table column target index out of range');
|
||
|
}
|
||
|
tableRowChildren[targetIndex].remove();
|
||
|
}
|
||
|
}
|
||
|
return tableNode;
|
||
|
}
|
||
|
function $deleteTableRow__EXPERIMENTAL() {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
|
||
|
throw Error(`Expected a RangeSelection or GridSelection`);
|
||
|
}
|
||
|
const anchor = selection.anchor.getNode();
|
||
|
const focus = selection.focus.getNode();
|
||
|
const [anchorCell,, grid] = $getNodeTriplet(anchor);
|
||
|
const [focusCell] = $getNodeTriplet(focus);
|
||
|
const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(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);
|
||
|
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 (!(nextRowNode !== null)) {
|
||
|
throw Error(`Expected nextRowNode not to be null`);
|
||
|
}
|
||
|
if (column === 0) {
|
||
|
$insertFirst(nextRowNode, cell);
|
||
|
} else {
|
||
|
const {
|
||
|
cell: previousCell
|
||
|
} = nextRow[column - 1];
|
||
|
previousCell.insertAfter(cell);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const rowNode = grid.getChildAtIndex(row);
|
||
|
if (!$isTableRowNode(rowNode)) {
|
||
|
throw Error(`Expected GridNode childAtIndex(${String(row)}) to be RowNode`);
|
||
|
}
|
||
|
rowNode.remove();
|
||
|
}
|
||
|
if (nextRow !== undefined) {
|
||
|
const {
|
||
|
cell
|
||
|
} = nextRow[0];
|
||
|
$moveSelectionToCell(cell);
|
||
|
} else {
|
||
|
const previousRow = gridMap[anchorStartRow - 1];
|
||
|
const {
|
||
|
cell
|
||
|
} = previousRow[0];
|
||
|
$moveSelectionToCell(cell);
|
||
|
}
|
||
|
}
|
||
|
function $deleteTableColumn__EXPERIMENTAL() {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
|
||
|
throw Error(`Expected a RangeSelection or GridSelection`);
|
||
|
}
|
||
|
const anchor = selection.anchor.getNode();
|
||
|
const focus = selection.focus.getNode();
|
||
|
const [anchorCell,, grid] = $getNodeTriplet(anchor);
|
||
|
const [focusCell] = $getNodeTriplet(focus);
|
||
|
const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(grid, anchorCell, focusCell);
|
||
|
const {
|
||
|
startColumn: anchorStartColumn
|
||
|
} = anchorCellMap;
|
||
|
const {
|
||
|
startRow: focusStartRow,
|
||
|
startColumn: focusStartColumn
|
||
|
} = focusCellMap;
|
||
|
const startColumn = Math.min(anchorStartColumn, focusStartColumn);
|
||
|
const endColumn = Math.max(anchorStartColumn + anchorCell.__colSpan - 1, focusStartColumn + focusCell.__colSpan - 1);
|
||
|
const selectedColumnCount = endColumn - startColumn + 1;
|
||
|
const columnCount = gridMap[0].length;
|
||
|
if (columnCount === endColumn - startColumn + 1) {
|
||
|
// Empty grid
|
||
|
grid.selectPrevious();
|
||
|
grid.remove();
|
||
|
return;
|
||
|
}
|
||
|
const rowCount = gridMap.length;
|
||
|
for (let row = 0; row < rowCount; row++) {
|
||
|
for (let column = startColumn; column <= endColumn; column++) {
|
||
|
const {
|
||
|
cell,
|
||
|
startColumn: cellStartColumn
|
||
|
} = gridMap[row][column];
|
||
|
if (cellStartColumn < startColumn) {
|
||
|
if (column === startColumn) {
|
||
|
const overflowLeft = startColumn - cellStartColumn;
|
||
|
// Overflowing left
|
||
|
cell.setColSpan(cell.__colSpan -
|
||
|
// Possible overflow right too
|
||
|
Math.min(selectedColumnCount, cell.__colSpan - overflowLeft));
|
||
|
}
|
||
|
} else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
|
||
|
if (column === endColumn) {
|
||
|
// Overflowing right
|
||
|
const inSelectedArea = endColumn - cellStartColumn + 1;
|
||
|
cell.setColSpan(cell.__colSpan - inSelectedArea);
|
||
|
}
|
||
|
} else {
|
||
|
cell.remove();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const focusRowMap = gridMap[focusStartRow];
|
||
|
const nextColumn = focusRowMap[focusStartColumn + focusCell.__colSpan];
|
||
|
if (nextColumn !== undefined) {
|
||
|
const {
|
||
|
cell
|
||
|
} = nextColumn;
|
||
|
$moveSelectionToCell(cell);
|
||
|
} else {
|
||
|
const previousRow = focusRowMap[focusStartColumn - 1];
|
||
|
const {
|
||
|
cell
|
||
|
} = previousRow;
|
||
|
$moveSelectionToCell(cell);
|
||
|
}
|
||
|
}
|
||
|
function $moveSelectionToCell(cell) {
|
||
|
const firstDescendant = cell.getFirstDescendant();
|
||
|
if (firstDescendant == null) {
|
||
|
cell.selectStart();
|
||
|
} else {
|
||
|
firstDescendant.getParentOrThrow().selectStart();
|
||
|
}
|
||
|
}
|
||
|
function $insertFirst(parent, node) {
|
||
|
const firstChild = parent.getFirstChild();
|
||
|
if (firstChild !== null) {
|
||
|
firstChild.insertBefore(node);
|
||
|
} else {
|
||
|
parent.append(node);
|
||
|
}
|
||
|
}
|
||
|
function $unmergeCell() {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
|
||
|
throw Error(`Expected a RangeSelection or GridSelection`);
|
||
|
}
|
||
|
const anchor = selection.anchor.getNode();
|
||
|
const [cell, row, grid] = $getNodeTriplet(anchor);
|
||
|
const colSpan = cell.__colSpan;
|
||
|
const rowSpan = cell.__rowSpan;
|
||
|
if (colSpan > 1) {
|
||
|
for (let i = 1; i < colSpan; i++) {
|
||
|
cell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS));
|
||
|
}
|
||
|
cell.setColSpan(1);
|
||
|
}
|
||
|
if (rowSpan > 1) {
|
||
|
const [map, cellMap] = $computeTableMap(grid, cell, cell);
|
||
|
const {
|
||
|
startColumn,
|
||
|
startRow
|
||
|
} = cellMap;
|
||
|
let currentRowNode;
|
||
|
for (let i = 1; i < rowSpan; i++) {
|
||
|
const currentRow = startRow + i;
|
||
|
const currentRowMap = map[currentRow];
|
||
|
currentRowNode = (currentRowNode || row).getNextSibling();
|
||
|
if (!$isTableRowNode(currentRowNode)) {
|
||
|
throw Error(`Expected row next sibling to be a row`);
|
||
|
}
|
||
|
let insertAfterCell = null;
|
||
|
for (let column = 0; column < startColumn; column++) {
|
||
|
const currentCellMap = currentRowMap[column];
|
||
|
const currentCell = currentCellMap.cell;
|
||
|
if (currentCellMap.startRow === currentRow) {
|
||
|
insertAfterCell = currentCell;
|
||
|
}
|
||
|
if (currentCell.__colSpan > 1) {
|
||
|
column += currentCell.__colSpan - 1;
|
||
|
}
|
||
|
}
|
||
|
if (insertAfterCell === null) {
|
||
|
for (let j = 0; j < colSpan; j++) {
|
||
|
$insertFirst(currentRowNode, $createTableCellNode(TableCellHeaderStates.NO_STATUS));
|
||
|
}
|
||
|
} else {
|
||
|
for (let j = 0; j < colSpan; j++) {
|
||
|
insertAfterCell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
cell.setRowSpan(1);
|
||
|
}
|
||
|
}
|
||
|
function $computeTableMap(grid, cellA, cellB) {
|
||
|
const tableMap = [];
|
||
|
let cellAValue = null;
|
||
|
let cellBValue = null;
|
||
|
function write(startRow, startColumn, cell) {
|
||
|
const value = {
|
||
|
cell,
|
||
|
startColumn,
|
||
|
startRow
|
||
|
};
|
||
|
const rowSpan = cell.__rowSpan;
|
||
|
const colSpan = cell.__colSpan;
|
||
|
for (let i = 0; i < rowSpan; i++) {
|
||
|
if (tableMap[startRow + i] === undefined) {
|
||
|
tableMap[startRow + i] = [];
|
||
|
}
|
||
|
for (let j = 0; j < colSpan; j++) {
|
||
|
tableMap[startRow + i][startColumn + j] = value;
|
||
|
}
|
||
|
}
|
||
|
if (cellA.is(cell)) {
|
||
|
cellAValue = value;
|
||
|
}
|
||
|
if (cellB.is(cell)) {
|
||
|
cellBValue = value;
|
||
|
}
|
||
|
}
|
||
|
function isEmpty(row, column) {
|
||
|
return tableMap[row] === undefined || tableMap[row][column] === undefined;
|
||
|
}
|
||
|
const gridChildren = grid.getChildren();
|
||
|
for (let i = 0; i < gridChildren.length; i++) {
|
||
|
const row = gridChildren[i];
|
||
|
if (!$isTableRowNode(row)) {
|
||
|
throw Error(`Expected GridNode children to be TableRowNode`);
|
||
|
}
|
||
|
const rowChildren = row.getChildren();
|
||
|
let j = 0;
|
||
|
for (const cell of rowChildren) {
|
||
|
if (!$isTableCellNode(cell)) {
|
||
|
throw Error(`Expected TableRowNode children to be TableCellNode`);
|
||
|
}
|
||
|
while (!isEmpty(i, j)) {
|
||
|
j++;
|
||
|
}
|
||
|
write(i, j, cell);
|
||
|
j += cell.__colSpan;
|
||
|
}
|
||
|
}
|
||
|
if (!(cellAValue !== null)) {
|
||
|
throw Error(`Anchor not found in Grid`);
|
||
|
}
|
||
|
if (!(cellBValue !== null)) {
|
||
|
throw Error(`Focus not found in Grid`);
|
||
|
}
|
||
|
return [tableMap, cellAValue, cellBValue];
|
||
|
}
|
||
|
function $getNodeTriplet(source) {
|
||
|
let cell;
|
||
|
if (source instanceof TableCellNode) {
|
||
|
cell = source;
|
||
|
} else if ('__type' in source) {
|
||
|
const cell_ = utils.$findMatchingParent(source, $isTableCellNode);
|
||
|
if (!$isTableCellNode(cell_)) {
|
||
|
throw Error(`Expected to find a parent TableCellNode`);
|
||
|
}
|
||
|
cell = cell_;
|
||
|
} else {
|
||
|
const cell_ = utils.$findMatchingParent(source.getNode(), $isTableCellNode);
|
||
|
if (!$isTableCellNode(cell_)) {
|
||
|
throw Error(`Expected to find a parent TableCellNode`);
|
||
|
}
|
||
|
cell = cell_;
|
||
|
}
|
||
|
const row = cell.getParent();
|
||
|
if (!$isTableRowNode(row)) {
|
||
|
throw Error(`Expected TableCellNode to have a parent TableRowNode`);
|
||
|
}
|
||
|
const grid = row.getParent();
|
||
|
if (!$isTableNode(grid)) {
|
||
|
throw Error(`Expected TableRowNode to have a parent GridNode`);
|
||
|
}
|
||
|
return [cell, row, grid];
|
||
|
}
|
||
|
function $getTableCellNodeRect(tableCellNode) {
|
||
|
const [cellNode,, gridNode] = $getNodeTriplet(tableCellNode);
|
||
|
const rows = gridNode.getChildren();
|
||
|
const rowCount = rows.length;
|
||
|
const columnCount = rows[0].getChildren().length;
|
||
|
|
||
|
// Create a matrix of the same size as the table to track the position of each cell
|
||
|
const cellMatrix = new Array(rowCount);
|
||
|
for (let i = 0; i < rowCount; i++) {
|
||
|
cellMatrix[i] = new Array(columnCount);
|
||
|
}
|
||
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||
|
const row = rows[rowIndex];
|
||
|
const cells = row.getChildren();
|
||
|
let columnIndex = 0;
|
||
|
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
||
|
// Find the next available position in the matrix, skip the position of merged cells
|
||
|
while (cellMatrix[rowIndex][columnIndex]) {
|
||
|
columnIndex++;
|
||
|
}
|
||
|
const cell = cells[cellIndex];
|
||
|
const rowSpan = cell.__rowSpan || 1;
|
||
|
const colSpan = cell.__colSpan || 1;
|
||
|
|
||
|
// Put the cell into the corresponding position in the matrix
|
||
|
for (let i = 0; i < rowSpan; i++) {
|
||
|
for (let j = 0; j < colSpan; j++) {
|
||
|
cellMatrix[rowIndex + i][columnIndex + j] = cell;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return to the original index, row span and column span of the cell.
|
||
|
if (cellNode === cell) {
|
||
|
return {
|
||
|
colSpan,
|
||
|
columnIndex,
|
||
|
rowIndex,
|
||
|
rowSpan
|
||
|
};
|
||
|
}
|
||
|
columnIndex += colSpan;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
class TableSelection {
|
||
|
constructor(tableKey, anchor, focus) {
|
||
|
this.anchor = anchor;
|
||
|
this.focus = focus;
|
||
|
anchor._selection = this;
|
||
|
focus._selection = this;
|
||
|
this._cachedNodes = null;
|
||
|
this.dirty = false;
|
||
|
this.tableKey = tableKey;
|
||
|
}
|
||
|
getStartEndPoints() {
|
||
|
return [this.anchor, this.focus];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the Selection is "backwards", meaning the focus
|
||
|
* logically precedes the anchor in the EditorState.
|
||
|
* @returns true if the Selection is backwards, false otherwise.
|
||
|
*/
|
||
|
isBackward() {
|
||
|
return this.focus.isBefore(this.anchor);
|
||
|
}
|
||
|
getCachedNodes() {
|
||
|
return this._cachedNodes;
|
||
|
}
|
||
|
setCachedNodes(nodes) {
|
||
|
this._cachedNodes = nodes;
|
||
|
}
|
||
|
is(selection) {
|
||
|
if (!$isTableSelection(selection)) {
|
||
|
return false;
|
||
|
}
|
||
|
return this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
|
||
|
}
|
||
|
set(tableKey, anchorCellKey, focusCellKey) {
|
||
|
this.dirty = true;
|
||
|
this.tableKey = tableKey;
|
||
|
this.anchor.key = anchorCellKey;
|
||
|
this.focus.key = focusCellKey;
|
||
|
this._cachedNodes = null;
|
||
|
}
|
||
|
clone() {
|
||
|
return new TableSelection(this.tableKey, this.anchor, this.focus);
|
||
|
}
|
||
|
isCollapsed() {
|
||
|
return false;
|
||
|
}
|
||
|
extract() {
|
||
|
return this.getNodes();
|
||
|
}
|
||
|
insertRawText(text) {
|
||
|
// Do nothing?
|
||
|
}
|
||
|
insertText() {
|
||
|
// Do nothing?
|
||
|
}
|
||
|
insertNodes(nodes) {
|
||
|
const focusNode = this.focus.getNode();
|
||
|
if (!lexical.$isElementNode(focusNode)) {
|
||
|
throw Error(`Expected TableSelection focus to be an ElementNode`);
|
||
|
}
|
||
|
const selection = lexical.$normalizeSelection__EXPERIMENTAL(focusNode.select(0, focusNode.getChildrenSize()));
|
||
|
selection.insertNodes(nodes);
|
||
|
}
|
||
|
|
||
|
// TODO Deprecate this method. It's confusing when used with colspan|rowspan
|
||
|
getShape() {
|
||
|
const anchorCellNode = lexical.$getNodeByKey(this.anchor.key);
|
||
|
if (!$isTableCellNode(anchorCellNode)) {
|
||
|
throw Error(`Expected TableSelection anchor to be (or a child of) TableCellNode`);
|
||
|
}
|
||
|
const anchorCellNodeRect = $getTableCellNodeRect(anchorCellNode);
|
||
|
if (!(anchorCellNodeRect !== null)) {
|
||
|
throw Error(`getCellRect: expected to find AnchorNode`);
|
||
|
}
|
||
|
const focusCellNode = lexical.$getNodeByKey(this.focus.key);
|
||
|
if (!$isTableCellNode(focusCellNode)) {
|
||
|
throw Error(`Expected TableSelection focus to be (or a child of) TableCellNode`);
|
||
|
}
|
||
|
const focusCellNodeRect = $getTableCellNodeRect(focusCellNode);
|
||
|
if (!(focusCellNodeRect !== null)) {
|
||
|
throw Error(`getCellRect: expected to find focusCellNode`);
|
||
|
}
|
||
|
const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
|
||
|
const stopX = Math.max(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
|
||
|
const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
|
||
|
const stopY = Math.max(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
|
||
|
return {
|
||
|
fromX: Math.min(startX, stopX),
|
||
|
fromY: Math.min(startY, stopY),
|
||
|
toX: Math.max(startX, stopX),
|
||
|
toY: Math.max(startY, stopY)
|
||
|
};
|
||
|
}
|
||
|
getNodes() {
|
||
|
const cachedNodes = this._cachedNodes;
|
||
|
if (cachedNodes !== null) {
|
||
|
return cachedNodes;
|
||
|
}
|
||
|
const anchorNode = this.anchor.getNode();
|
||
|
const focusNode = this.focus.getNode();
|
||
|
const anchorCell = utils.$findMatchingParent(anchorNode, $isTableCellNode);
|
||
|
// todo replace with triplet
|
||
|
const focusCell = utils.$findMatchingParent(focusNode, $isTableCellNode);
|
||
|
if (!$isTableCellNode(anchorCell)) {
|
||
|
throw Error(`Expected TableSelection anchor to be (or a child of) TableCellNode`);
|
||
|
}
|
||
|
if (!$isTableCellNode(focusCell)) {
|
||
|
throw Error(`Expected TableSelection focus to be (or a child of) TableCellNode`);
|
||
|
}
|
||
|
const anchorRow = anchorCell.getParent();
|
||
|
if (!$isTableRowNode(anchorRow)) {
|
||
|
throw Error(`Expected anchorCell to have a parent TableRowNode`);
|
||
|
}
|
||
|
const tableNode = anchorRow.getParent();
|
||
|
if (!$isTableNode(tableNode)) {
|
||
|
throw Error(`Expected tableNode to have a parent TableNode`);
|
||
|
}
|
||
|
const focusCellGrid = focusCell.getParents()[1];
|
||
|
if (focusCellGrid !== tableNode) {
|
||
|
if (!tableNode.isParentOf(focusCell)) {
|
||
|
// focus is on higher Grid level than anchor
|
||
|
const gridParent = tableNode.getParent();
|
||
|
if (!(gridParent != null)) {
|
||
|
throw Error(`Expected gridParent to have a parent`);
|
||
|
}
|
||
|
this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
|
||
|
} else {
|
||
|
// anchor is on higher Grid level than focus
|
||
|
const focusCellParent = focusCellGrid.getParent();
|
||
|
if (!(focusCellParent != null)) {
|
||
|
throw Error(`Expected focusCellParent to have a parent`);
|
||
|
}
|
||
|
this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
|
||
|
}
|
||
|
return this.getNodes();
|
||
|
}
|
||
|
|
||
|
// TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
|
||
|
// once (on load) and iterate on it as updates occur. However, to do this we need to have the
|
||
|
// ability to store a state. Killing TableSelection and moving the logic to the plugin would make
|
||
|
// this possible.
|
||
|
const [map, cellAMap, cellBMap] = $computeTableMap(tableNode, anchorCell, focusCell);
|
||
|
let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
|
||
|
let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
|
||
|
let maxColumn = Math.max(cellAMap.startColumn + cellAMap.cell.__colSpan - 1, cellBMap.startColumn + cellBMap.cell.__colSpan - 1);
|
||
|
let maxRow = Math.max(cellAMap.startRow + cellAMap.cell.__rowSpan - 1, cellBMap.startRow + cellBMap.cell.__rowSpan - 1);
|
||
|
let exploredMinColumn = minColumn;
|
||
|
let exploredMinRow = minRow;
|
||
|
let exploredMaxColumn = minColumn;
|
||
|
let exploredMaxRow = minRow;
|
||
|
function expandBoundary(mapValue) {
|
||
|
const {
|
||
|
cell,
|
||
|
startColumn: cellStartColumn,
|
||
|
startRow: cellStartRow
|
||
|
} = mapValue;
|
||
|
minColumn = Math.min(minColumn, cellStartColumn);
|
||
|
minRow = Math.min(minRow, cellStartRow);
|
||
|
maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1);
|
||
|
maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1);
|
||
|
}
|
||
|
while (minColumn < exploredMinColumn || minRow < exploredMinRow || maxColumn > exploredMaxColumn || maxRow > exploredMaxRow) {
|
||
|
if (minColumn < exploredMinColumn) {
|
||
|
// Expand on the left
|
||
|
const rowDiff = exploredMaxRow - exploredMinRow;
|
||
|
const previousColumn = exploredMinColumn - 1;
|
||
|
for (let i = 0; i <= rowDiff; i++) {
|
||
|
expandBoundary(map[exploredMinRow + i][previousColumn]);
|
||
|
}
|
||
|
exploredMinColumn = previousColumn;
|
||
|
}
|
||
|
if (minRow < exploredMinRow) {
|
||
|
// Expand on top
|
||
|
const columnDiff = exploredMaxColumn - exploredMinColumn;
|
||
|
const previousRow = exploredMinRow - 1;
|
||
|
for (let i = 0; i <= columnDiff; i++) {
|
||
|
expandBoundary(map[previousRow][exploredMinColumn + i]);
|
||
|
}
|
||
|
exploredMinRow = previousRow;
|
||
|
}
|
||
|
if (maxColumn > exploredMaxColumn) {
|
||
|
// Expand on the right
|
||
|
const rowDiff = exploredMaxRow - exploredMinRow;
|
||
|
const nextColumn = exploredMaxColumn + 1;
|
||
|
for (let i = 0; i <= rowDiff; i++) {
|
||
|
expandBoundary(map[exploredMinRow + i][nextColumn]);
|
||
|
}
|
||
|
exploredMaxColumn = nextColumn;
|
||
|
}
|
||
|
if (maxRow > exploredMaxRow) {
|
||
|
// Expand on the bottom
|
||
|
const columnDiff = exploredMaxColumn - exploredMinColumn;
|
||
|
const nextRow = exploredMaxRow + 1;
|
||
|
for (let i = 0; i <= columnDiff; i++) {
|
||
|
expandBoundary(map[nextRow][exploredMinColumn + i]);
|
||
|
}
|
||
|
exploredMaxRow = nextRow;
|
||
|
}
|
||
|
}
|
||
|
const nodes = [tableNode];
|
||
|
let lastRow = null;
|
||
|
for (let i = minRow; i <= maxRow; i++) {
|
||
|
for (let j = minColumn; j <= maxColumn; j++) {
|
||
|
const {
|
||
|
cell
|
||
|
} = map[i][j];
|
||
|
const currentRow = cell.getParent();
|
||
|
if (!$isTableRowNode(currentRow)) {
|
||
|
throw Error(`Expected TableCellNode parent to be a TableRowNode`);
|
||
|
}
|
||
|
if (currentRow !== lastRow) {
|
||
|
nodes.push(currentRow);
|
||
|
}
|
||
|
nodes.push(cell, ...$getChildrenRecursively(cell));
|
||
|
lastRow = currentRow;
|
||
|
}
|
||
|
}
|
||
|
if (!lexical.isCurrentlyReadOnlyMode()) {
|
||
|
this._cachedNodes = nodes;
|
||
|
}
|
||
|
return nodes;
|
||
|
}
|
||
|
getTextContent() {
|
||
|
const nodes = this.getNodes();
|
||
|
let textContent = '';
|
||
|
for (let i = 0; i < nodes.length; i++) {
|
||
|
textContent += nodes[i].getTextContent();
|
||
|
}
|
||
|
return textContent;
|
||
|
}
|
||
|
}
|
||
|
function $isTableSelection(x) {
|
||
|
return x instanceof TableSelection;
|
||
|
}
|
||
|
function $createTableSelection() {
|
||
|
const anchor = lexical.$createPoint('root', 0, 'element');
|
||
|
const focus = lexical.$createPoint('root', 0, 'element');
|
||
|
return new TableSelection('root', anchor, focus);
|
||
|
}
|
||
|
function $getChildrenRecursively(node) {
|
||
|
const nodes = [];
|
||
|
const stack = [node];
|
||
|
while (stack.length > 0) {
|
||
|
const currentNode = stack.pop();
|
||
|
if (!(currentNode !== undefined)) {
|
||
|
throw Error(`Stack.length > 0; can't be undefined`);
|
||
|
}
|
||
|
if (lexical.$isElementNode(currentNode)) {
|
||
|
stack.unshift(...currentNode.getChildren());
|
||
|
}
|
||
|
if (currentNode !== node) {
|
||
|
nodes.push(currentNode);
|
||
|
}
|
||
|
}
|
||
|
return nodes;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
const getDOMSelection = targetWindow => CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
|
||
|
class TableObserver {
|
||
|
constructor(editor, tableNodeKey) {
|
||
|
this.isHighlightingCells = false;
|
||
|
this.anchorX = -1;
|
||
|
this.anchorY = -1;
|
||
|
this.focusX = -1;
|
||
|
this.focusY = -1;
|
||
|
this.listenersToRemove = new Set();
|
||
|
this.tableNodeKey = tableNodeKey;
|
||
|
this.editor = editor;
|
||
|
this.table = {
|
||
|
columns: 0,
|
||
|
domRows: [],
|
||
|
rows: 0
|
||
|
};
|
||
|
this.tableSelection = null;
|
||
|
this.anchorCellNodeKey = null;
|
||
|
this.focusCellNodeKey = null;
|
||
|
this.anchorCell = null;
|
||
|
this.focusCell = null;
|
||
|
this.hasHijackedSelectionStyles = false;
|
||
|
this.trackTable();
|
||
|
}
|
||
|
getTable() {
|
||
|
return this.table;
|
||
|
}
|
||
|
removeListeners() {
|
||
|
Array.from(this.listenersToRemove).forEach(removeListener => removeListener());
|
||
|
}
|
||
|
trackTable() {
|
||
|
const observer = new MutationObserver(records => {
|
||
|
this.editor.update(() => {
|
||
|
let gridNeedsRedraw = false;
|
||
|
for (let i = 0; i < records.length; i++) {
|
||
|
const record = records[i];
|
||
|
const target = record.target;
|
||
|
const nodeName = target.nodeName;
|
||
|
if (nodeName === 'TABLE' || nodeName === 'TR') {
|
||
|
gridNeedsRedraw = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (!gridNeedsRedraw) {
|
||
|
return;
|
||
|
}
|
||
|
const tableElement = this.editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
this.table = getTable(tableElement);
|
||
|
});
|
||
|
});
|
||
|
this.editor.update(() => {
|
||
|
const tableElement = this.editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
this.table = getTable(tableElement);
|
||
|
observer.observe(tableElement, {
|
||
|
childList: true,
|
||
|
subtree: true
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
clearHighlight() {
|
||
|
const editor = this.editor;
|
||
|
this.isHighlightingCells = false;
|
||
|
this.anchorX = -1;
|
||
|
this.anchorY = -1;
|
||
|
this.focusX = -1;
|
||
|
this.focusY = -1;
|
||
|
this.tableSelection = null;
|
||
|
this.anchorCellNodeKey = null;
|
||
|
this.focusCellNodeKey = null;
|
||
|
this.anchorCell = null;
|
||
|
this.focusCell = null;
|
||
|
this.hasHijackedSelectionStyles = false;
|
||
|
this.enableHighlightStyle();
|
||
|
editor.update(() => {
|
||
|
const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
|
||
|
if (!$isTableNode(tableNode)) {
|
||
|
throw new Error('Expected TableNode.');
|
||
|
}
|
||
|
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
const grid = getTable(tableElement);
|
||
|
$updateDOMForSelection(editor, grid, null);
|
||
|
lexical.$setSelection(null);
|
||
|
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
||
|
});
|
||
|
}
|
||
|
enableHighlightStyle() {
|
||
|
const editor = this.editor;
|
||
|
editor.update(() => {
|
||
|
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
utils.removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
|
||
|
tableElement.classList.remove('disable-selection');
|
||
|
this.hasHijackedSelectionStyles = false;
|
||
|
});
|
||
|
}
|
||
|
disableHighlightStyle() {
|
||
|
const editor = this.editor;
|
||
|
editor.update(() => {
|
||
|
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
utils.addClassNamesToElement(tableElement, editor._config.theme.tableSelection);
|
||
|
this.hasHijackedSelectionStyles = true;
|
||
|
});
|
||
|
}
|
||
|
updateTableTableSelection(selection) {
|
||
|
if (selection !== null && selection.tableKey === this.tableNodeKey) {
|
||
|
const editor = this.editor;
|
||
|
this.tableSelection = selection;
|
||
|
this.isHighlightingCells = true;
|
||
|
this.disableHighlightStyle();
|
||
|
$updateDOMForSelection(editor, this.table, this.tableSelection);
|
||
|
} else if (selection == null) {
|
||
|
this.clearHighlight();
|
||
|
} else {
|
||
|
this.tableNodeKey = selection.tableKey;
|
||
|
this.updateTableTableSelection(selection);
|
||
|
}
|
||
|
}
|
||
|
setFocusCellForSelection(cell, ignoreStart = false) {
|
||
|
const editor = this.editor;
|
||
|
editor.update(() => {
|
||
|
const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
|
||
|
if (!$isTableNode(tableNode)) {
|
||
|
throw new Error('Expected TableNode.');
|
||
|
}
|
||
|
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||
|
if (!tableElement) {
|
||
|
throw new Error('Expected to find TableElement in DOM');
|
||
|
}
|
||
|
const cellX = cell.x;
|
||
|
const cellY = cell.y;
|
||
|
this.focusCell = cell;
|
||
|
if (this.anchorCell !== null) {
|
||
|
const domSelection = getDOMSelection(editor._window);
|
||
|
// Collapse the selection
|
||
|
if (domSelection) {
|
||
|
domSelection.setBaseAndExtent(this.anchorCell.elem, 0, this.focusCell.elem, 0);
|
||
|
}
|
||
|
}
|
||
|
if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
|
||
|
this.isHighlightingCells = true;
|
||
|
this.disableHighlightStyle();
|
||
|
} else if (cellX === this.focusX && cellY === this.focusY) {
|
||
|
return;
|
||
|
}
|
||
|
this.focusX = cellX;
|
||
|
this.focusY = cellY;
|
||
|
if (this.isHighlightingCells) {
|
||
|
const focusTableCellNode = lexical.$getNearestNodeFromDOMNode(cell.elem);
|
||
|
if (this.tableSelection != null && this.anchorCellNodeKey != null && $isTableCellNode(focusTableCellNode)) {
|
||
|
const focusNodeKey = focusTableCellNode.getKey();
|
||
|
this.tableSelection = this.tableSelection.clone() || $createTableSelection();
|
||
|
this.focusCellNodeKey = focusNodeKey;
|
||
|
this.tableSelection.set(this.tableNodeKey, this.anchorCellNodeKey, this.focusCellNodeKey);
|
||
|
lexical.$setSelection(this.tableSelection);
|
||
|
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
||
|
$updateDOMForSelection(editor, this.table, this.tableSelection);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
setAnchorCellForSelection(cell) {
|
||
|
this.isHighlightingCells = false;
|
||
|
this.anchorCell = cell;
|
||
|
this.anchorX = cell.x;
|
||
|
this.anchorY = cell.y;
|
||
|
this.editor.update(() => {
|
||
|
const anchorTableCellNode = lexical.$getNearestNodeFromDOMNode(cell.elem);
|
||
|
if ($isTableCellNode(anchorTableCellNode)) {
|
||
|
const anchorNodeKey = anchorTableCellNode.getKey();
|
||
|
this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
|
||
|
this.anchorCellNodeKey = anchorNodeKey;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
formatCells(type) {
|
||
|
this.editor.update(() => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isTableSelection(selection)) {
|
||
|
{
|
||
|
throw Error(`Expected grid selection`);
|
||
|
}
|
||
|
}
|
||
|
const formatSelection = lexical.$createRangeSelection();
|
||
|
const anchor = formatSelection.anchor;
|
||
|
const focus = formatSelection.focus;
|
||
|
selection.getNodes().forEach(cellNode => {
|
||
|
if ($isTableCellNode(cellNode) && cellNode.getTextContentSize() !== 0) {
|
||
|
anchor.set(cellNode.getKey(), 0, 'element');
|
||
|
focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
|
||
|
formatSelection.formatText(type);
|
||
|
}
|
||
|
});
|
||
|
lexical.$setSelection(selection);
|
||
|
this.editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
||
|
});
|
||
|
}
|
||
|
clearText() {
|
||
|
const editor = this.editor;
|
||
|
editor.update(() => {
|
||
|
const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
|
||
|
if (!$isTableNode(tableNode)) {
|
||
|
throw new Error('Expected TableNode.');
|
||
|
}
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isTableSelection(selection)) {
|
||
|
{
|
||
|
throw Error(`Expected grid selection`);
|
||
|
}
|
||
|
}
|
||
|
const selectedNodes = selection.getNodes().filter($isTableCellNode);
|
||
|
if (selectedNodes.length === this.table.columns * this.table.rows) {
|
||
|
tableNode.selectPrevious();
|
||
|
// Delete entire table
|
||
|
tableNode.remove();
|
||
|
const rootNode = lexical.$getRoot();
|
||
|
rootNode.selectStart();
|
||
|
return;
|
||
|
}
|
||
|
selectedNodes.forEach(cellNode => {
|
||
|
if (lexical.$isElementNode(cellNode)) {
|
||
|
const paragraphNode = lexical.$createParagraphNode();
|
||
|
const textNode = lexical.$createTextNode();
|
||
|
paragraphNode.append(textNode);
|
||
|
cellNode.append(paragraphNode);
|
||
|
cellNode.getChildren().forEach(child => {
|
||
|
if (child !== paragraphNode) {
|
||
|
child.remove();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
$updateDOMForSelection(editor, this.table, null);
|
||
|
lexical.$setSelection(null);
|
||
|
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
|
||
|
function applyTableHandlers(tableNode, tableElement, editor, hasTabHandler) {
|
||
|
const rootElement = editor.getRootElement();
|
||
|
if (rootElement === null) {
|
||
|
throw new Error('No root element.');
|
||
|
}
|
||
|
const tableObserver = new TableObserver(editor, tableNode.getKey());
|
||
|
const editorWindow = editor._window || window;
|
||
|
attachTableObserverToTableElement(tableElement, tableObserver);
|
||
|
tableElement.addEventListener('mousedown', event => {
|
||
|
setTimeout(() => {
|
||
|
if (event.button !== 0) {
|
||
|
return;
|
||
|
}
|
||
|
if (!editorWindow) {
|
||
|
return;
|
||
|
}
|
||
|
const anchorCell = getDOMCellFromTarget(event.target);
|
||
|
if (anchorCell !== null) {
|
||
|
stopEvent(event);
|
||
|
tableObserver.setAnchorCellForSelection(anchorCell);
|
||
|
}
|
||
|
const onMouseUp = () => {
|
||
|
editorWindow.removeEventListener('mouseup', onMouseUp);
|
||
|
editorWindow.removeEventListener('mousemove', onMouseMove);
|
||
|
};
|
||
|
const onMouseMove = moveEvent => {
|
||
|
const focusCell = getDOMCellFromTarget(moveEvent.target);
|
||
|
if (focusCell !== null && (tableObserver.anchorX !== focusCell.x || tableObserver.anchorY !== focusCell.y)) {
|
||
|
moveEvent.preventDefault();
|
||
|
tableObserver.setFocusCellForSelection(focusCell);
|
||
|
}
|
||
|
};
|
||
|
editorWindow.addEventListener('mouseup', onMouseUp);
|
||
|
editorWindow.addEventListener('mousemove', onMouseMove);
|
||
|
}, 0);
|
||
|
});
|
||
|
|
||
|
// Clear selection when clicking outside of dom.
|
||
|
const mouseDownCallback = event => {
|
||
|
if (event.button !== 0) {
|
||
|
return;
|
||
|
}
|
||
|
editor.update(() => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
const target = event.target;
|
||
|
if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target)) {
|
||
|
tableObserver.clearHighlight();
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
editorWindow.addEventListener('mousedown', mouseDownCallback);
|
||
|
tableObserver.listenersToRemove.add(() => editorWindow.removeEventListener('mousedown', mouseDownCallback));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ARROW_DOWN_COMMAND, event => $handleArrowKey(editor, event, 'down', tableNode, tableObserver), lexical.COMMAND_PRIORITY_HIGH));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ARROW_UP_COMMAND, event => $handleArrowKey(editor, event, 'up', tableNode, tableObserver), lexical.COMMAND_PRIORITY_HIGH));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ARROW_LEFT_COMMAND, event => $handleArrowKey(editor, event, 'backward', tableNode, tableObserver), lexical.COMMAND_PRIORITY_HIGH));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ARROW_RIGHT_COMMAND, event => $handleArrowKey(editor, event, 'forward', tableNode, tableObserver), lexical.COMMAND_PRIORITY_HIGH));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ESCAPE_COMMAND, event => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if ($isTableSelection(selection)) {
|
||
|
const focusCellNode = utils.$findMatchingParent(selection.focus.getNode(), $isTableCellNode);
|
||
|
if ($isTableCellNode(focusCellNode)) {
|
||
|
stopEvent(event);
|
||
|
focusCellNode.selectEnd();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}, lexical.COMMAND_PRIORITY_HIGH));
|
||
|
const deleteTextHandler = command => () => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ($isTableSelection(selection)) {
|
||
|
tableObserver.clearText();
|
||
|
return true;
|
||
|
} else if (lexical.$isRangeSelection(selection)) {
|
||
|
const tableCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
|
||
|
if (!$isTableCellNode(tableCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
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;
|
||
|
if (selectionContainsPartialTable) {
|
||
|
tableObserver.clearText();
|
||
|
return true;
|
||
|
}
|
||
|
const nearestElementNode = utils.$findMatchingParent(selection.anchor.getNode(), n => lexical.$isElementNode(n));
|
||
|
const topLevelCellElementNode = nearestElementNode && utils.$findMatchingParent(nearestElementNode, n => lexical.$isElementNode(n) && $isTableCellNode(n.getParent()));
|
||
|
if (!lexical.$isElementNode(topLevelCellElementNode) || !lexical.$isElementNode(nearestElementNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if (command === lexical.DELETE_LINE_COMMAND && topLevelCellElementNode.getPreviousSibling() === null) {
|
||
|
// TODO: Fix Delete Line in Table Cells.
|
||
|
return true;
|
||
|
}
|
||
|
if (command === lexical.DELETE_CHARACTER_COMMAND || command === lexical.DELETE_WORD_COMMAND) {
|
||
|
if (selection.isCollapsed() && selection.anchor.offset === 0) {
|
||
|
if (nearestElementNode !== topLevelCellElementNode) {
|
||
|
const children = nearestElementNode.getChildren();
|
||
|
const newParagraphNode = lexical.$createParagraphNode();
|
||
|
children.forEach(child => newParagraphNode.append(child));
|
||
|
nearestElementNode.replace(newParagraphNode);
|
||
|
nearestElementNode.getWritable().__parent = tableCellNode.getKey();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
[lexical.DELETE_WORD_COMMAND, lexical.DELETE_LINE_COMMAND, lexical.DELETE_CHARACTER_COMMAND].forEach(command => {
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler(command), lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
});
|
||
|
const deleteCellHandler = event => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ($isTableSelection(selection)) {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
tableObserver.clearText();
|
||
|
return true;
|
||
|
} else if (lexical.$isRangeSelection(selection)) {
|
||
|
const tableCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
|
||
|
if (!$isTableCellNode(tableCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_BACKSPACE_COMMAND, deleteCellHandler, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_DELETE_COMMAND, deleteCellHandler, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FORMAT_TEXT_COMMAND, payload => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ($isTableSelection(selection)) {
|
||
|
tableObserver.formatCells(payload);
|
||
|
return true;
|
||
|
} else if (lexical.$isRangeSelection(selection)) {
|
||
|
const tableCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
|
||
|
if (!$isTableCellNode(tableCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.CONTROLLED_TEXT_INSERTION_COMMAND, payload => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ($isTableSelection(selection)) {
|
||
|
tableObserver.clearHighlight();
|
||
|
return false;
|
||
|
} else if (lexical.$isRangeSelection(selection)) {
|
||
|
const tableCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
|
||
|
if (!$isTableCellNode(tableCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
if (hasTabHandler) {
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_TAB_COMMAND, event => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const tableCellNode = $findCellNode(selection.anchor.getNode());
|
||
|
if (tableCellNode === null) {
|
||
|
return false;
|
||
|
}
|
||
|
stopEvent(event);
|
||
|
const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
|
||
|
selectTableNodeInDirection(tableObserver, tableNode, currentCords.x, currentCords.y, !event.shiftKey ? 'forward' : 'backward');
|
||
|
return true;
|
||
|
}, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
}
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FOCUS_COMMAND, payload => {
|
||
|
return tableNode.isSelected();
|
||
|
}, lexical.COMMAND_PRIORITY_HIGH));
|
||
|
function getObserverCellFromCellNode(tableCellNode) {
|
||
|
const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
|
||
|
return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
|
||
|
}
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, selectionPayload => {
|
||
|
const {
|
||
|
nodes,
|
||
|
selection
|
||
|
} = selectionPayload;
|
||
|
const anchorAndFocus = selection.getStartEndPoints();
|
||
|
const isTableSelection = $isTableSelection(selection);
|
||
|
const isRangeSelection = lexical.$isRangeSelection(selection);
|
||
|
const isSelectionInsideOfGrid = isRangeSelection && utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n)) !== null && utils.$findMatchingParent(selection.focus.getNode(), n => $isTableCellNode(n)) !== null || isTableSelection;
|
||
|
if (nodes.length !== 1 || !$isTableNode(nodes[0]) || !isSelectionInsideOfGrid || anchorAndFocus === null) {
|
||
|
return false;
|
||
|
}
|
||
|
const [anchor] = anchorAndFocus;
|
||
|
const newGrid = nodes[0];
|
||
|
const newGridRows = newGrid.getChildren();
|
||
|
const newColumnCount = newGrid.getFirstChildOrThrow().getChildrenSize();
|
||
|
const newRowCount = newGrid.getChildrenSize();
|
||
|
const gridCellNode = utils.$findMatchingParent(anchor.getNode(), n => $isTableCellNode(n));
|
||
|
const gridRowNode = gridCellNode && utils.$findMatchingParent(gridCellNode, n => $isTableRowNode(n));
|
||
|
const gridNode = gridRowNode && utils.$findMatchingParent(gridRowNode, n => $isTableNode(n));
|
||
|
if (!$isTableCellNode(gridCellNode) || !$isTableRowNode(gridRowNode) || !$isTableNode(gridNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const startY = gridRowNode.getIndexWithinParent();
|
||
|
const stopY = Math.min(gridNode.getChildrenSize() - 1, startY + newRowCount - 1);
|
||
|
const startX = gridCellNode.getIndexWithinParent();
|
||
|
const stopX = Math.min(gridRowNode.getChildrenSize() - 1, startX + newColumnCount - 1);
|
||
|
const fromX = Math.min(startX, stopX);
|
||
|
const fromY = Math.min(startY, stopY);
|
||
|
const toX = Math.max(startX, stopX);
|
||
|
const toY = Math.max(startY, stopY);
|
||
|
const gridRowNodes = gridNode.getChildren();
|
||
|
let newRowIdx = 0;
|
||
|
let newAnchorCellKey;
|
||
|
let newFocusCellKey;
|
||
|
for (let r = fromY; r <= toY; r++) {
|
||
|
const currentGridRowNode = gridRowNodes[r];
|
||
|
if (!$isTableRowNode(currentGridRowNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const newGridRowNode = newGridRows[newRowIdx];
|
||
|
if (!$isTableRowNode(newGridRowNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const gridCellNodes = currentGridRowNode.getChildren();
|
||
|
const newGridCellNodes = newGridRowNode.getChildren();
|
||
|
let newColumnIdx = 0;
|
||
|
for (let c = fromX; c <= toX; c++) {
|
||
|
const currentGridCellNode = gridCellNodes[c];
|
||
|
if (!$isTableCellNode(currentGridCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const newGridCellNode = newGridCellNodes[newColumnIdx];
|
||
|
if (!$isTableCellNode(newGridCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if (r === fromY && c === fromX) {
|
||
|
newAnchorCellKey = currentGridCellNode.getKey();
|
||
|
} else if (r === toY && c === toX) {
|
||
|
newFocusCellKey = currentGridCellNode.getKey();
|
||
|
}
|
||
|
const originalChildren = currentGridCellNode.getChildren();
|
||
|
newGridCellNode.getChildren().forEach(child => {
|
||
|
if (lexical.$isTextNode(child)) {
|
||
|
const paragraphNode = lexical.$createParagraphNode();
|
||
|
paragraphNode.append(child);
|
||
|
currentGridCellNode.append(child);
|
||
|
} else {
|
||
|
currentGridCellNode.append(child);
|
||
|
}
|
||
|
});
|
||
|
originalChildren.forEach(n => n.remove());
|
||
|
newColumnIdx++;
|
||
|
}
|
||
|
newRowIdx++;
|
||
|
}
|
||
|
if (newAnchorCellKey && newFocusCellKey) {
|
||
|
const newTableSelection = $createTableSelection();
|
||
|
newTableSelection.set(nodes[0].getKey(), newAnchorCellKey, newFocusCellKey);
|
||
|
lexical.$setSelection(newTableSelection);
|
||
|
}
|
||
|
return true;
|
||
|
}, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_CHANGE_COMMAND, () => {
|
||
|
const selection = lexical.$getSelection();
|
||
|
const prevSelection = lexical.$getPreviousSelection();
|
||
|
if (lexical.$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');
|
||
|
lexical.$setSelection(newSelection);
|
||
|
$addHighlightStyleToTable(editor, tableObserver);
|
||
|
} 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)) {
|
||
|
tableObserver.setAnchorCellForSelection(getObserverCellFromCellNode(anchorCellNode));
|
||
|
tableObserver.setFocusCellForSelection(getObserverCellFromCellNode(focusCellNode), true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (selection && !selection.is(prevSelection) && ($isTableSelection(selection) || $isTableSelection(prevSelection)) && tableObserver.tableSelection && !tableObserver.tableSelection.is(prevSelection)) {
|
||
|
if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
|
||
|
tableObserver.updateTableTableSelection(selection);
|
||
|
} else if (!$isTableSelection(selection) && $isTableSelection(prevSelection) && prevSelection.tableKey === tableObserver.tableNodeKey) {
|
||
|
tableObserver.updateTableTableSelection(null);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
if (tableObserver.hasHijackedSelectionStyles && !tableNode.isSelected()) {
|
||
|
$removeHighlightStyleToTable(editor, tableObserver);
|
||
|
} else if (!tableObserver.hasHijackedSelectionStyles && tableNode.isSelected()) {
|
||
|
$addHighlightStyleToTable(editor, tableObserver);
|
||
|
}
|
||
|
return false;
|
||
|
}, lexical.COMMAND_PRIORITY_CRITICAL));
|
||
|
return tableObserver;
|
||
|
}
|
||
|
function attachTableObserverToTableElement(tableElement, tableObserver) {
|
||
|
tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
|
||
|
}
|
||
|
function getTableObserverFromTableElement(tableElement) {
|
||
|
return tableElement[LEXICAL_ELEMENT_KEY];
|
||
|
}
|
||
|
function getDOMCellFromTarget(node) {
|
||
|
let currentNode = node;
|
||
|
while (currentNode != null) {
|
||
|
const nodeName = currentNode.nodeName;
|
||
|
if (nodeName === 'TD' || nodeName === 'TH') {
|
||
|
// @ts-expect-error: internal field
|
||
|
const cell = currentNode._cell;
|
||
|
if (cell === undefined) {
|
||
|
return null;
|
||
|
}
|
||
|
return cell;
|
||
|
}
|
||
|
currentNode = currentNode.parentNode;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
function getTable(tableElement) {
|
||
|
const domRows = [];
|
||
|
const grid = {
|
||
|
columns: 0,
|
||
|
domRows,
|
||
|
rows: 0
|
||
|
};
|
||
|
let currentNode = tableElement.firstChild;
|
||
|
let x = 0;
|
||
|
let y = 0;
|
||
|
domRows.length = 0;
|
||
|
while (currentNode != null) {
|
||
|
const nodeMame = currentNode.nodeName;
|
||
|
if (nodeMame === 'TD' || nodeMame === 'TH') {
|
||
|
const elem = currentNode;
|
||
|
const cell = {
|
||
|
elem,
|
||
|
hasBackgroundColor: elem.style.backgroundColor !== '',
|
||
|
highlighted: false,
|
||
|
x,
|
||
|
y
|
||
|
};
|
||
|
|
||
|
// @ts-expect-error: internal field
|
||
|
currentNode._cell = cell;
|
||
|
let row = domRows[y];
|
||
|
if (row === undefined) {
|
||
|
row = domRows[y] = [];
|
||
|
}
|
||
|
row[x] = cell;
|
||
|
} else {
|
||
|
const child = currentNode.firstChild;
|
||
|
if (child != null) {
|
||
|
currentNode = child;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
const sibling = currentNode.nextSibling;
|
||
|
if (sibling != null) {
|
||
|
x++;
|
||
|
currentNode = sibling;
|
||
|
continue;
|
||
|
}
|
||
|
const parent = currentNode.parentNode;
|
||
|
if (parent != null) {
|
||
|
const parentSibling = parent.nextSibling;
|
||
|
if (parentSibling == null) {
|
||
|
break;
|
||
|
}
|
||
|
y++;
|
||
|
x = 0;
|
||
|
currentNode = parentSibling;
|
||
|
}
|
||
|
}
|
||
|
grid.columns = x + 1;
|
||
|
grid.rows = y + 1;
|
||
|
return grid;
|
||
|
}
|
||
|
function $updateDOMForSelection(editor, table, selection) {
|
||
|
const selectedCellNodes = new Set(selection ? selection.getNodes() : []);
|
||
|
$forEachTableCell(table, (cell, lexicalNode) => {
|
||
|
const elem = cell.elem;
|
||
|
if (selectedCellNodes.has(lexicalNode)) {
|
||
|
cell.highlighted = true;
|
||
|
$addHighlightToDOM(editor, cell);
|
||
|
} else {
|
||
|
cell.highlighted = false;
|
||
|
$removeHighlightFromDOM(editor, cell);
|
||
|
if (!elem.getAttribute('style')) {
|
||
|
elem.removeAttribute('style');
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
function $forEachTableCell(grid, cb) {
|
||
|
const {
|
||
|
domRows
|
||
|
} = grid;
|
||
|
for (let y = 0; y < domRows.length; y++) {
|
||
|
const row = domRows[y];
|
||
|
if (!row) {
|
||
|
continue;
|
||
|
}
|
||
|
for (let x = 0; x < row.length; x++) {
|
||
|
const cell = row[x];
|
||
|
if (!cell) {
|
||
|
continue;
|
||
|
}
|
||
|
const lexicalNode = lexical.$getNearestNodeFromDOMNode(cell.elem);
|
||
|
if (lexicalNode !== null) {
|
||
|
cb(cell, lexicalNode, {
|
||
|
x,
|
||
|
y
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function $addHighlightStyleToTable(editor, tableSelection) {
|
||
|
tableSelection.disableHighlightStyle();
|
||
|
$forEachTableCell(tableSelection.table, cell => {
|
||
|
cell.highlighted = true;
|
||
|
$addHighlightToDOM(editor, cell);
|
||
|
});
|
||
|
}
|
||
|
function $removeHighlightStyleToTable(editor, tableObserver) {
|
||
|
tableObserver.enableHighlightStyle();
|
||
|
$forEachTableCell(tableObserver.table, cell => {
|
||
|
const elem = cell.elem;
|
||
|
cell.highlighted = false;
|
||
|
$removeHighlightFromDOM(editor, cell);
|
||
|
if (!elem.getAttribute('style')) {
|
||
|
elem.removeAttribute('style');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
const selectTableNodeInDirection = (tableObserver, tableNode, x, y, direction) => {
|
||
|
const isForward = direction === 'forward';
|
||
|
switch (direction) {
|
||
|
case 'backward':
|
||
|
case 'forward':
|
||
|
if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
|
||
|
selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x + (isForward ? 1 : -1), y, tableObserver.table), isForward);
|
||
|
} else {
|
||
|
if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {
|
||
|
selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(isForward ? 0 : tableObserver.table.columns - 1, y + (isForward ? 1 : -1), tableObserver.table), isForward);
|
||
|
} else if (!isForward) {
|
||
|
tableNode.selectPrevious();
|
||
|
} else {
|
||
|
tableNode.selectNext();
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
case 'up':
|
||
|
if (y !== 0) {
|
||
|
selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table), false);
|
||
|
} else {
|
||
|
tableNode.selectPrevious();
|
||
|
}
|
||
|
return true;
|
||
|
case 'down':
|
||
|
if (y !== tableObserver.table.rows - 1) {
|
||
|
selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table), true);
|
||
|
} else {
|
||
|
tableNode.selectNext();
|
||
|
}
|
||
|
return true;
|
||
|
default:
|
||
|
return false;
|
||
|
}
|
||
|
};
|
||
|
const adjustFocusNodeInDirection = (tableObserver, tableNode, x, y, direction) => {
|
||
|
const isForward = direction === 'forward';
|
||
|
switch (direction) {
|
||
|
case 'backward':
|
||
|
case 'forward':
|
||
|
if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
|
||
|
tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x + (isForward ? 1 : -1), y, tableObserver.table));
|
||
|
}
|
||
|
return true;
|
||
|
case 'up':
|
||
|
if (y !== 0) {
|
||
|
tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x, y - 1, tableObserver.table));
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
case 'down':
|
||
|
if (y !== tableObserver.table.rows - 1) {
|
||
|
tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x, y + 1, tableObserver.table));
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
default:
|
||
|
return false;
|
||
|
}
|
||
|
};
|
||
|
function $isSelectionInTable(selection, tableNode) {
|
||
|
if (lexical.$isRangeSelection(selection) || $isTableSelection(selection)) {
|
||
|
const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
|
||
|
const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
|
||
|
return isAnchorInside && isFocusInside;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function selectTableCellNode(tableCell, fromStart) {
|
||
|
if (fromStart) {
|
||
|
tableCell.selectStart();
|
||
|
} else {
|
||
|
tableCell.selectEnd();
|
||
|
}
|
||
|
}
|
||
|
const BROWSER_BLUE_RGB = '172,206,247';
|
||
|
function $addHighlightToDOM(editor, cell) {
|
||
|
const element = cell.elem;
|
||
|
const node = lexical.$getNearestNodeFromDOMNode(element);
|
||
|
if (!$isTableCellNode(node)) {
|
||
|
throw Error(`Expected to find LexicalNode from Table Cell DOMNode`);
|
||
|
}
|
||
|
const backgroundColor = node.getBackgroundColor();
|
||
|
if (backgroundColor === null) {
|
||
|
element.style.setProperty('background-color', `rgb(${BROWSER_BLUE_RGB})`);
|
||
|
} else {
|
||
|
element.style.setProperty('background-image', `linear-gradient(to right, rgba(${BROWSER_BLUE_RGB},0.85), rgba(${BROWSER_BLUE_RGB},0.85))`);
|
||
|
}
|
||
|
element.style.setProperty('caret-color', 'transparent');
|
||
|
}
|
||
|
function $removeHighlightFromDOM(editor, cell) {
|
||
|
const element = cell.elem;
|
||
|
const node = lexical.$getNearestNodeFromDOMNode(element);
|
||
|
if (!$isTableCellNode(node)) {
|
||
|
throw Error(`Expected to find LexicalNode from Table Cell DOMNode`);
|
||
|
}
|
||
|
const backgroundColor = node.getBackgroundColor();
|
||
|
if (backgroundColor === null) {
|
||
|
element.style.removeProperty('background-color');
|
||
|
}
|
||
|
element.style.removeProperty('background-image');
|
||
|
element.style.removeProperty('caret-color');
|
||
|
}
|
||
|
function $findCellNode(node) {
|
||
|
const cellNode = utils.$findMatchingParent(node, $isTableCellNode);
|
||
|
return $isTableCellNode(cellNode) ? cellNode : null;
|
||
|
}
|
||
|
function $findTableNode(node) {
|
||
|
const tableNode = utils.$findMatchingParent(node, $isTableNode);
|
||
|
return $isTableNode(tableNode) ? tableNode : null;
|
||
|
}
|
||
|
function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
|
||
|
const selection = lexical.$getSelection();
|
||
|
if (!$isSelectionInTable(selection, tableNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
if (lexical.$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 = utils.$findMatchingParent(anchor.getNode(), $isTableCellNode);
|
||
|
const focusCellNode = utils.$findMatchingParent(focus.getNode(), $isTableCellNode);
|
||
|
if (!$isTableCellNode(anchorCellNode) || !anchorCellNode.is(focusCellNode)) {
|
||
|
return false;
|
||
|
}
|
||
|
const anchorCellTable = $findTableNode(anchorCellNode);
|
||
|
if (anchorCellTable !== tableNode && anchorCellTable != null) {
|
||
|
const anchorCellTableElement = editor.getElementByKey(anchorCellTable.getKey());
|
||
|
if (anchorCellTableElement != null) {
|
||
|
tableObserver.table = getTable(anchorCellTableElement);
|
||
|
return $handleArrowKey(editor, event, direction, anchorCellTable, tableObserver);
|
||
|
}
|
||
|
}
|
||
|
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, tableObserver.table);
|
||
|
if (event.shiftKey) {
|
||
|
const cell = tableNode.getDOMCellFromCordsOrThrow(cords.x, cords.y, tableObserver.table);
|
||
|
tableObserver.setAnchorCellForSelection(cell);
|
||
|
tableObserver.setFocusCellForSelection(cell, true);
|
||
|
} else {
|
||
|
return selectTableNodeInDirection(tableObserver, tableNode, cords.x, cords.y, direction);
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
} else if ($isTableSelection(selection)) {
|
||
|
const {
|
||
|
anchor,
|
||
|
focus
|
||
|
} = selection;
|
||
|
const anchorCellNode = utils.$findMatchingParent(anchor.getNode(), $isTableCellNode);
|
||
|
const focusCellNode = utils.$findMatchingParent(focus.getNode(), $isTableCellNode);
|
||
|
const [tableNodeFromSelection] = selection.getNodes();
|
||
|
const tableElement = editor.getElementByKey(tableNodeFromSelection.getKey());
|
||
|
if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
|
||
|
return false;
|
||
|
}
|
||
|
tableObserver.updateTableTableSelection(selection);
|
||
|
const grid = getTable(tableElement);
|
||
|
const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
|
||
|
const anchorCell = tableNode.getDOMCellFromCordsOrThrow(cordsAnchor.x, cordsAnchor.y, grid);
|
||
|
tableObserver.setAnchorCellForSelection(anchorCell);
|
||
|
stopEvent(event);
|
||
|
if (event.shiftKey) {
|
||
|
const cords = tableNode.getCordsFromCellNode(focusCellNode, grid);
|
||
|
return adjustFocusNodeInDirection(tableObserver, tableNodeFromSelection, cords.x, cords.y, direction);
|
||
|
} else {
|
||
|
focusCellNode.selectEnd();
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function stopEvent(event) {
|
||
|
event.preventDefault();
|
||
|
event.stopImmediatePropagation();
|
||
|
event.stopPropagation();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
*/
|
||
|
/** @noInheritDoc */
|
||
|
class TableNode extends lexical.ElementNode {
|
||
|
static getType() {
|
||
|
return 'table';
|
||
|
}
|
||
|
static clone(node) {
|
||
|
return new TableNode(node.__key);
|
||
|
}
|
||
|
static importDOM() {
|
||
|
return {
|
||
|
table: _node => ({
|
||
|
conversion: convertTableElement,
|
||
|
priority: 1
|
||
|
})
|
||
|
};
|
||
|
}
|
||
|
static importJSON(_serializedNode) {
|
||
|
return $createTableNode();
|
||
|
}
|
||
|
constructor(key) {
|
||
|
super(key);
|
||
|
}
|
||
|
exportJSON() {
|
||
|
return {
|
||
|
...super.exportJSON(),
|
||
|
type: 'table',
|
||
|
version: 1
|
||
|
};
|
||
|
}
|
||
|
createDOM(config, editor) {
|
||
|
const tableElement = document.createElement('table');
|
||
|
utils.addClassNamesToElement(tableElement, config.theme.table);
|
||
|
return tableElement;
|
||
|
}
|
||
|
updateDOM() {
|
||
|
return false;
|
||
|
}
|
||
|
exportDOM(editor) {
|
||
|
return {
|
||
|
...super.exportDOM(editor),
|
||
|
after: tableElement => {
|
||
|
if (tableElement) {
|
||
|
const newElement = tableElement.cloneNode();
|
||
|
const colGroup = document.createElement('colgroup');
|
||
|
const tBody = document.createElement('tbody');
|
||
|
if (utils.isHTMLElement(tableElement)) {
|
||
|
tBody.append(...tableElement.children);
|
||
|
}
|
||
|
const firstRow = this.getFirstChildOrThrow();
|
||
|
if (!$isTableRowNode(firstRow)) {
|
||
|
throw new Error('Expected to find row node.');
|
||
|
}
|
||
|
const colCount = firstRow.getChildrenSize();
|
||
|
for (let i = 0; i < colCount; i++) {
|
||
|
const col = document.createElement('col');
|
||
|
colGroup.append(col);
|
||
|
}
|
||
|
newElement.replaceChildren(colGroup, tBody);
|
||
|
return newElement;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// TODO 0.10 deprecate
|
||
|
canExtractContents() {
|
||
|
return false;
|
||
|
}
|
||
|
canBeEmpty() {
|
||
|
return false;
|
||
|
}
|
||
|
isShadowRoot() {
|
||
|
return true;
|
||
|
}
|
||
|
getCordsFromCellNode(tableCellNode, table) {
|
||
|
const {
|
||
|
rows,
|
||
|
domRows
|
||
|
} = table;
|
||
|
for (let y = 0; y < rows; y++) {
|
||
|
const row = domRows[y];
|
||
|
if (row == null) {
|
||
|
continue;
|
||
|
}
|
||
|
const x = row.findIndex(cell => {
|
||
|
if (!cell) return;
|
||
|
const {
|
||
|
elem
|
||
|
} = cell;
|
||
|
const cellNode = lexical.$getNearestNodeFromDOMNode(elem);
|
||
|
return cellNode === tableCellNode;
|
||
|
});
|
||
|
if (x !== -1) {
|
||
|
return {
|
||
|
x,
|
||
|
y
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
throw new Error('Cell not found in table.');
|
||
|
}
|
||
|
getDOMCellFromCords(x, y, table) {
|
||
|
const {
|
||
|
domRows
|
||
|
} = table;
|
||
|
const row = domRows[y];
|
||
|
if (row == null) {
|
||
|
return null;
|
||
|
}
|
||
|
const cell = row[x];
|
||
|
if (cell == null) {
|
||
|
return null;
|
||
|
}
|
||
|
return cell;
|
||
|
}
|
||
|
getDOMCellFromCordsOrThrow(x, y, table) {
|
||
|
const cell = this.getDOMCellFromCords(x, y, table);
|
||
|
if (!cell) {
|
||
|
throw new Error('Cell not found at cords.');
|
||
|
}
|
||
|
return cell;
|
||
|
}
|
||
|
getCellNodeFromCords(x, y, table) {
|
||
|
const cell = this.getDOMCellFromCords(x, y, table);
|
||
|
if (cell == null) {
|
||
|
return null;
|
||
|
}
|
||
|
const node = lexical.$getNearestNodeFromDOMNode(cell.elem);
|
||
|
if ($isTableCellNode(node)) {
|
||
|
return node;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
getCellNodeFromCordsOrThrow(x, y, table) {
|
||
|
const node = this.getCellNodeFromCords(x, y, table);
|
||
|
if (!node) {
|
||
|
throw new Error('Node at cords not TableCellNode.');
|
||
|
}
|
||
|
return node;
|
||
|
}
|
||
|
canSelectBefore() {
|
||
|
return true;
|
||
|
}
|
||
|
canIndent() {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
function $getElementForTableNode(editor, tableNode) {
|
||
|
const tableElement = editor.getElementByKey(tableNode.getKey());
|
||
|
if (tableElement == null) {
|
||
|
throw new Error('Table Element Not Found');
|
||
|
}
|
||
|
return getTable(tableElement);
|
||
|
}
|
||
|
function convertTableElement(_domNode) {
|
||
|
return {
|
||
|
node: $createTableNode()
|
||
|
};
|
||
|
}
|
||
|
function $createTableNode() {
|
||
|
return lexical.$applyNodeReplacement(new TableNode());
|
||
|
}
|
||
|
function $isTableNode(node) {
|
||
|
return node instanceof TableNode;
|
||
|
}
|
||
|
|
||
|
exports.$computeTableMap = $computeTableMap;
|
||
|
exports.$createTableCellNode = $createTableCellNode;
|
||
|
exports.$createTableNode = $createTableNode;
|
||
|
exports.$createTableNodeWithDimensions = $createTableNodeWithDimensions;
|
||
|
exports.$createTableRowNode = $createTableRowNode;
|
||
|
exports.$createTableSelection = $createTableSelection;
|
||
|
exports.$deleteTableColumn = $deleteTableColumn;
|
||
|
exports.$deleteTableColumn__EXPERIMENTAL = $deleteTableColumn__EXPERIMENTAL;
|
||
|
exports.$deleteTableRow__EXPERIMENTAL = $deleteTableRow__EXPERIMENTAL;
|
||
|
exports.$getElementForTableNode = $getElementForTableNode;
|
||
|
exports.$getNodeTriplet = $getNodeTriplet;
|
||
|
exports.$getTableCellNodeFromLexicalNode = $getTableCellNodeFromLexicalNode;
|
||
|
exports.$getTableCellNodeRect = $getTableCellNodeRect;
|
||
|
exports.$getTableColumnIndexFromTableCellNode = $getTableColumnIndexFromTableCellNode;
|
||
|
exports.$getTableNodeFromLexicalNodeOrThrow = $getTableNodeFromLexicalNodeOrThrow;
|
||
|
exports.$getTableRowIndexFromTableCellNode = $getTableRowIndexFromTableCellNode;
|
||
|
exports.$getTableRowNodeFromTableCellNodeOrThrow = $getTableRowNodeFromTableCellNodeOrThrow;
|
||
|
exports.$insertTableColumn = $insertTableColumn;
|
||
|
exports.$insertTableColumn__EXPERIMENTAL = $insertTableColumn__EXPERIMENTAL;
|
||
|
exports.$insertTableRow = $insertTableRow;
|
||
|
exports.$insertTableRow__EXPERIMENTAL = $insertTableRow__EXPERIMENTAL;
|
||
|
exports.$isTableCellNode = $isTableCellNode;
|
||
|
exports.$isTableNode = $isTableNode;
|
||
|
exports.$isTableRowNode = $isTableRowNode;
|
||
|
exports.$isTableSelection = $isTableSelection;
|
||
|
exports.$removeTableRowAtIndex = $removeTableRowAtIndex;
|
||
|
exports.$unmergeCell = $unmergeCell;
|
||
|
exports.INSERT_TABLE_COMMAND = INSERT_TABLE_COMMAND;
|
||
|
exports.TableCellHeaderStates = TableCellHeaderStates;
|
||
|
exports.TableCellNode = TableCellNode;
|
||
|
exports.TableNode = TableNode;
|
||
|
exports.TableObserver = TableObserver;
|
||
|
exports.TableRowNode = TableRowNode;
|
||
|
exports.applyTableHandlers = applyTableHandlers;
|
||
|
exports.getDOMCellFromTarget = getDOMCellFromTarget;
|
||
|
exports.getTableObserverFromTableElement = getTableObserverFromTableElement;
|