Files
echo-nexus/node_modules/@lexical/table/LexicalTable.dev.js
T

4207 lines
157 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');
var clipboard = require('@lexical/clipboard');
/**
* 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$/;
// .PlaygroundEditorTheme__tableCell width value from
// packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
const COLUMN_WIDTH = 75;
/**
* 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 */
/** @internal */
static getType() {
return 'tablecell';
}
static clone(node) {
return new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
}
afterCloneFrom(node) {
super.afterCloneFrom(node);
this.__rowSpan = node.__rowSpan;
this.__backgroundColor = node.__backgroundColor;
this.__verticalAlign = node.__verticalAlign;
}
static importDOM() {
return {
td: node => ({
conversion: $convertTableCellNodeElement,
priority: 0
}),
th: node => ({
conversion: $convertTableCellNodeElement,
priority: 0
})
};
}
static importJSON(serializedNode) {
return $createTableCellNode().updateFromJSON(serializedNode);
}
updateFromJSON(serializedNode) {
return super.updateFromJSON(serializedNode).setHeaderStyles(serializedNode.headerState).setColSpan(serializedNode.colSpan || 1).setRowSpan(serializedNode.rowSpan || 1).setWidth(serializedNode.width || undefined).setBackgroundColor(serializedNode.backgroundColor || null).setVerticalAlign(serializedNode.verticalAlign || undefined);
}
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;
this.__verticalAlign = undefined;
}
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;
}
if (isValidVerticalAlign(this.__verticalAlign)) {
element.style.verticalAlign = this.__verticalAlign;
}
utils.addClassNamesToElement(element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader);
return element;
}
exportDOM(editor) {
const output = super.exportDOM(editor);
if (lexical.isHTMLElement(output.element)) {
const element = output.element;
element.setAttribute('data-temporary-table-cell-lexical-key', this.getKey());
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() || COLUMN_WIDTH}px`;
element.style.verticalAlign = this.getVerticalAlign() || 'top';
element.style.textAlign = 'start';
if (this.__backgroundColor === null && this.hasHeader()) {
element.style.backgroundColor = '#f2f3f5';
}
}
return output;
}
exportJSON() {
return {
...super.exportJSON(),
...(isValidVerticalAlign(this.__verticalAlign) && {
verticalAlign: this.__verticalAlign
}),
backgroundColor: this.getBackgroundColor(),
colSpan: this.__colSpan,
headerState: this.__headerState,
rowSpan: this.__rowSpan,
width: this.getWidth()
};
}
getColSpan() {
return this.getLatest().__colSpan;
}
setColSpan(colSpan) {
const self = this.getWritable();
self.__colSpan = colSpan;
return self;
}
getRowSpan() {
return this.getLatest().__rowSpan;
}
setRowSpan(rowSpan) {
const self = this.getWritable();
self.__rowSpan = rowSpan;
return self;
}
getTag() {
return this.hasHeader() ? 'th' : 'td';
}
setHeaderStyles(headerState, mask = TableCellHeaderStates.BOTH) {
const self = this.getWritable();
self.__headerState = headerState & mask | self.__headerState & ~mask;
return self;
}
getHeaderStyles() {
return this.getLatest().__headerState;
}
setWidth(width) {
const self = this.getWritable();
self.__width = width;
return self;
}
getWidth() {
return this.getLatest().__width;
}
getBackgroundColor() {
return this.getLatest().__backgroundColor;
}
setBackgroundColor(newBackgroundColor) {
const self = this.getWritable();
self.__backgroundColor = newBackgroundColor;
return self;
}
getVerticalAlign() {
return this.getLatest().__verticalAlign;
}
setVerticalAlign(newVerticalAlign) {
const self = this.getWritable();
self.__verticalAlign = newVerticalAlign || undefined;
return self;
}
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 || prevNode.__verticalAlign !== this.__verticalAlign;
}
isShadowRoot() {
return true;
}
collapseAtStart() {
return true;
}
canBeEmpty() {
return false;
}
canIndent() {
return false;
}
}
function isValidVerticalAlign(verticalAlign) {
return verticalAlign === 'middle' || verticalAlign === 'bottom';
}
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;
}
const verticalAlign = domNode_.style.verticalAlign;
if (isValidVerticalAlign(verticalAlign)) {
tableCellNode.__verticalAlign = verticalAlign;
}
const style = domNode_.style;
const textDecoration = (style && style.textDecoration || '').split(' ');
const hasBoldFontWeight = style.fontWeight === '700' || style.fontWeight === 'bold';
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
const hasItalicFontStyle = style.fontStyle === 'italic';
const hasUnderlineTextDecoration = textDecoration.includes('underline');
return {
after: childLexicalNodes => {
const result = [];
let paragraphNode = null;
const removeSingleLineBreakNode = () => {
if (paragraphNode) {
const firstChild = paragraphNode.getFirstChild();
if (lexical.$isLineBreakNode(firstChild) && paragraphNode.getChildrenSize() === 1) {
firstChild.remove();
}
}
};
for (const child of childLexicalNodes) {
if (lexical.$isInlineElementOrDecoratorNode(child) || lexical.$isTextNode(child) || lexical.$isLineBreakNode(child)) {
if (lexical.$isTextNode(child)) {
if (hasBoldFontWeight) {
child.toggleFormat('bold');
}
if (hasLinethroughTextDecoration) {
child.toggleFormat('strikethrough');
}
if (hasItalicFontStyle) {
child.toggleFormat('italic');
}
if (hasUnderlineTextDecoration) {
child.toggleFormat('underline');
}
}
if (paragraphNode) {
paragraphNode.append(child);
} else {
paragraphNode = lexical.$createParagraphNode().append(child);
result.push(paragraphNode);
}
} else {
result.push(child);
removeSingleLineBreakNode();
paragraphNode = null;
}
}
removeSingleLineBreakNode();
if (result.length === 0) {
result.push(lexical.$createParagraphNode());
}
return result;
},
node: tableCellNode
};
}
function $createTableCellNode(headerState = TableCellHeaderStates.NO_STATUS, 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.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* 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().updateFromJSON(serializedNode);
}
updateFromJSON(serializedNode) {
return super.updateFromJSON(serializedNode).setHeight(serializedNode.height);
}
constructor(height, key) {
super(key);
this.__height = height;
}
exportJSON() {
const height = this.getHeight();
return {
...super.exportJSON(),
...(height === undefined ? undefined : {
height
})
};
}
createDOM(config) {
const element = document.createElement('tr');
if (this.__height) {
element.style.height = `${this.__height}px`;
}
utils.addClassNamesToElement(element, config.theme.tableRow);
return element;
}
extractWithChild(child, selection, destination) {
return destination === 'html';
}
isShadowRoot() {
return true;
}
setHeight(height) {
const self = this.getWritable();
self.__height = height;
return self;
}
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 {
after: children => utils.$descendantsMatching(children, $isTableCellNode),
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.
*
*/
const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
const IS_FIREFOX = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
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;
}
/**
* @deprecated This function does not support merged cells. Use {@link $insertTableRowAtSelection} or {@link $insertTableRowAtNode} instead.
*/
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)) {
formatDevErrorMessage(`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;
};
/**
* Inserts a table row before or after the current focus cell node,
* taking into account any spans. If successful, returns the
* inserted table row node.
*/
function $insertTableRowAtSelection(insertAfter = true) {
const selection = lexical.$getSelection();
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
}
const anchor = selection.anchor.getNode();
const focus = selection.focus.getNode();
const [anchorCell] = $getNodeTriplet(anchor);
const [focusCell,, grid] = $getNodeTriplet(focus);
const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
const {
startRow: anchorStartRow
} = anchorCellMap;
const {
startRow: focusStartRow
} = focusCellMap;
if (insertAfter) {
return $insertTableRowAtNode(anchorStartRow + anchorCell.__rowSpan > focusStartRow + focusCell.__rowSpan ? anchorCell : focusCell, true);
} else {
return $insertTableRowAtNode(focusStartRow < anchorStartRow ? focusCell : anchorCell, false);
}
}
/**
* @deprecated renamed to {@link $insertTableRowAtSelection}
*/
const $insertTableRow__EXPERIMENTAL = $insertTableRowAtSelection;
/**
* Inserts a table row before or after the given cell node,
* taking into account any spans. If successful, returns the
* inserted table row node.
*/
function $insertTableRowAtNode(cellNode, insertAfter = true) {
const [,, grid] = $getNodeTriplet(cellNode);
const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
const columnCount = gridMap[0].length;
const {
startRow: cellStartRow
} = cellMap;
let insertedRow = null;
if (insertAfter) {
const insertAfterEndRow = cellStartRow + cellNode.__rowSpan - 1;
const insertAfterEndRowMap = gridMap[insertAfterEndRow];
const newRow = $createTableRowNode();
for (let i = 0; i < columnCount; i++) {
const {
cell,
startRow
} = insertAfterEndRowMap[i];
if (startRow + cell.__rowSpan - 1 <= insertAfterEndRow) {
const currentCell = insertAfterEndRowMap[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 insertAfterEndRowNode = grid.getChildAtIndex(insertAfterEndRow);
if (!$isTableRowNode(insertAfterEndRowNode)) {
formatDevErrorMessage(`insertAfterEndRow is not a TableRowNode`);
}
insertAfterEndRowNode.insertAfter(newRow);
insertedRow = newRow;
} else {
const insertBeforeStartRow = cellStartRow;
const insertBeforeStartRowMap = gridMap[insertBeforeStartRow];
const newRow = $createTableRowNode();
for (let i = 0; i < columnCount; i++) {
const {
cell,
startRow
} = insertBeforeStartRowMap[i];
if (startRow === insertBeforeStartRow) {
const currentCell = insertBeforeStartRowMap[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 insertBeforeStartRowNode = grid.getChildAtIndex(insertBeforeStartRow);
if (!$isTableRowNode(insertBeforeStartRowNode)) {
formatDevErrorMessage(`insertBeforeStartRow is not a TableRowNode`);
}
insertBeforeStartRowNode.insertBefore(newRow);
insertedRow = newRow;
}
return insertedRow;
}
/**
* @deprecated This function does not support merged cells. Use {@link $insertTableColumnAtSelection} or {@link $insertTableColumnAtNode} instead.
*/
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)) {
formatDevErrorMessage(`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;
}
/**
* Inserts a column before or after the current focus cell node,
* taking into account any spans. If successful, returns the
* first inserted cell node.
*/
function $insertTableColumnAtSelection(insertAfter = true) {
const selection = lexical.$getSelection();
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
}
const anchor = selection.anchor.getNode();
const focus = selection.focus.getNode();
const [anchorCell] = $getNodeTriplet(anchor);
const [focusCell,, grid] = $getNodeTriplet(focus);
const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
const {
startColumn: anchorStartColumn
} = anchorCellMap;
const {
startColumn: focusStartColumn
} = focusCellMap;
if (insertAfter) {
return $insertTableColumnAtNode(anchorStartColumn + anchorCell.__colSpan > focusStartColumn + focusCell.__colSpan ? anchorCell : focusCell, true);
} else {
return $insertTableColumnAtNode(focusStartColumn < anchorStartColumn ? focusCell : anchorCell, false);
}
}
/**
* @deprecated renamed to {@link $insertTableColumnAtSelection}
*/
const $insertTableColumn__EXPERIMENTAL = $insertTableColumnAtSelection;
/**
* Inserts a column before or after the given cell node,
* taking into account any spans. If successful, returns the
* first inserted cell node.
*/
function $insertTableColumnAtNode(cellNode, insertAfter = true, shouldSetSelection = true) {
const [,, grid] = $getNodeTriplet(cellNode);
const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
const rowCount = gridMap.length;
const {
startColumn
} = cellMap;
const insertAfterColumn = insertAfter ? startColumn + cellNode.__colSpan - 1 : startColumn - 1;
const gridFirstChild = grid.getFirstChild();
if (!$isTableRowNode(gridFirstChild)) {
formatDevErrorMessage(`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)) {
formatDevErrorMessage(`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 && shouldSetSelection) {
$moveSelectionToCell(firstInsertedCell);
}
const colWidths = grid.getColWidths();
if (colWidths) {
const newColWidths = [...colWidths];
const columnIndex = insertAfterColumn < 0 ? 0 : insertAfterColumn;
const newWidth = newColWidths[columnIndex];
newColWidths.splice(columnIndex, 0, newWidth);
grid.setColWidths(newColWidths);
}
return firstInsertedCell;
}
/**
* @deprecated This function does not support merged cells. Use {@link $deleteTableColumnAtSelection} instead.
*/
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 $deleteTableRowAtSelection() {
const selection = lexical.$getSelection();
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
}
const [anchor, focus] = selection.isBackward() ? [selection.focus.getNode(), selection.anchor.getNode()] : [selection.anchor.getNode(), 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 or bottom have to be trimmed
if (cellStartRow < anchorStartRow || cellStartRow + cell.__rowSpan - 1 > focusEndRow) {
const intersectionStart = Math.max(cellStartRow, anchorStartRow);
const intersectionEnd = Math.min(cell.__rowSpan + cellStartRow - 1, focusEndRow);
const overflowRowsCount = intersectionStart <= intersectionEnd ? intersectionEnd - intersectionStart + 1 : 0;
cell.setRowSpan(cell.__rowSpan - overflowRowsCount);
}
// Rows overflowing bottom have to be moved to the next row
if (cellStartRow >= anchorStartRow && cellStartRow + cell.__rowSpan - 1 > focusEndRow &&
// Handle overflow only once
row === focusEndRow) {
if (!(nextRowNode !== null)) {
formatDevErrorMessage(`Expected nextRowNode not to be null`);
}
let insertAfterCell = null;
for (let columnIndex = 0; columnIndex < column; columnIndex++) {
const currentCellMap = nextRow[columnIndex];
const currentCell = currentCellMap.cell;
// Checking the cell having startRow as same as nextRow
if (currentCellMap.startRow === row + 1) {
insertAfterCell = currentCell;
}
if (currentCell.__colSpan > 1) {
columnIndex += currentCell.__colSpan - 1;
}
}
if (insertAfterCell === null) {
$insertFirst(nextRowNode, cell);
} else {
insertAfterCell.insertAfter(cell);
}
}
}
const rowNode = grid.getChildAtIndex(row);
if (!$isTableRowNode(rowNode)) {
formatDevErrorMessage(`Expected TableNode 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);
}
}
/**
* @deprecated renamed to {@link $deleteTableRowAtSelection}
*/
const $deleteTableRow__EXPERIMENTAL = $deleteTableRowAtSelection;
function $deleteTableColumnAtSelection() {
const selection = lexical.$getSelection();
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
}
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 = anchorStartColumn > focusStartColumn ? focusRowMap[anchorStartColumn + anchorCell.__colSpan] : focusRowMap[focusStartColumn + focusCell.__colSpan];
if (nextColumn !== undefined) {
const {
cell
} = nextColumn;
$moveSelectionToCell(cell);
} else {
const previousRow = focusStartColumn < anchorStartColumn ? focusRowMap[focusStartColumn - 1] : focusRowMap[anchorStartColumn - 1];
const {
cell
} = previousRow;
$moveSelectionToCell(cell);
}
const colWidths = grid.getColWidths();
if (colWidths) {
const newColWidths = [...colWidths];
newColWidths.splice(startColumn, selectedColumnCount);
grid.setColWidths(newColWidths);
}
}
/**
* @deprecated renamed to {@link $deleteTableColumnAtSelection}
*/
const $deleteTableColumn__EXPERIMENTAL = $deleteTableColumnAtSelection;
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 $mergeCells(cellNodes) {
if (cellNodes.length === 0) {
return null;
}
// Find the table node
const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNodes[0]);
const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
// Find the boundaries of the selection including merged cells
let minRow = Infinity;
let maxRow = -Infinity;
let minCol = Infinity;
let maxCol = -Infinity;
// First pass: find the actual boundaries considering merged cells
const processedCells = new Set();
for (const row of gridMap) {
for (const mapCell of row) {
if (!mapCell || !mapCell.cell) {
continue;
}
const cellKey = mapCell.cell.getKey();
if (processedCells.has(cellKey)) {
continue;
}
if (cellNodes.some(cell => cell.is(mapCell.cell))) {
processedCells.add(cellKey);
// Get the actual position of this cell in the grid
const cellStartRow = mapCell.startRow;
const cellStartCol = mapCell.startColumn;
const cellRowSpan = mapCell.cell.__rowSpan || 1;
const cellColSpan = mapCell.cell.__colSpan || 1;
// Update boundaries considering the cell's actual position and span
minRow = Math.min(minRow, cellStartRow);
maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1);
minCol = Math.min(minCol, cellStartCol);
maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1);
}
}
}
// Validate boundaries
if (minRow === Infinity || minCol === Infinity) {
return null;
}
// The total span of the merged cell
const totalRowSpan = maxRow - minRow + 1;
const totalColSpan = maxCol - minCol + 1;
// Use the top-left cell as the target cell
const targetCellMap = gridMap[minRow][minCol];
if (!targetCellMap.cell) {
return null;
}
const targetCell = targetCellMap.cell;
// Set the spans for the target cell
targetCell.setColSpan(totalColSpan);
targetCell.setRowSpan(totalRowSpan);
// Move content from other cells to the target cell
const seenCells = new Set([targetCell.getKey()]);
// Second pass: merge content and remove other cells
for (let row = minRow; row <= maxRow; row++) {
for (let col = minCol; col <= maxCol; col++) {
const mapCell = gridMap[row][col];
if (!mapCell.cell) {
continue;
}
const currentCell = mapCell.cell;
const key = currentCell.getKey();
if (!seenCells.has(key)) {
seenCells.add(key);
const isEmpty = $cellContainsEmptyParagraph(currentCell);
if (!isEmpty) {
targetCell.append(...currentCell.getChildren());
}
currentCell.remove();
}
}
}
// Ensure target cell has content
if (targetCell.getChildrenSize() === 0) {
targetCell.append(lexical.$createParagraphNode());
}
return targetCell;
}
function $cellContainsEmptyParagraph(cell) {
if (cell.getChildrenSize() !== 1) {
return false;
}
const firstChild = cell.getFirstChildOrThrow();
if (!lexical.$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
return false;
}
return true;
}
function $unmergeCell() {
const selection = lexical.$getSelection();
if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
}
const anchor = selection.anchor.getNode();
const cellNode = utils.$findMatchingParent(anchor, $isTableCellNode);
if (!$isTableCellNode(cellNode)) {
formatDevErrorMessage(`Expected to find a parent TableCellNode`);
}
return $unmergeCellNode(cellNode);
}
function $unmergeCellNode(cellNode) {
const [cell, row, grid] = $getNodeTriplet(cellNode);
const colSpan = cell.__colSpan;
const rowSpan = cell.__rowSpan;
if (colSpan === 1 && rowSpan === 1) {
return;
}
const [map, cellMap] = $computeTableMap(grid, cell, cell);
const {
startColumn,
startRow
} = cellMap;
// Create a heuristic for what the style of the unmerged cells should be
// based on whether every row or column already had that state before the
// unmerge.
const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
const colStyles = Array.from({
length: colSpan
}, (_v, i) => {
let colStyle = baseColStyle;
for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
}
return colStyle;
});
const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
const rowStyles = Array.from({
length: rowSpan
}, (_v, i) => {
let rowStyle = baseRowStyle;
for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
rowStyle &= map[i + startRow][colIdx].cell.__headerState;
}
return rowStyle;
});
if (colSpan > 1) {
for (let i = 1; i < colSpan; i++) {
cell.insertAfter($createTableCellNode(colStyles[i] | rowStyles[0]).append(lexical.$createParagraphNode()));
}
cell.setColSpan(1);
}
if (rowSpan > 1) {
let currentRowNode;
for (let i = 1; i < rowSpan; i++) {
const currentRow = startRow + i;
const currentRowMap = map[currentRow];
currentRowNode = (currentRowNode || row).getNextSibling();
if (!$isTableRowNode(currentRowNode)) {
formatDevErrorMessage(`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 = colSpan - 1; j >= 0; j--) {
$insertFirst(currentRowNode, $createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
}
} else {
for (let j = colSpan - 1; j >= 0; j--) {
insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
}
}
}
cell.setRowSpan(1);
}
}
function $computeTableMap(tableNode, cellA, cellB) {
const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(tableNode, cellA, cellB);
if (!(cellAValue !== null)) {
formatDevErrorMessage(`Anchor not found in Table`);
}
if (!(cellBValue !== null)) {
formatDevErrorMessage(`Focus not found in Table`);
}
return [tableMap, cellAValue, cellBValue];
}
function $computeTableMapSkipCellCheck(tableNode, cellA, cellB) {
const tableMap = [];
let cellAValue = null;
let cellBValue = null;
function getMapRow(i) {
let row = tableMap[i];
if (row === undefined) {
tableMap[i] = row = [];
}
return row;
}
const gridChildren = tableNode.getChildren();
for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
const row = gridChildren[rowIdx];
if (!$isTableRowNode(row)) {
formatDevErrorMessage(`Expected TableNode children to be TableRowNode`);
}
const startMapRow = getMapRow(rowIdx);
for (let cell = row.getFirstChild(), colIdx = 0; cell != null; cell = cell.getNextSibling()) {
if (!$isTableCellNode(cell)) {
formatDevErrorMessage(`Expected TableRowNode children to be TableCellNode`);
} // Skip past any columns that were merged from a higher row
while (startMapRow[colIdx] !== undefined) {
colIdx++;
}
const value = {
cell,
startColumn: colIdx,
startRow: rowIdx
};
const {
__rowSpan: rowSpan,
__colSpan: colSpan
} = cell;
for (let j = 0; j < rowSpan; j++) {
if (rowIdx + j >= gridChildren.length) {
// The table is non-rectangular with a rowSpan
// below the last <tr> in the table.
// We should probably handle this with a node transform
// to ensure that tables are always rectangular but this
// will avoid crashes such as #6584
// Note that there are probably still latent bugs
// regarding colSpan or general cell count mismatches.
break;
}
const mapRow = getMapRow(rowIdx + j);
for (let i = 0; i < colSpan; i++) {
mapRow[colIdx + i] = value;
}
}
if (cellA !== null && cellAValue === null && cellA.is(cell)) {
cellAValue = value;
}
if (cellB !== null && cellBValue === null && cellB.is(cell)) {
cellBValue = value;
}
}
}
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_)) {
formatDevErrorMessage(`Expected to find a parent TableCellNode`);
}
cell = cell_;
} else {
const cell_ = utils.$findMatchingParent(source.getNode(), $isTableCellNode);
if (!$isTableCellNode(cell_)) {
formatDevErrorMessage(`Expected to find a parent TableCellNode`);
}
cell = cell_;
}
const row = cell.getParent();
if (!$isTableRowNode(row)) {
formatDevErrorMessage(`Expected TableCellNode to have a parent TableRowNode`);
}
const grid = row.getParent();
if (!$isTableNode(grid)) {
formatDevErrorMessage(`Expected TableRowNode to have a parent TableNode`);
}
return [cell, row, grid];
}
function $computeTableCellRectSpans(map, boundary) {
const {
minColumn,
maxColumn,
minRow,
maxRow
} = boundary;
let topSpan = 1;
let leftSpan = 1;
let rightSpan = 1;
let bottomSpan = 1;
const topRow = map[minRow];
const bottomRow = map[maxRow];
for (let col = minColumn; col <= maxColumn; col++) {
topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan);
bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan);
}
for (let row = minRow; row <= maxRow; row++) {
leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan);
rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan);
}
return {
bottomSpan,
leftSpan,
rightSpan,
topSpan
};
}
function $computeTableCellRectBoundary(map, cellAMap, cellBMap) {
// Initial boundaries based on the anchor and focus cells
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);
// Keep expanding until we have a complete rectangle
let hasChanges;
do {
hasChanges = false;
// Check all cells in the table
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[0].length; col++) {
const cell = map[row][col];
if (!cell) {
continue;
}
const cellEndCol = cell.startColumn + cell.cell.__colSpan - 1;
const cellEndRow = cell.startRow + cell.cell.__rowSpan - 1;
// Check if this cell intersects with our current selection rectangle
const intersectsHorizontally = cell.startColumn <= maxColumn && cellEndCol >= minColumn;
const intersectsVertically = cell.startRow <= maxRow && cellEndRow >= minRow;
// If the cell intersects either horizontally or vertically
if (intersectsHorizontally && intersectsVertically) {
// Expand boundaries to include this cell completely
const newMinColumn = Math.min(minColumn, cell.startColumn);
const newMaxColumn = Math.max(maxColumn, cellEndCol);
const newMinRow = Math.min(minRow, cell.startRow);
const newMaxRow = Math.max(maxRow, cellEndRow);
// Check if boundaries changed
if (newMinColumn !== minColumn || newMaxColumn !== maxColumn || newMinRow !== minRow || newMaxRow !== maxRow) {
minColumn = newMinColumn;
maxColumn = newMaxColumn;
minRow = newMinRow;
maxRow = newMaxRow;
hasChanges = true;
}
}
}
}
} while (hasChanges);
return {
maxColumn,
maxRow,
minColumn,
minRow
};
}
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;
}
function $getCellNodes(tableSelection) {
const [[anchorNode, anchorCell, anchorRow, anchorTable], [focusNode, focusCell, focusRow, focusTable]] = ['anchor', 'focus'].map(k => {
const node = tableSelection[k].getNode();
const cellNode = utils.$findMatchingParent(node, $isTableCellNode);
if (!$isTableCellNode(cellNode)) {
formatDevErrorMessage(`Expected TableSelection ${k} to be (or a child of) TableCellNode, got key ${node.getKey()} of type ${node.getType()}`);
}
const rowNode = cellNode.getParent();
if (!$isTableRowNode(rowNode)) {
formatDevErrorMessage(`Expected TableSelection ${k} cell parent to be a TableRowNode`);
}
const tableNode = rowNode.getParent();
if (!$isTableNode(tableNode)) {
formatDevErrorMessage(`Expected TableSelection ${k} row parent to be a TableNode`);
}
return [node, cellNode, rowNode, tableNode];
});
// TODO: nested tables may violate this
if (!anchorTable.is(focusTable)) {
formatDevErrorMessage(`Expected TableSelection anchor and focus to be in the same table`);
}
return {
anchorCell,
anchorNode,
anchorRow,
anchorTable,
focusCell,
focusNode,
focusRow,
focusTable
};
}
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];
}
/**
* {@link $createTableSelection} unfortunately makes it very easy to create
* nonsense selections, so we have a method to see if the selection probably
* makes sense.
*
* @returns true if the TableSelection is (probably) valid
*/
isValid() {
return this.tableKey !== 'root' && this.anchor.key !== 'root' && this.anchor.type === 'element' && this.focus.key !== 'root' && this.focus.type === 'element';
}
/**
* 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) {
return $isTableSelection(selection) && this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
}
set(tableKey, anchorCellKey, focusCellKey) {
// note: closure compiler's acorn does not support ||=
this.dirty = this.dirty || tableKey !== this.tableKey || anchorCellKey !== this.anchor.key || focusCellKey !== this.focus.key;
this.tableKey = tableKey;
this.anchor.key = anchorCellKey;
this.focus.key = focusCellKey;
this._cachedNodes = null;
}
clone() {
return new TableSelection(this.tableKey, lexical.$createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), lexical.$createPoint(this.focus.key, this.focus.offset, this.focus.type));
}
isCollapsed() {
return false;
}
extract() {
return this.getNodes();
}
insertRawText(text) {
// Do nothing?
}
insertText() {
// Do nothing?
}
/**
* Returns whether the provided TextFormatType is present on the Selection.
* This will be true if any paragraph in table cells has the specified format.
*
* @param type the TextFormatType to check for.
* @returns true if the provided format is currently toggled on on the Selection, false otherwise.
*/
hasFormat(type) {
let format = 0;
const cellNodes = this.getNodes().filter($isTableCellNode);
cellNodes.forEach(cellNode => {
const paragraph = cellNode.getFirstChild();
if (lexical.$isParagraphNode(paragraph)) {
format |= paragraph.getTextFormat();
}
});
const formatFlag = lexical.TEXT_TYPE_TO_FORMAT[type];
return (format & formatFlag) !== 0;
}
insertNodes(nodes) {
const focusNode = this.focus.getNode();
if (!lexical.$isElementNode(focusNode)) {
formatDevErrorMessage(`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 {
anchorCell,
focusCell
} = $getCellNodes(this);
const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
if (!(anchorCellNodeRect !== null)) {
formatDevErrorMessage(`getCellRect: expected to find AnchorNode`);
}
const focusCellNodeRect = $getTableCellNodeRect(focusCell);
if (!(focusCellNodeRect !== null)) {
formatDevErrorMessage(`getCellRect: expected to find focusCellNode`);
}
const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
const stopX = Math.max(anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1, focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1);
const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
const stopY = Math.max(anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1, focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1);
return {
fromX: Math.min(startX, stopX),
fromY: Math.min(startY, stopY),
toX: Math.max(startX, stopX),
toY: Math.max(startY, stopY)
};
}
getNodes() {
if (!this.isValid()) {
return [];
}
const cachedNodes = this._cachedNodes;
if (cachedNodes !== null) {
return cachedNodes;
}
const {
anchorTable: tableNode,
anchorCell,
focusCell
} = $getCellNodes(this);
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)) {
formatDevErrorMessage(`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)) {
formatDevErrorMessage(`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);
const {
minColumn,
maxColumn,
minRow,
maxRow
} = $computeTableCellRectBoundary(map, cellAMap, cellBMap);
// We use a Map here because merged cells in the grid would otherwise
// show up multiple times in the nodes array
const nodeMap = new Map([[tableNode.getKey(), 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)) {
formatDevErrorMessage(`Expected TableCellNode parent to be a TableRowNode`);
}
if (currentRow !== lastRow) {
nodeMap.set(currentRow.getKey(), currentRow);
lastRow = currentRow;
}
if (!nodeMap.has(cell.getKey())) {
$visitRecursively(cell, childNode => {
nodeMap.set(childNode.getKey(), childNode);
});
}
}
}
const nodes = Array.from(nodeMap.values());
if (!lexical.isCurrentlyReadOnlyMode()) {
this._cachedNodes = nodes;
}
return nodes;
}
getTextContent() {
const nodes = this.getNodes().filter(node => $isTableCellNode(node));
let textContent = '';
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const row = node.__parent;
const nextRow = (nodes[i + 1] || {}).__parent;
textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
}
return textContent;
}
}
function $isTableSelection(x) {
return x instanceof TableSelection;
}
function $createTableSelection() {
// TODO this is a suboptimal design, it doesn't make sense to have
// a table selection that isn't associated with a table. This
// constructor should have required arguments and in true we
// should check that they point to a table and are element points to
// cell nodes of that table.
const anchor = lexical.$createPoint('root', 0, 'element');
const focus = lexical.$createPoint('root', 0, 'element');
return new TableSelection('root', anchor, focus);
}
function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
const tableNodeKey = tableNode.getKey();
const anchorCellKey = anchorCell.getKey();
const focusCellKey = focusCell.getKey();
{
if (!tableNode.isAttached()) {
formatDevErrorMessage(`$createTableSelectionFrom: tableNode ${tableNodeKey} is not attached`);
}
if (!tableNode.is($findTableNode(anchorCell))) {
formatDevErrorMessage(`$createTableSelectionFrom: anchorCell ${anchorCellKey} is not in table ${tableNodeKey}`);
}
if (!tableNode.is($findTableNode(focusCell))) {
formatDevErrorMessage(`$createTableSelectionFrom: focusCell ${focusCellKey} is not in table ${tableNodeKey}`);
} // TODO: Check for rectangular grid
}
const prevSelection = lexical.$getSelection();
const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() : $createTableSelection();
nextSelection.set(tableNode.getKey(), anchorCell.getKey(), focusCell.getKey());
return nextSelection;
}
/**
* Depth first visitor
* @param node The starting node
* @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
*/
function $visitRecursively(node, $visit) {
const stack = [[node]];
for (let currentArray = stack.at(-1); currentArray !== undefined && stack.length > 0; currentArray = stack.at(-1)) {
const currentNode = currentArray.pop();
if (currentNode === undefined) {
stack.pop();
} else if ($visit(currentNode) !== false && lexical.$isElementNode(currentNode)) {
stack.push(currentNode.getChildren());
}
}
}
function $getTableAndElementByKey(tableNodeKey, editor = lexical.$getEditor()) {
const tableNode = lexical.$getNodeByKey(tableNodeKey);
if (!$isTableNode(tableNode)) {
formatDevErrorMessage(`TableObserver: Expected tableNodeKey ${tableNodeKey} to be a TableNode`);
}
const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNodeKey));
if (!(tableElement !== null)) {
formatDevErrorMessage(`TableObserver: Expected to find TableElement in DOM for key ${tableNodeKey}`);
}
return {
tableElement,
tableNode
};
}
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.isSelecting = false;
this.pointerType = null;
this.shouldCheckSelection = false;
this.abortController = new AbortController();
this.listenerOptions = {
signal: this.abortController.signal
};
this.nextFocus = null;
this.trackTable();
}
getTable() {
return this.table;
}
removeListeners() {
this.abortController.abort('removeListeners');
Array.from(this.listenersToRemove).forEach(removeListener => removeListener());
this.listenersToRemove.clear();
}
$lookup() {
return $getTableAndElementByKey(this.tableNodeKey, this.editor);
}
trackTable() {
const observer = new MutationObserver(records => {
this.editor.getEditorState().read(() => {
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 === 'TBODY' || nodeName === 'THEAD' || nodeName === 'TR') {
gridNeedsRedraw = true;
break;
}
}
if (!gridNeedsRedraw) {
return;
}
const {
tableNode,
tableElement
} = this.$lookup();
this.table = getTable(tableNode, tableElement);
}, {
editor: this.editor
});
});
this.editor.getEditorState().read(() => {
const {
tableNode,
tableElement
} = this.$lookup();
this.table = getTable(tableNode, tableElement);
observer.observe(tableElement, {
attributes: true,
childList: true,
subtree: true
});
}, {
editor: this.editor
});
}
$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();
const {
tableNode,
tableElement
} = this.$lookup();
const grid = getTable(tableNode, tableElement);
$updateDOMForSelection(editor, grid, null);
if (lexical.$getSelection() !== null) {
lexical.$setSelection(null);
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
}
}
$enableHighlightStyle() {
const editor = this.editor;
const {
tableElement
} = this.$lookup();
utils.removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
tableElement.classList.remove('disable-selection');
this.hasHijackedSelectionStyles = false;
}
$disableHighlightStyle() {
const {
tableElement
} = this.$lookup();
utils.addClassNamesToElement(tableElement, this.editor._config.theme.tableSelection);
this.hasHijackedSelectionStyles = true;
}
$updateTableTableSelection(selection) {
if (selection !== null) {
if (!(selection.tableKey === this.tableNodeKey)) {
formatDevErrorMessage(`TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('${selection.tableKey}' !== '${this.tableNodeKey}')`);
}
const editor = this.editor;
this.tableSelection = selection;
this.isHighlightingCells = true;
this.$disableHighlightStyle();
this.updateDOMSelection();
$updateDOMForSelection(editor, this.table, this.tableSelection);
} else {
this.$clearHighlight();
}
}
/**
* @internal
* Firefox has a strange behavior where pressing the down arrow key from
* above the table will move the caret after the table and then lexical
* will select the last cell instead of the first.
* We do still want to let the browser handle caret movement but we will
* use this property to "tag" the update so that we can recheck the
* selection after the event is processed.
*/
setShouldCheckSelection() {
this.shouldCheckSelection = true;
}
/**
* @internal
*/
getAndClearShouldCheckSelection() {
if (this.shouldCheckSelection) {
this.shouldCheckSelection = false;
return true;
}
return false;
}
/**
* @internal
* When handling mousemove events we track what the focus cell should be, but
* the DOM selection may end up somewhere else entirely. We don't have an elegant
* way to handle this after the DOM selection has been resolved in a
* SELECTION_CHANGE_COMMAND callback.
*/
setNextFocus(nextFocus) {
this.nextFocus = nextFocus;
}
/** @internal */
getAndClearNextFocus() {
const {
nextFocus
} = this;
if (nextFocus !== null) {
this.nextFocus = null;
}
return nextFocus;
}
/** @internal */
updateDOMSelection() {
if (this.anchorCell !== null && this.focusCell !== null) {
const domSelection = lexical.getDOMSelection(this.editor._window);
// We are not using a native selection for tables, and if we
// set one then the reconciler will undo it.
// TODO - it would make sense to have one so that native
// copy/paste worked. Right now we have to emulate with
// keyboard events but it won't fire if triggered from the menu
if (domSelection && domSelection.rangeCount > 0) {
domSelection.removeAllRanges();
}
}
}
$setFocusCellForSelection(cell, ignoreStart = false) {
const editor = this.editor;
const {
tableNode
} = this.$lookup();
const cellX = cell.x;
const cellY = cell.y;
this.focusCell = cell;
if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
this.isHighlightingCells = true;
this.$disableHighlightStyle();
} else if (cellX === this.focusX && cellY === this.focusY) {
return false;
}
this.focusX = cellX;
this.focusY = cellY;
if (this.isHighlightingCells) {
const focusTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
if (this.tableSelection != null && this.anchorCellNodeKey != null && focusTableCellNode !== null) {
this.focusCellNodeKey = focusTableCellNode.getKey();
this.tableSelection = $createTableSelectionFrom(tableNode, this.$getAnchorTableCellOrThrow(), focusTableCellNode);
lexical.$setSelection(this.tableSelection);
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
$updateDOMForSelection(editor, this.table, this.tableSelection);
return true;
}
}
return false;
}
$getAnchorTableCell() {
return this.anchorCellNodeKey ? lexical.$getNodeByKey(this.anchorCellNodeKey) : null;
}
$getAnchorTableCellOrThrow() {
const anchorTableCell = this.$getAnchorTableCell();
if (!(anchorTableCell !== null)) {
formatDevErrorMessage(`TableObserver anchorTableCell is null`);
}
return anchorTableCell;
}
$getFocusTableCell() {
return this.focusCellNodeKey ? lexical.$getNodeByKey(this.focusCellNodeKey) : null;
}
$getFocusTableCellOrThrow() {
const focusTableCell = this.$getFocusTableCell();
if (!(focusTableCell !== null)) {
formatDevErrorMessage(`TableObserver focusTableCell is null`);
}
return focusTableCell;
}
$setAnchorCellForSelection(cell) {
this.isHighlightingCells = false;
this.anchorCell = cell;
this.anchorX = cell.x;
this.anchorY = cell.y;
const {
tableNode
} = this.$lookup();
const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
if (anchorTableCellNode !== null) {
const anchorNodeKey = anchorTableCellNode.getKey();
this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
this.anchorCellNodeKey = anchorNodeKey;
}
}
$formatCells(type) {
const selection = lexical.$getSelection();
if (!$isTableSelection(selection)) {
formatDevErrorMessage(`Expected Table selection`);
}
const formatSelection = lexical.$createRangeSelection();
const anchor = formatSelection.anchor;
const focus = formatSelection.focus;
const cellNodes = selection.getNodes().filter($isTableCellNode);
if (!(cellNodes.length > 0)) {
formatDevErrorMessage(`No table cells present`);
}
const paragraph = cellNodes[0].getFirstChild();
const alignFormatWith = lexical.$isParagraphNode(paragraph) ? paragraph.getFormatFlags(type, null) : null;
cellNodes.forEach(cellNode => {
anchor.set(cellNode.getKey(), 0, 'element');
focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
formatSelection.formatText(type, alignFormatWith);
});
lexical.$setSelection(selection);
this.editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
}
$clearText() {
const {
editor
} = this;
const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
if (!$isTableNode(tableNode)) {
throw new Error('Expected TableNode.');
}
const selection = lexical.$getSelection();
if (!$isTableSelection(selection)) {
formatDevErrorMessage(`Expected TableSelection`);
}
const selectedNodes = selection.getNodes().filter($isTableCellNode);
// Check if the entire table is selected by verifying first and last cells
const firstRow = tableNode.getFirstChild();
const lastRow = tableNode.getLastChild();
const isEntireTableSelected = selectedNodes.length > 0 && firstRow !== null && lastRow !== null && $isTableRowNode(firstRow) && $isTableRowNode(lastRow) && selectedNodes[0] === firstRow.getFirstChild() && selectedNodes[selectedNodes.length - 1] === lastRow.getLastChild();
if (isEntireTableSelected) {
tableNode.selectPrevious();
const parent = tableNode.getParent();
// Delete entire table
tableNode.remove();
// Handle case when table was the only node
if (lexical.$isRootNode(parent) && parent.isEmpty()) {
editor.dispatchCommand(lexical.INSERT_PARAGRAPH_COMMAND, undefined);
}
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);
}
}
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
const isPointerDownOnEvent = event => {
return (event.buttons & 1) === 1;
};
function isHTMLTableElement(el) {
return lexical.isHTMLElement(el) && el.nodeName === 'TABLE';
}
function getTableElement(tableNode, dom) {
if (!dom) {
return dom;
}
const element = isHTMLTableElement(dom) ? dom : tableNode.getDOMSlot(dom).element;
if (!(element.nodeName === 'TABLE')) {
formatDevErrorMessage(`getTableElement: Expecting table in as DOM node for TableNode, not ${dom.nodeName}`);
}
return element;
}
function getEditorWindow(editor) {
return editor._window;
}
function $findParentTableCellNodeInTable(tableNode, node) {
for (let currentNode = node, lastTableCellNode = null; currentNode !== null; currentNode = currentNode.getParent()) {
if (tableNode.is(currentNode)) {
return lastTableCellNode;
} else if ($isTableCellNode(currentNode)) {
lastTableCellNode = currentNode;
}
}
return null;
}
const ARROW_KEY_COMMANDS_WITH_DIRECTION = [[lexical.KEY_ARROW_DOWN_COMMAND, 'down'], [lexical.KEY_ARROW_UP_COMMAND, 'up'], [lexical.KEY_ARROW_LEFT_COMMAND, 'backward'], [lexical.KEY_ARROW_RIGHT_COMMAND, 'forward']];
const DELETE_TEXT_COMMANDS = [lexical.DELETE_WORD_COMMAND, lexical.DELETE_LINE_COMMAND, lexical.DELETE_CHARACTER_COMMAND];
const DELETE_KEY_COMMANDS = [lexical.KEY_BACKSPACE_COMMAND, lexical.KEY_DELETE_COMMAND];
function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
const rootElement = editor.getRootElement();
const editorWindow = getEditorWindow(editor);
if (!(rootElement !== null && editorWindow !== null)) {
formatDevErrorMessage(`applyTableHandlers: editor has no root element set`);
}
const tableObserver = new TableObserver(editor, tableNode.getKey());
const tableElement = getTableElement(tableNode, element);
attachTableObserverToTableElement(tableElement, tableObserver);
tableObserver.listenersToRemove.add(() => detachTableObserverFromTableElement(tableElement, tableObserver));
const createPointerHandlers = () => {
if (tableObserver.isSelecting) {
return;
}
const onPointerUp = () => {
tableObserver.isSelecting = false;
editorWindow.removeEventListener('pointerup', onPointerUp);
editorWindow.removeEventListener('pointermove', onPointerMove);
};
const onPointerMove = moveEvent => {
if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
tableObserver.isSelecting = false;
editorWindow.removeEventListener('pointerup', onPointerUp);
editorWindow.removeEventListener('pointermove', onPointerMove);
return;
}
if (!lexical.isDOMNode(moveEvent.target)) {
return;
}
let focusCell = null;
// In firefox the moveEvent.target may be captured so we must always
// consult the coordinates #7245
const override = !(IS_FIREFOX || tableElement.contains(moveEvent.target));
if (override) {
focusCell = getDOMCellInTableFromTarget(tableElement, moveEvent.target);
} else {
for (const el of document.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
focusCell = getDOMCellInTableFromTarget(tableElement, el);
if (focusCell) {
break;
}
}
}
if (focusCell && (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem)) {
tableObserver.setNextFocus({
focusCell,
override
});
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
}
};
tableObserver.isSelecting = true;
editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
};
const onPointerDown = event => {
tableObserver.pointerType = event.pointerType;
if (event.button !== 0 || !lexical.isDOMNode(event.target) || !editorWindow) {
return;
}
const targetCell = getDOMCellFromTarget(event.target);
if (targetCell !== null) {
editor.update(() => {
const prevSelection = lexical.$getPreviousSelection();
// We can't trust Firefox to do the right thing with the selection and
// we don't have a proper state machine to do this "correctly" but
// if we go ahead and make the table selection now it will work
if (IS_FIREFOX && event.shiftKey && $isSelectionInTable(prevSelection, tableNode) && (lexical.$isRangeSelection(prevSelection) || $isTableSelection(prevSelection))) {
const prevAnchorNode = prevSelection.anchor.getNode();
const prevAnchorCell = $findParentTableCellNodeInTable(tableNode, prevSelection.anchor.getNode());
if (prevAnchorCell) {
tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCell));
tableObserver.$setFocusCellForSelection(targetCell);
stopEvent(event);
} else {
const newSelection = tableNode.isBefore(prevAnchorNode) ? tableNode.selectStart() : tableNode.selectEnd();
newSelection.anchor.set(prevSelection.anchor.key, prevSelection.anchor.offset, prevSelection.anchor.type);
}
} else {
// Only set anchor cell for selection if this is not a simple touch tap
// Touch taps should not initiate table selection mode
if (event.pointerType !== 'touch') {
tableObserver.$setAnchorCellForSelection(targetCell);
}
}
});
}
createPointerHandlers();
};
tableElement.addEventListener('pointerdown', onPointerDown, tableObserver.listenerOptions);
tableObserver.listenersToRemove.add(() => {
tableElement.removeEventListener('pointerdown', onPointerDown);
});
const onTripleClick = event => {
if (event.detail >= 3 && lexical.isDOMNode(event.target)) {
const targetCell = getDOMCellFromTarget(event.target);
if (targetCell !== null) {
event.preventDefault();
}
}
};
tableElement.addEventListener('mousedown', onTripleClick, tableObserver.listenerOptions);
tableObserver.listenersToRemove.add(() => {
tableElement.removeEventListener('mousedown', onTripleClick);
});
// Clear selection when clicking outside of dom.
const pointerDownCallback = event => {
const target = event.target;
if (event.button !== 0 || !lexical.isDOMNode(target)) {
return;
}
editor.update(() => {
const selection = lexical.$getSelection();
if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target)) {
tableObserver.$clearHighlight();
}
});
};
editorWindow.addEventListener('pointerdown', pointerDownCallback, tableObserver.listenerOptions);
tableObserver.listenersToRemove.add(() => {
editorWindow.removeEventListener('pointerdown', pointerDownCallback);
});
for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
tableObserver.listenersToRemove.add(editor.registerCommand(command, event => $handleArrowKey(editor, event, direction, 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 = $findParentTableCellNodeInTable(tableNode, selection.focus.getNode());
if (focusCellNode !== null) {
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 = $findParentTableCellNodeInTable(tableNode, selection.anchor.getNode());
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;
}
}
return false;
};
for (const command of DELETE_TEXT_COMMANDS) {
tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler(command), lexical.COMMAND_PRIORITY_CRITICAL));
}
const $deleteCellHandler = event => {
const selection = lexical.$getSelection();
if (!($isTableSelection(selection) || lexical.$isRangeSelection(selection))) {
return false;
}
// If the selection is inside the table but should remove the whole table
// we expand the selection so that both the anchor and focus are outside
// the table and the editor's command listener will handle the delete
const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
if (isAnchorInside !== isFocusInside) {
const tablePoint = isAnchorInside ? 'anchor' : 'focus';
const outerPoint = isAnchorInside ? 'focus' : 'anchor';
// Preserve the outer point
const {
key,
offset,
type
} = selection[outerPoint];
// Expand the selection around the table
const newSelection = tableNode[selection[tablePoint].isBefore(selection[outerPoint]) ? 'selectPrevious' : 'selectNext']();
// Restore the outer point of the selection
newSelection[outerPoint].set(key, offset, type);
// Let the base implementation handle the rest
return false;
}
if (!$isSelectionInTable(selection, tableNode)) {
return false;
}
if ($isTableSelection(selection)) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
tableObserver.$clearText();
return true;
}
return false;
};
for (const command of DELETE_KEY_COMMANDS) {
tableObserver.listenersToRemove.add(editor.registerCommand(command, $deleteCellHandler, lexical.COMMAND_PRIORITY_CRITICAL));
}
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.CUT_COMMAND, event => {
const selection = lexical.$getSelection();
if (selection) {
if (!($isTableSelection(selection) || lexical.$isRangeSelection(selection))) {
return false;
}
// Copying to the clipboard is async so we must capture the data
// before we delete it
void clipboard.copyToClipboard(editor, utils.objectKlassEquals(event, ClipboardEvent) ? event : null, clipboard.$getClipboardDataFromSelection(selection));
const intercepted = $deleteCellHandler(event);
if (lexical.$isRangeSelection(selection)) {
selection.removeText();
return true;
}
return intercepted;
}
return false;
}, 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.FORMAT_ELEMENT_COMMAND, formatType => {
const selection = lexical.$getSelection();
if (!$isTableSelection(selection) || !$isSelectionInTable(selection, tableNode)) {
return false;
}
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
return false;
}
// Align the table if the entire table is selected
if ($isFullTableSelection(selection, tableNode)) {
tableNode.setFormat(formatType);
return true;
}
const [tableMap, anchorCell, focusCell] = $computeTableMap(tableNode, anchorNode, focusNode);
const maxRow = Math.max(anchorCell.startRow + anchorCell.cell.__rowSpan - 1, focusCell.startRow + focusCell.cell.__rowSpan - 1);
const maxColumn = Math.max(anchorCell.startColumn + anchorCell.cell.__colSpan - 1, focusCell.startColumn + focusCell.cell.__colSpan - 1);
const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
const minColumn = Math.min(anchorCell.startColumn, focusCell.startColumn);
const visited = new Set();
for (let i = minRow; i <= maxRow; i++) {
for (let j = minColumn; j <= maxColumn; j++) {
const cell = tableMap[i][j].cell;
if (visited.has(cell)) {
continue;
}
visited.add(cell);
cell.setFormat(formatType);
const cellChildren = cell.getChildren();
for (let k = 0; k < cellChildren.length; k++) {
const child = cellChildren[k];
if (lexical.$isElementNode(child) && !child.isInline()) {
child.setFormat(formatType);
}
}
}
}
return true;
}, 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;
}
if (typeof payload === 'string') {
const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
if (edgePosition) {
$insertParagraphAtTableEdge(edgePosition, tableNode, [lexical.$createTextNode(payload)]);
return true;
}
}
}
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 || !tableNode.is($findTableNode(tableCellNode))) {
return false;
}
stopEvent(event);
$selectAdjacentCell(tableCellNode, event.shiftKey ? 'previous' : 'next');
return true;
}, lexical.COMMAND_PRIORITY_CRITICAL));
}
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FOCUS_COMMAND, payload => {
return tableNode.isSelected();
}, lexical.COMMAND_PRIORITY_HIGH));
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, (selectionPayload, dispatchEditor) => {
if (editor !== dispatchEditor) {
return false;
}
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, focus] = anchorAndFocus;
const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
const focusCellNode = utils.$findMatchingParent(focus.getNode(), n => $isTableCellNode(n));
if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableRowNode(anchorRowNode) || !$isTableNode(gridNode)) {
return false;
}
const templateGrid = nodes[0];
const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(gridNode, anchorCellNode, focusCellNode);
const [templateGridMap] = $computeTableMapSkipCellCheck(templateGrid, null, null);
const initialRowCount = initialGridMap.length;
const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
// If we have a range selection, we'll fit the template grid into the
// table, growing the table if necessary.
let startRow = anchorCellMap.startRow;
let startCol = anchorCellMap.startColumn;
let affectedRowCount = templateGridMap.length;
let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
if (isTableSelection) {
// If we have a table selection, we'll only modify the cells within
// the selection boundary.
const selectionBoundary = $computeTableCellRectBoundary(initialGridMap, anchorCellMap, focusCellMap);
const selectionRowCount = selectionBoundary.maxRow - selectionBoundary.minRow + 1;
const selectionColCount = selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
startRow = selectionBoundary.minRow;
startCol = selectionBoundary.minColumn;
affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
affectedColCount = Math.min(affectedColCount, selectionColCount);
}
// Step 1: Unmerge all merged cells within the affected area
let didPerformMergeOperations = false;
const lastRowForUnmerge = Math.min(initialRowCount, startRow + affectedRowCount) - 1;
const lastColForUnmerge = Math.min(initialColCount, startCol + affectedColCount) - 1;
const unmergedKeys = new Set();
for (let row = startRow; row <= lastRowForUnmerge; row++) {
for (let col = startCol; col <= lastColForUnmerge; col++) {
const cellMap = initialGridMap[row][col];
if (unmergedKeys.has(cellMap.cell.getKey())) {
continue; // cell was a merged cell that was already handled
}
if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
continue; // cell is not a merged cell
}
$unmergeCellNode(cellMap.cell);
unmergedKeys.add(cellMap.cell.getKey());
didPerformMergeOperations = true;
}
}
let [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
// Step 2: Expand current table (if needed)
const rowsToInsert = affectedRowCount - initialRowCount + startRow;
for (let i = 0; i < rowsToInsert; i++) {
const cellMap = interimGridMap[initialRowCount - 1][0];
$insertTableRowAtNode(cellMap.cell);
}
const colsToInsert = affectedColCount - initialColCount + startCol;
for (let i = 0; i < colsToInsert; i++) {
const cellMap = interimGridMap[0][initialColCount - 1];
$insertTableColumnAtNode(cellMap.cell, true, false);
}
[interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
// Step 3: Merge cells and set cell content, to match template grid
for (let row = startRow; row < startRow + affectedRowCount; row++) {
for (let col = startCol; col < startCol + affectedColCount; col++) {
const templateRow = row - startRow;
const templateCol = col - startCol;
const templateCellMap = templateGridMap[templateRow][templateCol];
if (templateCellMap.startRow !== templateRow || templateCellMap.startColumn !== templateCol) {
continue; // cell is a merged cell that was already handled
}
const templateCell = templateCellMap.cell;
if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
const cellsToMerge = [];
const lastRowForMerge = Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) - 1;
const lastColForMerge = Math.min(col + templateCell.__colSpan, startCol + affectedColCount) - 1;
for (let r = row; r <= lastRowForMerge; r++) {
for (let c = col; c <= lastColForMerge; c++) {
const cellMap = interimGridMap[r][c];
cellsToMerge.push(cellMap.cell);
}
}
$mergeCells(cellsToMerge);
didPerformMergeOperations = true;
}
const {
cell
} = interimGridMap[row][col];
const originalChildren = cell.getChildren();
templateCell.getChildren().forEach(child => {
if (lexical.$isTextNode(child)) {
const paragraphNode = lexical.$createParagraphNode();
paragraphNode.append(child);
cell.append(child);
} else {
cell.append(child);
}
});
originalChildren.forEach(n => n.remove());
}
}
if (isTableSelection && didPerformMergeOperations) {
// reset the table selection in case the anchor or focus cell was
// removed via merge operations
const [finalGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
const newAnchorCellMap = finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
newAnchorCellMap.cell.selectEnd();
}
return true;
}, lexical.COMMAND_PRIORITY_CRITICAL));
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_CHANGE_COMMAND, () => {
const selection = lexical.$getSelection();
const prevSelection = lexical.$getPreviousSelection();
const nextFocus = tableObserver.getAndClearNextFocus();
if (nextFocus !== null) {
const {
focusCell
} = nextFocus;
if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
if (focusCell.x === tableObserver.focusX && focusCell.y === tableObserver.focusY) {
// The selection is already the correct table selection
return false;
} else {
tableObserver.$setFocusCellForSelection(focusCell);
return true;
}
} else if (focusCell !== tableObserver.anchorCell && $isSelectionInTable(selection, tableNode)) {
// The selection has crossed cells
tableObserver.$setFocusCellForSelection(focusCell);
return true;
}
}
const shouldCheckSelection = tableObserver.getAndClearShouldCheckSelection();
// If they pressed the down arrow with the selection outside of the
// table, and then the selection ends up in the table but not in the
// first cell, then move the selection to the first cell.
if (shouldCheckSelection && lexical.$isRangeSelection(prevSelection) && lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
const anchor = selection.anchor.getNode();
const firstRow = tableNode.getFirstChild();
const anchorCell = $findCellNode(anchor);
if (anchorCell !== null && $isTableRowNode(firstRow)) {
const firstCell = firstRow.getFirstChild();
if ($isTableCellNode(firstCell) && tableNode.is(utils.$findMatchingParent(anchorCell, node => node.is(tableNode) || node.is(firstCell)))) {
// The selection moved to the table, but not in the first cell
firstCell.selectStart();
return true;
}
}
}
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 isPartiallyWithinTable = isAnchorInside !== isFocusInside;
const isWithinTable = isAnchorInside && isFocusInside;
const isBackward = selection.isBackward();
if (isPartiallyWithinTable) {
const newSelection = selection.clone();
if (isFocusInside) {
const [tableMap] = $computeTableMap(tableNode, focusCellNode, focusCellNode);
const firstCell = tableMap[0][0].cell;
const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
newSelection.focus.set(isBackward ? firstCell.getKey() : lastCell.getKey(), isBackward ? firstCell.getChildrenSize() : lastCell.getChildrenSize(), 'element');
} else if (isAnchorInside) {
const [tableMap] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
const firstCell = tableMap[0][0].cell;
const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
/**
* If isBackward, set the anchor to be at the end of the table so that when the cursor moves outside of
* the table in the backward direction, the entire table will be selected from its end.
* Otherwise, if forward, set the anchor to be at the start of the table so that when the focus is dragged
* outside th end of the table, it will start from the beginning of the table.
*/
newSelection.anchor.set(isBackward ? lastCell.getKey() : firstCell.getKey(), isBackward ? lastCell.getChildrenSize() : 0, '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 table selection
if (!anchorCellNode.is(focusCellNode)) {
tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, anchorCellNode));
tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
}
// Handle case when the pointer type is touch and the current and
// previous selection are collapsed, and the previous anchor and current
// focus cell nodes are different, then we convert it into table selection
// However, only do this if the table observer is actively selecting (user dragging)
// to prevent unwanted selections when simply tapping between cells on mobile
if (tableObserver.pointerType === 'touch' && tableObserver.isSelecting && selection.isCollapsed() && lexical.$isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
const prevAnchorCellNode = $findCellNode(prevSelection.anchor.getNode());
if (prevAnchorCellNode && !prevAnchorCellNode.is(focusCellNode)) {
tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCellNode));
tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
tableObserver.pointerType = null;
}
}
}
} else if (selection && $isTableSelection(selection) && selection.is(prevSelection) && selection.tableKey === tableNode.getKey()) {
// if selection goes outside of the table we need to change it to Range selection
const domSelection = lexical.getDOMSelection(editorWindow);
if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
const focusNode = lexical.$getNearestNodeFromDOMNode(domSelection.focusNode);
const isFocusOutside = focusNode && !tableNode.isParentOf(focusNode);
const anchorNode = lexical.$getNearestNodeFromDOMNode(domSelection.anchorNode);
const isAnchorInside = anchorNode && tableNode.isParentOf(anchorNode);
if (isFocusOutside && isAnchorInside && domSelection.rangeCount > 0) {
const newSelection = lexical.$createRangeSelectionFromDom(domSelection, editor);
if (newSelection) {
newSelection.anchor.set(tableNode.getKey(), selection.isBackward() ? tableNode.getChildrenSize() : 0, 'element');
domSelection.removeAllRanges();
lexical.$setSelection(newSelection);
}
}
}
}
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));
tableObserver.listenersToRemove.add(editor.registerCommand(lexical.INSERT_PARAGRAPH_COMMAND, () => {
const selection = lexical.$getSelection();
if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
return false;
}
const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
if (edgePosition) {
$insertParagraphAtTableEdge(edgePosition, tableNode);
return true;
}
return false;
}, lexical.COMMAND_PRIORITY_CRITICAL));
return tableObserver;
}
function detachTableObserverFromTableElement(tableElement, tableObserver) {
if (getTableObserverFromTableElement(tableElement) === tableObserver) {
delete tableElement[LEXICAL_ELEMENT_KEY];
}
}
function attachTableObserverToTableElement(tableElement, tableObserver) {
if (!(getTableObserverFromTableElement(tableElement) === null)) {
formatDevErrorMessage(`tableElement already has an attached TableObserver`);
}
tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
}
function getTableObserverFromTableElement(tableElement) {
return tableElement[LEXICAL_ELEMENT_KEY] || null;
}
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 getDOMCellInTableFromTarget(table, node) {
if (!table.contains(node)) {
return null;
}
let cell = null;
for (let currentNode = node; currentNode != null; currentNode = currentNode.parentNode) {
if (currentNode === table) {
return cell;
}
const nodeName = currentNode.nodeName;
if (nodeName === 'TD' || nodeName === 'TH') {
// @ts-expect-error: internal field
cell = currentNode._cell || null;
}
}
return null;
}
function getTable(tableNode, dom) {
const tableElement = getTableElement(tableNode, dom);
const domRows = [];
const grid = {
columns: 0,
domRows,
rows: 0
};
let currentNode = tableElement.querySelector('tr');
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');
}
});
}
function $selectAdjacentCell(tableCellNode, direction) {
const siblingMethod = direction === 'next' ? 'getNextSibling' : 'getPreviousSibling';
const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild';
const sibling = tableCellNode[siblingMethod]();
if (lexical.$isElementNode(sibling)) {
return sibling.selectEnd();
}
const parentRow = utils.$findMatchingParent(tableCellNode, $isTableRowNode);
if (!(parentRow !== null)) {
formatDevErrorMessage(`selectAdjacentCell: Cell not in table row`);
}
for (let nextRow = parentRow[siblingMethod](); $isTableRowNode(nextRow); nextRow = nextRow[siblingMethod]()) {
const child = nextRow[childMethod]();
if (lexical.$isElementNode(child)) {
return child.selectEnd();
}
}
const parentTable = utils.$findMatchingParent(parentRow, $isTableNode);
if (!(parentTable !== null)) {
formatDevErrorMessage(`selectAdjacentCell: Row not in table`);
}
return direction === 'next' ? parentTable.selectNext() : parentTable.selectPrevious();
}
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;
}
};
function getCorner(rect, cellValue) {
let colName;
let rowName;
if (cellValue.startColumn === rect.minColumn) {
colName = 'minColumn';
} else if (cellValue.startColumn + cellValue.cell.__colSpan - 1 === rect.maxColumn) {
colName = 'maxColumn';
} else {
return null;
}
if (cellValue.startRow === rect.minRow) {
rowName = 'minRow';
} else if (cellValue.startRow + cellValue.cell.__rowSpan - 1 === rect.maxRow) {
rowName = 'maxRow';
} else {
return null;
}
return [colName, rowName];
}
function getCornerOrThrow(rect, cellValue) {
const corner = getCorner(rect, cellValue);
if (!(corner !== null)) {
formatDevErrorMessage(`getCornerOrThrow: cell ${cellValue.cell.getKey()} is not at a corner of rect`);
}
return corner;
}
function oppositeCorner([colName, rowName]) {
return [colName === 'minColumn' ? 'maxColumn' : 'minColumn', rowName === 'minRow' ? 'maxRow' : 'minRow'];
}
function cellAtCornerOrThrow(tableMap, rect, [colName, rowName]) {
const rowNum = rect[rowName];
const rowMap = tableMap[rowNum];
if (!(rowMap !== undefined)) {
formatDevErrorMessage(`cellAtCornerOrThrow: ${rowName} = ${String(rowNum)} missing in tableMap`);
}
const colNum = rect[colName];
const cell = rowMap[colNum];
if (!(cell !== undefined)) {
formatDevErrorMessage(`cellAtCornerOrThrow: ${colName} = ${String(colNum)} missing in tableMap`);
}
return cell;
}
function $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue) {
// We are sure that the focus now either contracts or expands the rect
// but both the anchor and focus might be moved to ensure a rectangle
// given a potentially ragged merge shape
const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, newFocusCellValue);
const anchorCorner = getCorner(rect, anchorCellValue);
if (anchorCorner) {
return [cellAtCornerOrThrow(tableMap, rect, anchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner))];
}
const newFocusCorner = getCorner(rect, newFocusCellValue);
if (newFocusCorner) {
return [cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)), cellAtCornerOrThrow(tableMap, rect, newFocusCorner)];
}
// TODO this doesn't have to be arbitrary, use the closest corner instead
const newAnchorCorner = ['minColumn', 'minRow'];
return [cellAtCornerOrThrow(tableMap, rect, newAnchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner))];
}
function $adjustFocusInDirection(tableObserver, tableMap, anchorCellValue, focusCellValue, direction) {
const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, focusCellValue);
const spans = $computeTableCellRectSpans(tableMap, rect);
const {
topSpan,
leftSpan,
bottomSpan,
rightSpan
} = spans;
const anchorCorner = getCornerOrThrow(rect, anchorCellValue);
const [focusColumn, focusRow] = oppositeCorner(anchorCorner);
let fCol = rect[focusColumn];
let fRow = rect[focusRow];
if (direction === 'forward') {
fCol += focusColumn === 'maxColumn' ? 1 : leftSpan;
} else if (direction === 'backward') {
fCol -= focusColumn === 'minColumn' ? 1 : rightSpan;
} else if (direction === 'down') {
fRow += focusRow === 'maxRow' ? 1 : topSpan;
} else if (direction === 'up') {
fRow -= focusRow === 'minRow' ? 1 : bottomSpan;
}
const targetRowMap = tableMap[fRow];
if (targetRowMap === undefined) {
return false;
}
const newFocusCellValue = targetRowMap[fCol];
if (newFocusCellValue === undefined) {
return false;
}
// We can be certain that anchorCellValue and newFocusCellValue are
// contained within the desired selection, but we are not certain if
// they need to be expanded or not to maintain a rectangular shape
const [finalAnchorCell, finalFocusCell] = $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue);
const anchorDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalAnchorCell.cell);
const focusDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalFocusCell.cell);
tableObserver.$setAnchorCellForSelection(anchorDOM);
tableObserver.$setFocusCellForSelection(focusDOM, true);
return true;
}
function $isSelectionInTable(selection, tableNode) {
if (lexical.$isRangeSelection(selection) || $isTableSelection(selection)) {
// TODO this should probably return false if there's an unrelated
// shadow root between the node and the table (e.g. another table,
// collapsible, etc.)
const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
return isAnchorInside && isFocusInside;
}
return false;
}
function $isFullTableSelection(selection, tableNode) {
if ($isTableSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (tableNode && anchorNode && focusNode) {
const [map] = $computeTableMap(tableNode, anchorNode, focusNode);
return anchorNode.getKey() === map[0][0].cell.getKey() && focusNode.getKey() === map[map.length - 1].at(-1).cell.getKey();
}
}
return false;
}
function selectTableCellNode(tableCell, fromStart) {
if (fromStart) {
tableCell.selectStart();
} else {
tableCell.selectEnd();
}
}
function $addHighlightToDOM(editor, cell) {
const element = cell.elem;
const editorThemeClasses = editor._config.theme;
const node = lexical.$getNearestNodeFromDOMNode(element);
if (!$isTableCellNode(node)) {
formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
}
utils.addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
}
function $removeHighlightFromDOM(editor, cell) {
const element = cell.elem;
const node = lexical.$getNearestNodeFromDOMNode(element);
if (!$isTableCellNode(node)) {
formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
}
const editorThemeClasses = editor._config.theme;
utils.removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
}
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 $getBlockParentIfFirstNode(node) {
for (let prevNode = node, currentNode = node; currentNode !== null; prevNode = currentNode, currentNode = currentNode.getParent()) {
if (lexical.$isElementNode(currentNode)) {
if (currentNode !== prevNode && currentNode.getFirstChild() !== prevNode) {
// Not the first child or the initial node
return null;
} else if (!currentNode.isInline()) {
return currentNode;
}
}
}
return null;
}
function $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, isBackward, tableNode, tableObserver) {
const initialFocus = lexical.$caretFromPoint(selection.focus, isBackward ? 'previous' : 'next');
if (lexical.$isExtendableTextPointCaret(initialFocus)) {
return false;
}
let lastCaret = initialFocus;
// TableCellNode is the only shadow root we are interested in piercing so
// we find the last internal caret and then check its parent
for (const nextCaret of lexical.$extendCaretToRange(initialFocus).iterNodeCarets('shadowRoot')) {
if (!(lexical.$isSiblingCaret(nextCaret) && lexical.$isElementNode(nextCaret.origin))) {
return false;
}
lastCaret = nextCaret;
}
const lastCaretParent = lastCaret.getParentAtCaret();
if (!$isTableCellNode(lastCaretParent)) {
return false;
}
const anchorCell = lastCaretParent;
const focusCaret = $findNextTableCell(lexical.$getSiblingCaret(anchorCell, lastCaret.direction));
const anchorCellTable = utils.$findMatchingParent(anchorCell, $isTableNode);
if (!(anchorCellTable && anchorCellTable.is(tableNode))) {
return false;
}
const anchorCellDOM = editor.getElementByKey(anchorCell.getKey());
const anchorDOMCell = getDOMCellFromTarget(anchorCellDOM);
if (!anchorCellDOM || !anchorDOMCell) {
return false;
}
const anchorCellTableElement = $getElementForTableNode(editor, anchorCellTable);
tableObserver.table = anchorCellTableElement;
if (!focusCaret) {
if (alter === 'extend') {
// extend the selection from a range inside the cell to a table selection of the cell
tableObserver.$setAnchorCellForSelection(anchorDOMCell);
tableObserver.$setFocusCellForSelection(anchorDOMCell, true);
} else {
// exit the table
const outerFocusCaret = $getTableExitCaret(lexical.$getSiblingCaret(anchorCellTable, initialFocus.direction));
lexical.$setPointFromCaret(selection.anchor, outerFocusCaret);
lexical.$setPointFromCaret(selection.focus, outerFocusCaret);
}
} else if (alter === 'extend') {
const focusDOMCell = getDOMCellFromTarget(editor.getElementByKey(focusCaret.origin.getKey()));
if (!focusDOMCell) {
return false;
}
tableObserver.$setAnchorCellForSelection(anchorDOMCell);
tableObserver.$setFocusCellForSelection(focusDOMCell, true);
} else {
// alter === 'move'
const innerFocusCaret = lexical.$normalizeCaret(focusCaret);
lexical.$setPointFromCaret(selection.anchor, innerFocusCaret);
lexical.$setPointFromCaret(selection.focus, innerFocusCaret);
}
stopEvent(event);
return true;
}
function $getTableExitCaret(initialCaret) {
const adjacent = lexical.$getAdjacentChildCaret(initialCaret);
return lexical.$isChildCaret(adjacent) ? lexical.$normalizeCaret(adjacent) : initialCaret;
}
function $findNextTableCell(initialCaret) {
for (const nextCaret of lexical.$extendCaretToRange(initialCaret).iterNodeCarets('root')) {
const {
origin
} = nextCaret;
if ($isTableCellNode(origin)) {
// not sure why ts isn't narrowing here (even if the guard is on nextCaret.origin)
// but returning a new caret is fine
if (lexical.$isChildCaret(nextCaret)) {
return lexical.$getChildCaret(origin, initialCaret.direction);
}
} else if (!$isTableRowNode(origin)) {
break;
}
}
return null;
}
function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
if ((direction === 'up' || direction === 'down') && isTypeaheadMenuInView(editor)) {
return false;
}
const selection = lexical.$getSelection();
if (!$isSelectionInTable(selection, tableNode)) {
if (lexical.$isRangeSelection(selection)) {
if (direction === 'backward') {
if (selection.focus.offset > 0) {
return false;
}
const parentNode = $getBlockParentIfFirstNode(selection.focus.getNode());
if (!parentNode) {
return false;
}
const siblingNode = parentNode.getPreviousSibling();
if (!$isTableNode(siblingNode)) {
return false;
}
stopEvent(event);
if (event.shiftKey) {
selection.focus.set(siblingNode.getParentOrThrow().getKey(), siblingNode.getIndexWithinParent(), 'element');
} else {
siblingNode.selectEnd();
}
return true;
} else if (event.shiftKey && (direction === 'up' || direction === 'down')) {
const focusNode = selection.focus.getNode();
const isTableUnselect = !selection.isCollapsed() && (direction === 'up' && !selection.isBackward() || direction === 'down' && selection.isBackward());
if (isTableUnselect) {
let focusParentNode = utils.$findMatchingParent(focusNode, n => $isTableNode(n));
if ($isTableCellNode(focusParentNode)) {
focusParentNode = utils.$findMatchingParent(focusParentNode, $isTableNode);
}
if (focusParentNode !== tableNode) {
return false;
}
if (!focusParentNode) {
return false;
}
const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
if (!sibling) {
return false;
}
let newOffset = 0;
if (direction === 'up') {
if (lexical.$isElementNode(sibling)) {
newOffset = sibling.getChildrenSize();
}
}
let newFocusNode = sibling;
if (direction === 'up') {
if (lexical.$isElementNode(sibling)) {
const lastCell = sibling.getLastChild();
newFocusNode = lastCell ? lastCell : sibling;
newOffset = lexical.$isTextNode(newFocusNode) ? newFocusNode.getTextContentSize() : 0;
}
}
const newSelection = selection.clone();
newSelection.focus.set(newFocusNode.getKey(), newOffset, lexical.$isTextNode(newFocusNode) ? 'text' : 'element');
lexical.$setSelection(newSelection);
stopEvent(event);
return true;
} else if (lexical.$isRootOrShadowRoot(focusNode)) {
const selectedNode = direction === 'up' ? selection.getNodes()[selection.getNodes().length - 1] : selection.getNodes()[0];
if (selectedNode) {
const tableCellNode = $findParentTableCellNodeInTable(tableNode, selectedNode);
if (tableCellNode !== null) {
const firstDescendant = tableNode.getFirstDescendant();
const lastDescendant = tableNode.getLastDescendant();
if (!firstDescendant || !lastDescendant) {
return false;
}
const [firstCellNode] = $getNodeTriplet(firstDescendant);
const [lastCellNode] = $getNodeTriplet(lastDescendant);
const firstCellCoords = tableNode.getCordsFromCellNode(firstCellNode, tableObserver.table);
const lastCellCoords = tableNode.getCordsFromCellNode(lastCellNode, tableObserver.table);
const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(firstCellCoords.x, firstCellCoords.y, tableObserver.table);
const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(lastCellCoords.x, lastCellCoords.y, tableObserver.table);
tableObserver.$setAnchorCellForSelection(firstCellDOM);
tableObserver.$setFocusCellForSelection(lastCellDOM, true);
return true;
}
}
return false;
} else {
let focusParentNode = utils.$findMatchingParent(focusNode, n => lexical.$isElementNode(n) && !n.isInline());
if ($isTableCellNode(focusParentNode)) {
focusParentNode = utils.$findMatchingParent(focusParentNode, $isTableNode);
}
if (!focusParentNode) {
return false;
}
const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
if ($isTableNode(sibling) && tableObserver.tableNodeKey === sibling.getKey()) {
const firstDescendant = sibling.getFirstDescendant();
const lastDescendant = sibling.getLastDescendant();
if (!firstDescendant || !lastDescendant) {
return false;
}
const [firstCellNode] = $getNodeTriplet(firstDescendant);
const [lastCellNode] = $getNodeTriplet(lastDescendant);
const newSelection = selection.clone();
newSelection.focus.set((direction === 'up' ? firstCellNode : lastCellNode).getKey(), direction === 'up' ? 0 : lastCellNode.getChildrenSize(), 'element');
stopEvent(event);
lexical.$setSelection(newSelection);
return true;
}
}
}
}
if (direction === 'down' && $isScrollableTablesActive(editor)) {
// Enable Firefox workaround
tableObserver.setShouldCheckSelection();
}
return false;
}
if (lexical.$isRangeSelection(selection)) {
if (direction === 'backward' || direction === 'forward') {
const alter = event.shiftKey ? 'extend' : 'move';
return $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, direction === 'backward', tableNode, tableObserver);
}
if (selection.isCollapsed()) {
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 = getTableElement(anchorCellTable, editor.getElementByKey(anchorCellTable.getKey()));
if (anchorCellTableElement != null) {
tableObserver.table = getTable(anchorCellTable, 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 = lexical.getDOMSelection(getEditorWindow(editor));
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();
if (!$isTableNode(tableNodeFromSelection)) {
formatDevErrorMessage(`$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode`);
}
const tableElement = getTableElement(tableNodeFromSelection, editor.getElementByKey(tableNodeFromSelection.getKey()));
if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
return false;
}
tableObserver.$updateTableTableSelection(selection);
const grid = getTable(tableNodeFromSelection, 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 [tableMap, anchorValue, focusValue] = $computeTableMap(tableNode, anchorCellNode, focusCellNode);
return $adjustFocusInDirection(tableObserver, tableMap, anchorValue, focusValue, direction);
} else {
focusCellNode.selectEnd();
}
return true;
}
return false;
}
function stopEvent(event) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
}
function isTypeaheadMenuInView(editor) {
// There is no inbuilt way to check if the component picker is in view
// but we can check if the root DOM element has the aria-controls attribute "typeahead-menu".
const root = editor.getRootElement();
if (!root) {
return false;
}
return root.hasAttribute('aria-controls') && root.getAttribute('aria-controls') === 'typeahead-menu';
}
function $insertParagraphAtTableEdge(edgePosition, tableNode, children) {
const paragraphNode = lexical.$createParagraphNode();
if (edgePosition === 'first') {
tableNode.insertBefore(paragraphNode);
} else {
tableNode.insertAfter(paragraphNode);
}
paragraphNode.append(...(children || []));
paragraphNode.selectEnd();
}
function $getTableEdgeCursorPosition(editor, selection, tableNode) {
const tableNodeParent = tableNode.getParent();
if (!tableNodeParent) {
return undefined;
}
// TODO: Add support for nested tables
const domSelection = lexical.getDOMSelection(getEditorWindow(editor));
if (!domSelection) {
return undefined;
}
const domAnchorNode = domSelection.anchorNode;
const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNode.getKey()));
// We are only interested in the scenario where the
// native selection anchor is:
// - at or inside the table's parent DOM
// - and NOT at or inside the table DOM
// It may be adjacent to the table DOM (e.g. in a wrapper)
if (!domAnchorNode || !tableNodeParentDOM || !tableElement || !tableNodeParentDOM.contains(domAnchorNode) || tableElement.contains(domAnchorNode)) {
return undefined;
}
const anchorCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
if (!anchorCellNode) {
return undefined;
}
const parentTable = utils.$findMatchingParent(anchorCellNode, n => $isTableNode(n));
if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {
return undefined;
}
const [tableMap, cellValue] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
const firstCell = tableMap[0][0];
const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
const {
startRow,
startColumn
} = cellValue;
const isAtFirstCell = startRow === firstCell.startRow && startColumn === firstCell.startColumn;
const isAtLastCell = startRow === lastCell.startRow && startColumn === lastCell.startColumn;
if (isAtFirstCell) {
return 'first';
} else if (isAtLastCell) {
return 'last';
} else {
return undefined;
}
}
function $getObserverCellFromCellNodeOrThrow(tableObserver, tableCellNode) {
const {
tableNode
} = tableObserver.$lookup();
const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
}
function $getNearestTableCellInTableFromDOMNode(tableNode, startingDOM, editorState) {
return $findParentTableCellNodeInTable(tableNode, lexical.$getNearestNodeFromDOMNode(startingDOM, editorState));
}
function isHTMLDivElement(element) {
return utils.isHTMLElement(element) && element.nodeName === 'DIV';
}
function updateColgroup(dom, config, colCount, colWidths) {
const colGroup = dom.querySelector('colgroup');
if (!colGroup) {
return;
}
const cols = [];
for (let i = 0; i < colCount; i++) {
const col = document.createElement('col');
const width = colWidths && colWidths[i];
if (width) {
col.style.width = `${width}px`;
}
cols.push(col);
}
colGroup.replaceChildren(...cols);
}
function setRowStriping(dom, config, rowStriping) {
if (rowStriping) {
utils.addClassNamesToElement(dom, config.theme.tableRowStriping);
dom.setAttribute('data-lexical-row-striping', 'true');
} else {
utils.removeClassNamesFromElement(dom, config.theme.tableRowStriping);
dom.removeAttribute('data-lexical-row-striping');
}
}
function setFrozenColumns(dom, tableElement, config, frozenColumnCount) {
if (frozenColumnCount > 0) {
utils.addClassNamesToElement(dom, config.theme.tableFrozenColumn);
tableElement.setAttribute('data-lexical-frozen-column', 'true');
} else {
utils.removeClassNamesFromElement(dom, config.theme.tableFrozenColumn);
tableElement.removeAttribute('data-lexical-frozen-column');
}
}
function setFrozenRows(dom, tableElement, config, frozenRowCount) {
if (frozenRowCount > 0) {
utils.addClassNamesToElement(dom, config.theme.tableFrozenRow);
tableElement.setAttribute('data-lexical-frozen-row', 'true');
} else {
utils.removeClassNamesFromElement(dom, config.theme.tableFrozenRow);
tableElement.removeAttribute('data-lexical-frozen-row');
}
}
function alignTableElement(dom, config, formatType) {
if (!config.theme.tableAlignment) {
return;
}
const removeClasses = [];
const addClasses = [];
for (const format of ['center', 'right']) {
const classes = config.theme.tableAlignment[format];
if (!classes) {
continue;
}
(format === formatType ? addClasses : removeClasses).push(classes);
}
utils.removeClassNamesFromElement(dom, ...removeClasses);
utils.addClassNamesToElement(dom, ...addClasses);
}
const scrollableEditors = new WeakSet();
function $isScrollableTablesActive(editor = lexical.$getEditor()) {
return scrollableEditors.has(editor);
}
function setScrollableTablesActive(editor, active) {
if (active) {
if (!editor._config.theme.tableScrollableWrapper) {
console.warn('TableNode: hasHorizontalScroll is active but theme.tableScrollableWrapper is not defined.');
}
scrollableEditors.add(editor);
} else {
scrollableEditors.delete(editor);
}
}
/** @noInheritDoc */
class TableNode extends lexical.ElementNode {
/** @internal */
static getType() {
return 'table';
}
getColWidths() {
const self = this.getLatest();
return self.__colWidths;
}
setColWidths(colWidths) {
const self = this.getWritable();
// NOTE: Node properties should be immutable. Freeze to prevent accidental mutation.
self.__colWidths = colWidths !== undefined && true ? Object.freeze(colWidths) : colWidths;
return self;
}
static clone(node) {
return new TableNode(node.__key);
}
afterCloneFrom(prevNode) {
super.afterCloneFrom(prevNode);
this.__colWidths = prevNode.__colWidths;
this.__rowStriping = prevNode.__rowStriping;
this.__frozenColumnCount = prevNode.__frozenColumnCount;
this.__frozenRowCount = prevNode.__frozenRowCount;
}
static importDOM() {
return {
table: _node => ({
conversion: $convertTableElement,
priority: 1
})
};
}
static importJSON(serializedNode) {
return $createTableNode().updateFromJSON(serializedNode);
}
updateFromJSON(serializedNode) {
return super.updateFromJSON(serializedNode).setRowStriping(serializedNode.rowStriping || false).setFrozenColumns(serializedNode.frozenColumnCount || 0).setFrozenRows(serializedNode.frozenRowCount || 0).setColWidths(serializedNode.colWidths);
}
constructor(key) {
super(key);
this.__rowStriping = false;
this.__frozenColumnCount = 0;
this.__frozenRowCount = 0;
}
exportJSON() {
return {
...super.exportJSON(),
colWidths: this.getColWidths(),
frozenColumnCount: this.__frozenColumnCount ? this.__frozenColumnCount : undefined,
frozenRowCount: this.__frozenRowCount ? this.__frozenRowCount : undefined,
rowStriping: this.__rowStriping ? this.__rowStriping : undefined
};
}
extractWithChild(child, selection, destination) {
return destination === 'html';
}
getDOMSlot(element) {
const tableElement = !isHTMLTableElement(element) ? element.querySelector('table') : element;
if (!isHTMLTableElement(tableElement)) {
formatDevErrorMessage(`TableNode.getDOMSlot: createDOM() did not return a table`);
}
return super.getDOMSlot(element).withElement(tableElement).withAfter(tableElement.querySelector('colgroup'));
}
createDOM(config, editor) {
const tableElement = document.createElement('table');
if (this.__style) {
tableElement.style.cssText = this.__style;
}
const colGroup = document.createElement('colgroup');
tableElement.appendChild(colGroup);
lexical.setDOMUnmanaged(colGroup);
utils.addClassNamesToElement(tableElement, config.theme.table);
this.updateTableElement(null, tableElement, config);
if ($isScrollableTablesActive(editor)) {
const wrapperElement = document.createElement('div');
const classes = config.theme.tableScrollableWrapper;
if (classes) {
utils.addClassNamesToElement(wrapperElement, classes);
} else {
wrapperElement.style.cssText = 'overflow-x: auto;';
}
wrapperElement.appendChild(tableElement);
this.updateTableWrapper(null, wrapperElement, tableElement, config);
return wrapperElement;
}
return tableElement;
}
updateTableWrapper(prevNode, tableWrapper, tableElement, config) {
if (this.__frozenColumnCount !== (prevNode ? prevNode.__frozenColumnCount : 0)) {
setFrozenColumns(tableWrapper, tableElement, config, this.__frozenColumnCount);
}
if (this.__frozenRowCount !== (prevNode ? prevNode.__frozenRowCount : 0)) {
setFrozenRows(tableWrapper, tableElement, config, this.__frozenRowCount);
}
}
updateTableElement(prevNode, tableElement, config) {
if (this.__style !== (prevNode ? prevNode.__style : '')) {
tableElement.style.cssText = this.__style;
}
if (this.__rowStriping !== (prevNode ? prevNode.__rowStriping : false)) {
setRowStriping(tableElement, config, this.__rowStriping);
}
updateColgroup(tableElement, config, this.getColumnCount(), this.getColWidths());
alignTableElement(tableElement, config, this.getFormatType());
}
updateDOM(prevNode, dom, config) {
const slot = this.getDOMSlot(dom);
const tableElement = slot.element;
if (dom === tableElement === $isScrollableTablesActive()) {
return true;
}
if (isHTMLDivElement(dom)) {
this.updateTableWrapper(prevNode, dom, tableElement, config);
}
this.updateTableElement(prevNode, tableElement, config);
return false;
}
exportDOM(editor) {
const superExport = super.exportDOM(editor);
const {
element
} = superExport;
return {
after: tableElement => {
if (superExport.after) {
tableElement = superExport.after(tableElement);
}
if (!isHTMLTableElement(tableElement) && utils.isHTMLElement(tableElement)) {
tableElement = tableElement.querySelector('table');
}
if (!isHTMLTableElement(tableElement)) {
return null;
}
alignTableElement(tableElement, editor._config, this.getFormatType());
// Scan the table map to build a map of table cell key to the columns it needs
const [tableMap] = $computeTableMapSkipCellCheck(this, null, null);
const cellValues = new Map();
for (const mapRow of tableMap) {
for (const mapValue of mapRow) {
const key = mapValue.cell.getKey();
if (!cellValues.has(key)) {
cellValues.set(key, {
colSpan: mapValue.cell.getColSpan(),
startColumn: mapValue.startColumn
});
}
}
}
// scan the DOM to find the table cell keys that were used and mark those columns
const knownColumns = new Set();
for (const cellDOM of tableElement.querySelectorAll(':scope > tr > [data-temporary-table-cell-lexical-key]')) {
const key = cellDOM.getAttribute('data-temporary-table-cell-lexical-key');
if (key) {
const cellSpan = cellValues.get(key);
cellDOM.removeAttribute('data-temporary-table-cell-lexical-key');
if (cellSpan) {
cellValues.delete(key);
for (let i = 0; i < cellSpan.colSpan; i++) {
knownColumns.add(i + cellSpan.startColumn);
}
}
}
}
// Compute the colgroup and columns in the export
const colGroup = tableElement.querySelector(':scope > colgroup');
if (colGroup) {
// Only include the <col /> for rows that are in the output
const cols = Array.from(tableElement.querySelectorAll(':scope > colgroup > col')).filter((dom, i) => knownColumns.has(i));
colGroup.replaceChildren(...cols);
}
// Wrap direct descendant rows in a tbody for export
const rows = tableElement.querySelectorAll(':scope > tr');
if (rows.length > 0) {
const tBody = document.createElement('tbody');
for (const row of rows) {
tBody.appendChild(row);
}
tableElement.append(tBody);
}
return tableElement;
},
element: !isHTMLTableElement(element) && utils.isHTMLElement(element) ? element.querySelector('table') : element
};
}
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;
}
for (let x = 0; x < row.length; x++) {
const cell = row[x];
if (cell == null) {
continue;
}
const {
elem
} = cell;
const cellNode = $getNearestTableCellInTableFromDOMNode(this, elem);
if (cellNode !== null && tableCellNode.is(cellNode)) {
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 index = x < row.length ? x : row.length - 1;
const cell = row[index];
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;
}
getRowStriping() {
return Boolean(this.getLatest().__rowStriping);
}
setRowStriping(newRowStriping) {
const self = this.getWritable();
self.__rowStriping = newRowStriping;
return self;
}
setFrozenColumns(columnCount) {
const self = this.getWritable();
self.__frozenColumnCount = columnCount;
return self;
}
getFrozenColumns() {
return this.getLatest().__frozenColumnCount;
}
setFrozenRows(rowCount) {
const self = this.getWritable();
self.__frozenRowCount = rowCount;
return self;
}
getFrozenRows() {
return this.getLatest().__frozenRowCount;
}
canSelectBefore() {
return true;
}
canIndent() {
return false;
}
getColumnCount() {
const firstRow = this.getFirstChild();
if (!firstRow) {
return 0;
}
let columnCount = 0;
firstRow.getChildren().forEach(cell => {
if ($isTableCellNode(cell)) {
columnCount += cell.getColSpan();
}
});
return columnCount;
}
}
function $getElementForTableNode(editor, tableNode) {
const tableElement = editor.getElementByKey(tableNode.getKey());
if (!(tableElement !== null)) {
formatDevErrorMessage(`$getElementForTableNode: Table Element Not Found`);
}
return getTable(tableNode, tableElement);
}
function $convertTableElement(domNode) {
const tableNode = $createTableNode();
if (domNode.hasAttribute('data-lexical-row-striping')) {
tableNode.setRowStriping(true);
}
if (domNode.hasAttribute('data-lexical-frozen-column')) {
tableNode.setFrozenColumns(1);
}
if (domNode.hasAttribute('data-lexical-frozen-row')) {
tableNode.setFrozenRows(1);
}
const colGroup = domNode.querySelector(':scope > colgroup');
if (colGroup) {
let columns = [];
for (const col of colGroup.querySelectorAll(':scope > col')) {
let width = col.style.width || '';
if (!PIXEL_VALUE_REG_EXP.test(width)) {
// Also support deprecated width attribute for google docs
width = col.getAttribute('width') || '';
if (!/^\d+$/.test(width)) {
columns = undefined;
break;
}
}
columns.push(parseFloat(width));
}
if (columns) {
tableNode.setColWidths(columns);
}
}
return {
after: children => utils.$descendantsMatching(children, $isTableRowNode),
node: tableNode
};
}
function $createTableNode() {
return lexical.$applyNodeReplacement(new TableNode());
}
function $isTableNode(node) {
return node instanceof TableNode;
}
function $insertTableCommandListener({
rows,
columns,
includeHeaders
}) {
const selection = lexical.$getSelection() || lexical.$getPreviousSelection();
if (!selection || !lexical.$isRangeSelection(selection)) {
return false;
}
// Prevent nested tables by checking if we're already inside a table
if ($findTableNode(selection.anchor.getNode())) {
return false;
}
const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders);
utils.$insertNodeToNearestRoot(tableNode);
const firstDescendant = tableNode.getFirstDescendant();
if (lexical.$isTextNode(firstDescendant)) {
firstDescendant.select();
}
return true;
}
function $tableCellTransform(node) {
if (!$isTableRowNode(node.getParent())) {
// TableCellNode must be a child of TableRowNode.
node.remove();
} else if (node.isEmpty()) {
// TableCellNode should never be empty
node.append(lexical.$createParagraphNode());
}
}
function $tableRowTransform(node) {
if (!$isTableNode(node.getParent())) {
// TableRowNode must be a child of TableNode.
// TODO: Future support of tbody/thead/tfoot may change this
node.remove();
} else {
utils.$unwrapAndFilterDescendants(node, $isTableCellNode);
}
}
function $tableTransform(node) {
// TableRowNode is the only valid child for TableNode
// TODO: Future support of tbody/thead/tfoot/caption may change this
utils.$unwrapAndFilterDescendants(node, $isTableRowNode);
const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
const maxRowLength = gridMap.reduce((curLength, row) => {
return Math.max(curLength, row.length);
}, 0);
const rowNodes = node.getChildren();
for (let i = 0; i < gridMap.length; ++i) {
const rowNode = rowNodes[i];
if (!rowNode) {
continue;
}
if (!$isTableRowNode(rowNode)) {
formatDevErrorMessage(`TablePlugin: Expecting all children of TableNode to be TableRowNode, found ${rowNode.constructor.name} (type ${rowNode.getType()})`);
}
const rowLength = gridMap[i].reduce((acc, cell) => cell ? 1 + acc : acc, 0);
if (rowLength === maxRowLength) {
continue;
}
for (let j = rowLength; j < maxRowLength; ++j) {
// TODO: inherit header state from another header or body
const newCell = $createTableCellNode();
newCell.append(lexical.$createParagraphNode());
rowNode.append(newCell);
}
}
}
function $tableClickCommand(event) {
if (event.detail < 3 || !lexical.isDOMNode(event.target)) {
return false;
}
const startNode = lexical.$getNearestNodeFromDOMNode(event.target);
if (startNode === null) {
return false;
}
const blockNode = utils.$findMatchingParent(startNode, node => lexical.$isElementNode(node) && !node.isInline());
if (blockNode === null) {
return false;
}
const rootNode = blockNode.getParent();
if (!$isTableCellNode(rootNode)) {
return false;
}
blockNode.select(0);
return true;
}
/**
* Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
* This should only be registered when you do not want to support merged cells.
*
* @param editor The editor
* @returns An unregister callback
*/
function registerTableCellUnmergeTransform(editor) {
return editor.registerNodeTransform(TableCellNode, node => {
if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
// When we have rowSpan we have to map the entire Table to understand where the new Cells
// fit best; let's analyze all Cells at once to save us from further transform iterations
const [,, gridNode] = $getNodeTriplet(node);
const [gridMap] = $computeTableMap(gridNode, node, node);
// TODO this function expects Tables to be normalized. Look into this once it exists
const rowsCount = gridMap.length;
const columnsCount = gridMap[0].length;
let row = gridNode.getFirstChild();
if (!$isTableRowNode(row)) {
formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
}
const unmerged = [];
for (let i = 0; i < rowsCount; i++) {
if (i !== 0) {
row = row.getNextSibling();
if (!$isTableRowNode(row)) {
formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
}
}
let lastRowCell = null;
for (let j = 0; j < columnsCount; j++) {
const cellMap = gridMap[i][j];
const cell = cellMap.cell;
if (cellMap.startRow === i && cellMap.startColumn === j) {
lastRowCell = cell;
unmerged.push(cell);
} else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
if (!$isTableCellNode(cell)) {
formatDevErrorMessage(`Expected TableNode cell to be a TableCellNode`);
}
const newCell = $createTableCellNode(cell.__headerState);
if (lastRowCell !== null) {
lastRowCell.insertAfter(newCell);
} else {
utils.$insertFirst(row, newCell);
}
}
}
}
for (const cell of unmerged) {
cell.setColSpan(1);
cell.setRowSpan(1);
}
}
});
}
function registerTableSelectionObserver(editor, hasTabHandler = true) {
const tableSelections = new Map();
const initializeTableNode = (tableNode, nodeKey, dom) => {
const tableElement = getTableElement(tableNode, dom);
const tableSelection = applyTableHandlers(tableNode, tableElement, editor, hasTabHandler);
tableSelections.set(nodeKey, [tableSelection, tableElement]);
};
const unregisterMutationListener = editor.registerMutationListener(TableNode, nodeMutations => {
editor.getEditorState().read(() => {
for (const [nodeKey, mutation] of nodeMutations) {
const tableSelection = tableSelections.get(nodeKey);
if (mutation === 'created' || mutation === 'updated') {
const {
tableNode,
tableElement
} = $getTableAndElementByKey(nodeKey);
if (tableSelection === undefined) {
initializeTableNode(tableNode, nodeKey, tableElement);
} else if (tableElement !== tableSelection[1]) {
// The update created a new DOM node, destroy the existing TableObserver
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
initializeTableNode(tableNode, nodeKey, tableElement);
}
} else if (mutation === 'destroyed') {
if (tableSelection !== undefined) {
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
}
}
}
}, {
editor
});
}, {
skipInitialization: false
});
return () => {
unregisterMutationListener();
// Hook might be called multiple times so cleaning up tables listeners as well,
// as it'll be reinitialized during recurring call
for (const [, [tableSelection]] of tableSelections) {
tableSelection.removeListeners();
}
};
}
/**
* Register the INSERT_TABLE_COMMAND listener and the table integrity transforms. The
* table selection observer should be registered separately after this with
* {@link registerTableSelectionObserver}.
*
* @param editor The editor
* @returns An unregister callback
*/
function registerTablePlugin(editor) {
if (!editor.hasNodes([TableNode])) {
{
formatDevErrorMessage(`TablePlugin: TableNode is not registered on editor`);
}
}
return utils.mergeRegister(editor.registerCommand(INSERT_TABLE_COMMAND, $insertTableCommandListener, lexical.COMMAND_PRIORITY_EDITOR), editor.registerCommand(lexical.SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ({
nodes,
selection
}, dispatchEditor) => {
if (editor !== dispatchEditor || !lexical.$isRangeSelection(selection)) {
return false;
}
const isInsideTableCell = $findTableNode(selection.anchor.getNode()) !== null;
return isInsideTableCell && nodes.some($isTableNode);
}, lexical.COMMAND_PRIORITY_EDITOR), editor.registerCommand(lexical.CLICK_COMMAND, $tableClickCommand, lexical.COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(TableNode, $tableTransform), editor.registerNodeTransform(TableRowNode, $tableRowTransform), editor.registerNodeTransform(TableCellNode, $tableCellTransform));
}
exports.$computeTableMap = $computeTableMap;
exports.$computeTableMapSkipCellCheck = $computeTableMapSkipCellCheck;
exports.$createTableCellNode = $createTableCellNode;
exports.$createTableNode = $createTableNode;
exports.$createTableNodeWithDimensions = $createTableNodeWithDimensions;
exports.$createTableRowNode = $createTableRowNode;
exports.$createTableSelection = $createTableSelection;
exports.$createTableSelectionFrom = $createTableSelectionFrom;
exports.$deleteTableColumn = $deleteTableColumn;
exports.$deleteTableColumnAtSelection = $deleteTableColumnAtSelection;
exports.$deleteTableColumn__EXPERIMENTAL = $deleteTableColumn__EXPERIMENTAL;
exports.$deleteTableRowAtSelection = $deleteTableRowAtSelection;
exports.$deleteTableRow__EXPERIMENTAL = $deleteTableRow__EXPERIMENTAL;
exports.$findCellNode = $findCellNode;
exports.$findTableNode = $findTableNode;
exports.$getElementForTableNode = $getElementForTableNode;
exports.$getNodeTriplet = $getNodeTriplet;
exports.$getTableAndElementByKey = $getTableAndElementByKey;
exports.$getTableCellNodeFromLexicalNode = $getTableCellNodeFromLexicalNode;
exports.$getTableCellNodeRect = $getTableCellNodeRect;
exports.$getTableColumnIndexFromTableCellNode = $getTableColumnIndexFromTableCellNode;
exports.$getTableNodeFromLexicalNodeOrThrow = $getTableNodeFromLexicalNodeOrThrow;
exports.$getTableRowIndexFromTableCellNode = $getTableRowIndexFromTableCellNode;
exports.$getTableRowNodeFromTableCellNodeOrThrow = $getTableRowNodeFromTableCellNodeOrThrow;
exports.$insertTableColumn = $insertTableColumn;
exports.$insertTableColumnAtSelection = $insertTableColumnAtSelection;
exports.$insertTableColumn__EXPERIMENTAL = $insertTableColumn__EXPERIMENTAL;
exports.$insertTableRow = $insertTableRow;
exports.$insertTableRowAtSelection = $insertTableRowAtSelection;
exports.$insertTableRow__EXPERIMENTAL = $insertTableRow__EXPERIMENTAL;
exports.$isScrollableTablesActive = $isScrollableTablesActive;
exports.$isTableCellNode = $isTableCellNode;
exports.$isTableNode = $isTableNode;
exports.$isTableRowNode = $isTableRowNode;
exports.$isTableSelection = $isTableSelection;
exports.$mergeCells = $mergeCells;
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.getTableElement = getTableElement;
exports.getTableObserverFromTableElement = getTableObserverFromTableElement;
exports.registerTableCellUnmergeTransform = registerTableCellUnmergeTransform;
exports.registerTablePlugin = registerTablePlugin;
exports.registerTableSelectionObserver = registerTableSelectionObserver;
exports.setScrollableTablesActive = setScrollableTablesActive;