@lexical/table
Version:
This package provides the Table feature for Lexical.
1,446 lines (1,391 loc) • 159 kB
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 selectedRowCount = anchorCell.__rowSpan;
const nextRow = gridMap[focusEndRow + 1];
const nextRowNode = grid.getChildAtIndex(focusEndRow + 1);
for (let row = focusEndRow; row >= anchorStartRow; row--) {
for (let column = columnCount - 1; column >= 0; column--) {
const {
cell,
startRow: cellStartRow,
startColumn: cellStartColumn
} = gridMap[row][column];
if (cellStartColumn !== column) {
// Don't repeat work for the same Cell
continue;
}
// Rows overflowing top have to be trimmed
if (row === anchorStartRow && cellStartRow < anchorStartRow) {
const overflowTop = anchorStartRow - cellStartRow;
cell.setRowSpan(cell.__rowSpan - Math.min(selectedRowCount, cell.__rowSpan - overflowTop));
}
// Rows overflowing bottom have to be trimmed and moved to the next row
if (cellStartRow >= anchorStartRow && cellStartRow + cell.__rowSpan - 1 > focusEndRow) {
cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1));
if (!(nextRowNode !== null)) {
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 +