/**
 * 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.
 *
 */

import { addClassNamesToElement, $descendantsMatching, $findMatchingParent, removeClassNamesFromElement, objectKlassEquals, isHTMLElement as isHTMLElement$1, $insertFirst as $insertFirst$1, mergeRegister, $insertNodeToNearestRoot, $dfs, $unwrapAndFilterDescendants } from '@lexical/utils';
import { ElementNode, isHTMLElement, $isInlineElementOrDecoratorNode, $isTextNode, $isLineBreakNode, $createParagraphNode, $applyNodeReplacement, createCommand, $createTextNode, $getSelection, $isRangeSelection, $isParagraphNode, $createPoint, $getNodeByKey, $isElementNode, $normalizeSelection__EXPERIMENTAL, isCurrentlyReadOnlyMode, TEXT_TYPE_TO_FORMAT, $getEditor, $setSelection, SELECTION_CHANGE_COMMAND, getDOMSelection, $createRangeSelection, $isRootNode, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_HIGH, KEY_ESCAPE_COMMAND, DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, CUT_COMMAND, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_TAB_COMMAND, FOCUS_COMMAND, $getNearestNodeFromDOMNode, isDOMNode, $isRootOrShadowRoot, $getPreviousSelection, $getNodeByKeyOrThrow, $caretFromPoint, $isExtendableTextPointCaret, $extendCaretToRange, $isSiblingCaret, $getSiblingCaret, $setPointFromCaret, $normalizeCaret, $createRangeSelectionFromDom, $isChildCaret, $getChildCaret, $getAdjacentChildCaret, setDOMUnmanaged, COMMAND_PRIORITY_EDITOR, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, SELECT_ALL_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, $getRoot, defineExtension, safeCast } from 'lexical';
import { signal, effect, namedSignals } from '@lexical/extension';
import { copyToClipboard, $getClipboardDataFromSelection } from '@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 ElementNode {
  /** @internal */
  __colSpan;
  /** @internal */
  __rowSpan;
  /** @internal */
  __headerState;
  /** @internal */
  __width;
  /** @internal */
  __backgroundColor;
  /** @internal */
  __verticalAlign;
  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;
    this.__colSpan = node.__colSpan;
    this.__headerState = node.__headerState;
    this.__width = node.__width;
  }
  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;
    }
    addClassNamesToElement(element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader);
    return element;
  }
  exportDOM(editor) {
    const output = super.exportDOM(editor);
    if (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);
  }

  // Determine header state based on the 'scope' attribute
  let headerState = TableCellHeaderStates.NO_STATUS;
  if (nodeName === 'th') {
    const scope = domNode_.getAttribute('scope');
    if (scope === 'col') {
      headerState = TableCellHeaderStates.COLUMN;
    } else if (scope === 'row') {
      headerState = TableCellHeaderStates.ROW;
    } else {
      const parentRow = domNode_.parentElement;
      const isInHeaderRow = isHTMLElement(parentRow) && parentRow.nodeName.toLowerCase() === 'tr' && isHTMLElement(parentRow.parentElement) && (parentRow.parentElement.nodeName.toLowerCase() === 'thead' || parentRow.rowIndex === 0);
      const isFirstColumn = domNode_.cellIndex === 0;
      if (isInHeaderRow) {
        headerState |= TableCellHeaderStates.ROW;
      }
      if (isFirstColumn) {
        headerState |= TableCellHeaderStates.COLUMN;
      }
      if (headerState === TableCellHeaderStates.NO_STATUS) {
        headerState = TableCellHeaderStates.ROW;
      }
    }
  }
  const tableCellNode = $createTableCellNode(headerState, 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 ($isLineBreakNode(firstChild) && paragraphNode.getChildrenSize() === 1) {
            firstChild.remove();
          }
        }
      };
      for (const child of childLexicalNodes) {
        if ($isInlineElementOrDecoratorNode(child) || $isTextNode(child) || $isLineBreakNode(child)) {
          if ($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 = $createParagraphNode().append(child);
            result.push(paragraphNode);
          }
        } else {
          result.push(child);
          removeSingleLineBreakNode();
          paragraphNode = null;
        }
      }
      removeSingleLineBreakNode();
      if (result.length === 0) {
        result.push($createParagraphNode());
      }
      return result;
    },
    node: tableCellNode
  };
}
function $createTableCellNode(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width) {
  return $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 = 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 ElementNode {
  /** @internal */
  __height;
  static getType() {
    return 'tablerow';
  }
  static clone(node) {
    return new TableRowNode(node.__height, node.__key);
  }
  afterCloneFrom(prevNode) {
    super.afterCloneFrom(prevNode);
    this.__height = prevNode.__height;
  }
  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`;
    }
    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 => $descendantsMatching(children, $isTableCellNode),
    node: $createTableRowNode(height)
  };
}
function $createTableRowNode(height) {
  return $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 = $createParagraphNode();
      paragraphNode.append($createTextNode());
      tableCellNode.append(paragraphNode);
      tableRowNode.append(tableCellNode);
    }
    tableNode.append(tableRowNode);
  }
  return tableNode;
}
function $getTableCellNodeFromLexicalNode(startingNode) {
  const node = $findMatchingParent(startingNode, n => $isTableCellNode(n));
  if ($isTableCellNode(node)) {
    return node;
  }
  return null;
}
function $getTableRowNodeFromTableCellNodeOrThrow(startingNode) {
  const node = $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 = $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($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 = $getSelection();
  if (!($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($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($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($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 = $getSelection();
  if (!($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($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 = $getSelection();
  if (!($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 = $getSelection();
  if (!($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($createParagraphNode());
  }
  return targetCell;
}
function $cellContainsEmptyParagraph(cell) {
  if (cell.getChildrenSize() !== 1) {
    return false;
  }
  const firstChild = cell.getFirstChildOrThrow();
  if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
    return false;
  }
  return true;
}
function $unmergeCell() {
  const selection = $getSelection();
  if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
    formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
  }
  const anchor = selection.anchor.getNode();
  const cellNode = $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($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($createParagraphNode()));
        }
      } else {
        for (let j = colSpan - 1; j >= 0; j--) {
          insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append($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_ = $findMatchingParent(source, $isTableCellNode);
    if (!$isTableCellNode(cell_)) {
      formatDevErrorMessage(`Expected to find a parent TableCellNode`);
    }
    cell = cell_;
  } else {
    const cell_ = $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
  };
}

/**
 * Checks if the table does not have any merged cells.
 *
 * @param table Table to check for if it has any merged cells.
 * @returns True if the table does not have any merged cells, false otherwise.
 */
function $isSimpleTable(table) {
  const rows = table.getChildren();
  let columns = null;
  for (const row of rows) {
    if (!$isTableRowNode(row)) {
      return false;
    }
    if (columns === null) {
      columns = row.getChildrenSize();
    }
    if (row.getChildrenSize() !== columns) {
      return false;
    }
    const cells = row.getChildren();
    for (const cell of cells) {
      if (!$isTableCellNode(cell) || cell.getRowSpan() !== 1 || cell.getColSpan() !== 1) {
        return false;
      }
    }
  }
  return (columns || 0) > 0;
}

/**
 * Moves a column from one position to another within a simple (non-merged) table.
 *
 * @param tableNode The table node to modify.
 * @param originColumn The index of the column to move.
 * @param targetColumn The index to move the column to.
 */
function $moveTableColumn(tableNode, originColumn, targetColumn) {
  if (originColumn === targetColumn) {
    return;
  }
  const columnCount = tableNode.getColumnCount();
  if (originColumn < 0 || originColumn >= columnCount || targetColumn < 0 || targetColumn >= columnCount) {
    return;
  }
  if (!$isSimpleTable(tableNode)) {
    return;
  }
  const rows = tableNode.getChildren().filter($isTableRowNode);
  rows.forEach(row => {
    const cells = row.getChildren();
    const [moved] = cells.splice(originColumn, 1);
    cells.splice(targetColumn, 0, moved);
    row.splice(0, cells.length, cells);
  });
  const colWidths = tableNode.getColWidths();
  if (colWidths && colWidths.length === columnCount) {
    const newWidths = [...colWidths];
    const [movedWidth] = newWidths.splice(originColumn, 1);
    newWidths.splice(targetColumn, 0, movedWidth);
    tableNode.setColWidths(newWidths);
  }
}
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 = $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 {
  tableKey;
  anchor;
  focus;
  _cachedNodes;
  dirty;
  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() {
    if (this.tableKey === 'root' || this.anchor.key === 'root' || this.anchor.type !== 'element' || this.focus.key === 'root' || this.focus.type !== 'element') {
      return false;
    }

    // Check if the referenced nodes still exist in the editor
    const tableNode = $getNodeByKey(this.tableKey);
    const anchorNode = $getNodeByKey(this.anchor.key);
    const focusNode = $getNodeByKey(this.focus.key);
    return tableNode !== null && anchorNode !== null && focusNode !== null;
  }

  /**
   * 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, $createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), $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 ($isParagraphNode(paragraph)) {
        format |= paragraph.getTextFormat();
      }
    });
    const formatFlag = TEXT_TYPE_TO_FORMAT[type];
    return (format & formatFlag) !== 0;
  }
  insertNodes(nodes) {
    const focusNode = this.focus.getNode();
    if (!$isElementNode(focusNode)) {
      formatDevErrorMessage(`Expected TableSelection focus to be an ElementNode`);
    }
    const selection = $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 (!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 = $createPoint('root', 0, 'element');
  const focus = $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 = $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 && $isElementNode(currentNode)) {
      stack.push(currentNode.getChildren());
    }
  }
}

function $getTableAndElementByKey(tableNodeKey, editor = $getEditor()) {
  const tableNode = $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
  };
}
/**
 * Tracks table selection state that sits across all tables.
 */
class TableObservers {
  observers;
  nextFocus;
  shouldCheckSelectionForTable;
  constructor() {
    this.observers = new Map();
    this.nextFocus = null;
    this.shouldCheckSelectionForTable = null;
  }

  /**
   * @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
   * 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.
   */
  setShouldCheckSelectionForTable(tableKey) {
    this.shouldCheckSelectionForTable = tableKey;
  }
  /**
   * @internal
   */
  getAndClearShouldCheckSelectionForTable() {
    const {
      shouldCheckSelectionForTable
    } = this;
    if (shouldCheckSelectionForTable) {
      this.shouldCheckSelectionForTable = null;
      return shouldCheckSelectionForTable;
    }
    return null;
  }
}
class TableObserver {
  focusX;
  focusY;
  listenersToRemove;
  table;
  isHighlightingCells;
  anchorX;
  anchorY;
  tableNodeKey;
  anchorCell;
  focusCell;
  anchorCellNodeKey;
  focusCellNodeKey;
  editor;
  tableSelection;
  hasHijackedSelectionStyles;
  isSelecting;
  pointerType;
  abortController;
  listenerOptions;
  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.abortController = new AbortController();
    this.listenerOptions = {
      signal: this.abortController.signal
    };
    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(setEmptySelection = true) {
    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 (setEmptySelection && $getSelection() !== null) {
      $setSelection(null);
      editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
    }
  }
  $enableHighlightStyle() {
    const editor = this.editor;
    const {
      tableElement
    } = this.$lookup();
    removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
    tableElement.classList.remove('disable-selection');
    this.hasHijackedSelectionStyles = false;
  }
  $disableHighlightStyle() {
    const {
      tableElement
    } = this.$lookup();
    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 */
  updateDOMSelection() {
    if (this.anchorCell !== null && this.focusCell !== null) {
      const domSelection = 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;

    // Enable highlighting if: ignoreStart is true, or anchor differs from focus,
    // or we have valid tableSelection with anchor (for first drag after column switch)
    if (!this.isHighlightingCells) {
      const shouldEnable = ignoreStart || this.anchorX !== cellX || this.anchorY !== cellY || this.tableSelection != null && this.anchorCellNodeKey != null;
      if (shouldEnable) {
        this.isHighlightingCells = true;
        this.$disableHighlightStyle();
      }
    }

    // Skip if we're trying to select the same cell we already have selected
    // But only if focusX/focusY are valid (not -1, which means not reset)
    if (this.focusX !== -1 && this.focusY !== -1 && 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) {
        let targetCellNode = focusTableCellNode;

        // Fallback: use coordinates if DOM lookup failed (handles timing issues on first drag)
        if (targetCellNode === null && ignoreStart) {
          targetCellNode = tableNode.getCellNodeFromCords(cellX, cellY, this.table);
        }
        if (targetCellNode !== null) {
          const anchorTableCell = this.$getAnchorTableCellOrThrow();
          this.focusCellNodeKey = targetCellNode.getKey();
          this.tableSelection = $createTableSelectionFrom(tableNode, anchorTableCell, targetCellNode);
          $setSelection(this.tableSelection);
          editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
          $updateDOMForSelection(editor, this.table, this.tableSelection);
          return true;
        }
      }
    }
    return false;
  }
  $getAnchorTableCell() {
    return this.anchorCellNodeKey ? $getNodeByKey(this.anchorCellNodeKey) : null;
  }
  $getAnchorTableCellOrThrow() {
    const anchorTableCell = this.$getAnchorTableCell();
    if (!(anchorTableCell !== null)) {
      formatDevErrorMessage(`TableObserver anchorTableCell is null`);
    }
    return anchorTableCell;
  }
  $getFocusTableCell() {
    return this.focusCellNodeKey ? $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;
    // Reset focus state to prevent stale values from previous selections
    this.focusX = -1;
    this.focusY = -1;
    this.focusCell = null;
    this.focusCellNodeKey = null;
    const {
      tableNode
    } = this.$lookup();
    const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
    if (anchorTableCellNode !== null) {
      const anchorNodeKey = anchorTableCellNode.getKey();
      if (this.tableSelection != null) {
        this.tableSelection = this.tableSelection.clone();
        this.tableSelection.set(tableNode.getKey(), anchorNodeKey, anchorNodeKey);
      } else {
        this.tableSelection = $createTableSelectionFrom(tableNode, anchorTableCellNode, anchorTableCellNode);
      }
      this.anchorCellNodeKey = anchorNodeKey;
    }
  }
  $formatCells(type) {
    const selection = $getSelection();
    if (!$isTableSelection(selection)) {
      formatDevErrorMessage(`Expected Table selection`);
    }
    const formatSelection = $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 = $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);
    });
    $setSelection(selection);
    this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
  }
  $clearText() {
    const {
      editor
    } = this;
    const tableNode = $getNodeByKey(this.tableNodeKey);
    if (!$isTableNode(tableNode)) {
      throw new Error('Expected TableNode.');
    }
    const selection = $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 ($isRootNode(parent) && parent.isEmpty()) {
        editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
      }
      return;
    }
    selectedNodes.forEach(cellNode => {
      if ($isElementNode(cellNode)) {
        const paragraphNode = $createParagraphNode();
        const textNode = $createTextNode();
        paragraphNode.append(textNode);
        cellNode.append(paragraphNode);
        cellNode.getChildren().forEach(child => {
          if (child !== paragraphNode) {
            child.remove();
          }
        });
      }
    });
    $updateDOMForSelection(editor, this.table, null);
    $setSelection(null);
    editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
  }
}

const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
const isPointerDownOnEvent = event => {
  return (event.buttons & 1) === 1;
};
function isHTMLTableElement(el) {
  return 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 = [[KEY_ARROW_DOWN_COMMAND, 'down'], [KEY_ARROW_UP_COMMAND, 'up'], [KEY_ARROW_LEFT_COMMAND, 'backward'], [KEY_ARROW_RIGHT_COMMAND, 'forward']];
const DELETE_TEXT_COMMANDS = [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND];
const DELETE_KEY_COMMANDS = [KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND];
function registerTableWindowHandlers(editor, tableObservers) {
  const rootElement = editor.getRootElement();
  const editorWindow = editor._window;
  if (!rootElement || !editorWindow) {
    return () => {};
  }
  const pointerDownCallback = event => {
    const target = event.target;
    if (event.button !== 0 || !isDOMNode(target) || !rootElement.contains(target)) {
      return;
    }
    const selectionInfo = getTableObserverFromCellNode(target);
    editor.update(() => {
      // Clear highlights from all tables (even one we're actively clicking on)
      const selection = $getSelection();
      if ($isTableSelection(selection)) {
        for (const [observer] of tableObservers.observers.values()) {
          observer.$clearHighlight(false);
        }
        $setSelection(null);
        editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
      }
      if (!selectionInfo) {
        return;
      }
      const {
        tableObserver,
        tableElement,
        cellElement
      } = selectionInfo;
      $handleTableClick(editor, event, cellElement, tableElement, tableObserver, tableObservers);
    });
  };
  editorWindow.addEventListener('pointerdown', pointerDownCallback);
  return () => {
    editorWindow.removeEventListener('pointerdown', pointerDownCallback);
  };
}
function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableObserver, tableObservers) {
  const editorWindow = editor._window;
  if (!editorWindow) {
    return;
  }
  const createPointerHandlers = startingCell => {
    if (tableObserver.isSelecting) {
      return;
    }
    tableObserver.isSelecting = true;

    // Set anchor immediately if starting cell provided (handles direct drag without click)
    if (startingCell !== null && tableObserver.anchorCell === null) {
      editor.update(() => {
        tableObserver.$setAnchorCellForSelection(startingCell);
      });
    }
    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 (!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) {
        const anchorCell = focusCell;
        // Fallback: set anchor if still missing (handles race conditions)
        if (tableObserver.anchorCell === null) {
          editor.update(() => {
            tableObserver.$setAnchorCellForSelection(anchorCell);
          });
        }
        if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
          tableObservers.setNextFocus({
            focusCell,
            override,
            tableKey: tableObserver.tableNodeKey
          });
          editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
        }
      }
    };
    editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
    editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
  };
  tableObserver.pointerType = event.pointerType;
  const tableNode = $getNodeByKeyOrThrow(tableObserver.tableNodeKey);
  const prevSelection = $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) && ($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(selectedDOMCell);
      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(selectedDOMCell);
    }
  }

  // Pass the target cell to createPointerHandlers so it can be used as anchor
  // if user drags directly without clicking first
  createPointerHandlers(selectedDOMCell);
}
function applyTableHandlers(tableNode, element, editor, hasTabHandler, tableObservers) {
  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 onTripleClick = event => {
    if (event.detail >= 3 && 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);
  });
  for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
    tableObserver.listenersToRemove.add(editor.registerCommand(command, event => $handleArrowKey(editor, event, direction, tableNode, tableObserver, tableObservers), COMMAND_PRIORITY_HIGH));
  }
  tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ESCAPE_COMMAND, event => {
    const selection = $getSelection();
    if ($isTableSelection(selection)) {
      const focusCellNode = $findParentTableCellNodeInTable(tableNode, selection.focus.getNode());
      if (focusCellNode !== null) {
        stopEvent(event);
        focusCellNode.selectEnd();
        return true;
      }
    }
    return false;
  }, COMMAND_PRIORITY_HIGH));
  const deleteTextHandler = command => () => {
    const selection = $getSelection();
    if (!$isSelectionInTable(selection, tableNode)) {
      return false;
    }
    if ($isTableSelection(selection)) {
      tableObserver.$clearText();
      return true;
    } else if ($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 = $findMatchingParent(selection.anchor.getNode(), n => $isElementNode(n));
      const topLevelCellElementNode = nearestElementNode && $findMatchingParent(nearestElementNode, n => $isElementNode(n) && $isTableCellNode(n.getParent()));
      if (!$isElementNode(topLevelCellElementNode) || !$isElementNode(nearestElementNode)) {
        return false;
      }
      if (command === 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), COMMAND_PRIORITY_HIGH));
  }
  const $deleteCellHandler = event => {
    const selection = $getSelection();
    if (!($isTableSelection(selection) || $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, COMMAND_PRIORITY_HIGH));
  }
  tableObserver.listenersToRemove.add(editor.registerCommand(CUT_COMMAND, event => {
    const selection = $getSelection();
    if (selection) {
      if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
        return false;
      }
      // Copying to the clipboard is async so we must capture the data
      // before we delete it
      void copyToClipboard(editor, objectKlassEquals(event, ClipboardEvent) ? event : null, $getClipboardDataFromSelection(selection));
      const intercepted = $deleteCellHandler(event);
      if ($isRangeSelection(selection)) {
        selection.removeText();
        return true;
      }
      return intercepted;
    }
    return false;
  }, COMMAND_PRIORITY_HIGH));
  tableObserver.listenersToRemove.add(editor.registerCommand(FORMAT_TEXT_COMMAND, payload => {
    const selection = $getSelection();
    if (!$isSelectionInTable(selection, tableNode)) {
      return false;
    }
    if ($isTableSelection(selection)) {
      tableObserver.$formatCells(payload);
      return true;
    } else if ($isRangeSelection(selection)) {
      const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
      if (!$isTableCellNode(tableCellNode)) {
        return false;
      }
    }
    return false;
  }, COMMAND_PRIORITY_HIGH));
  tableObserver.listenersToRemove.add(editor.registerCommand(FORMAT_ELEMENT_COMMAND, formatType => {
    const selection = $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 ($isElementNode(child) && !child.isInline()) {
            child.setFormat(formatType);
          }
        }
      }
    }
    return true;
  }, COMMAND_PRIORITY_HIGH));
  tableObserver.listenersToRemove.add(editor.registerCommand(CONTROLLED_TEXT_INSERTION_COMMAND, payload => {
    const selection = $getSelection();
    if (!$isSelectionInTable(selection, tableNode)) {
      return false;
    }
    if ($isTableSelection(selection)) {
      tableObserver.$clearHighlight();
      return false;
    } else if ($isRangeSelection(selection)) {
      const tableCellNode = $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, [$createTextNode(payload)]);
          return true;
        }
      }
    }
    return false;
  }, COMMAND_PRIORITY_HIGH));
  if (hasTabHandler) {
    tableObserver.listenersToRemove.add(editor.registerCommand(KEY_TAB_COMMAND, event => {
      const selection = $getSelection();
      if (!$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;
    }, COMMAND_PRIORITY_HIGH));
  }
  tableObserver.listenersToRemove.add(editor.registerCommand(FOCUS_COMMAND, payload => {
    return tableNode.isSelected();
  }, COMMAND_PRIORITY_HIGH));
  tableObserver.listenersToRemove.add(editor.registerCommand(INSERT_PARAGRAPH_COMMAND, () => {
    const selection = $getSelection();
    if (!$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
      return false;
    }
    const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
    if (edgePosition) {
      $insertParagraphAtTableEdge(edgePosition, tableNode);
      return true;
    }
    return false;
  }, COMMAND_PRIORITY_HIGH));
  return tableObserver;
}

/** @internal */
function $handleTableSelectionChangeCommand(tableObservers, editor) {
  const selection = $getSelection();
  const prevSelection = $getPreviousSelection();
  const nextFocus = tableObservers.getAndClearNextFocus();
  if (nextFocus !== null) {
    const {
      tableKey,
      focusCell
    } = nextFocus;
    const observerAndTable = tableObservers.observers.get(tableKey);
    if (!!!observerAndTable) {
      formatDevErrorMessage(`tableObserver not found for tableKey: ${tableKey}`);
    }
    const [tableObserver] = observerAndTable;
    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 (tableObserver.anchorCell !== null && tableObserver.anchorCellNodeKey !== null && focusCell.elem !== tableObserver.anchorCell.elem && tableObserver.tableSelection !== null) {
      // The selection has crossed cells
      // If we have an anchor cell set and tableSelection initialized,
      // we have all the necessary state to create the selection.
      // The presence of nextFocus means we're dragging, so process it.
      // Use ignoreStart=true to ensure isHighlightingCells is set correctly
      // on the first drag attempt, especially when switching columns.
      tableObserver.$setFocusCellForSelection(focusCell, true);
      return true;
    }
  }
  const shouldCheckSelectionForTable = tableObservers.getAndClearShouldCheckSelectionForTable();
  // 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 (!!shouldCheckSelectionForTable && $isRangeSelection(prevSelection) && $isRangeSelection(selection) && selection.isCollapsed()) {
    const tableNode = $getNodeByKeyOrThrow(shouldCheckSelectionForTable);
    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($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 ($isTableSelection(selection)) {
    $fixTableSelectionForSelectedTable(editor, selection);
  }
  if ($isRangeSelection(selection)) {
    $fixRangeSelectionForSelectedTable(selection, tableObservers);
  }

  // Generic selection logic that runs across every table observer when the selection changes.
  // Note: the selection might have changed in the code above, which re-dispatches the selection change command
  // and gets handled here on the second pass. This should be refactored.
  const tableNodesAndObservers = tableObservers.observers.entries().map(([tableKey, [tableObserver]]) => ({
    tableNode: $getNodeByKeyOrThrow(tableKey),
    tableObserver
  })).toArray();
  for (const {
    tableNode,
    tableObserver
  } of tableNodesAndObservers) {
    $syncTableSelectionState(editor, tableNode, tableObserver);
  }
  return false;
}

/**
 * Handles cases where range selections cross into, out of, or within tables.
 */
function $fixRangeSelectionForSelectedTable(selection, tableObservers) {
  const prevSelection = $getPreviousSelection();
  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 anchorCellTable = anchorCellNode ? $findTableNode(anchorCellNode) : null;
  const focusCellTable = focusCellNode ? $findTableNode(focusCellNode) : null;
  const isBackward = selection.isBackward();
  const isSameTable = anchorCellNode && focusCellNode && anchorCellTable && focusCellTable && anchorCellTable.is(focusCellTable);

  // The focus should be moved (to cover the whole focus table) if it is moved outside of the anchor's table.
  // For example, when dragging from outside a table into it.
  const shouldMoveFocus = focusCellTable && (!anchorCellTable || anchorCellTable.isParentOf(focusCellTable));
  // The anchor should be moved (to cover the whole anchor table) if the focus is moved outside of the anchor table.
  // For example, when dragging from inside a table out of it.
  const shouldMoveAnchor = anchorCellTable && (!focusCellTable || focusCellTable.isParentOf(anchorCellTable));
  if (shouldMoveFocus) {
    // Select the whole focus table.
    const newSelection = selection.clone();
    const [tableMap] = $computeTableMap(focusCellTable, 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 ? 0 : lastCell.getChildrenSize(), 'element');
    $setSelection(newSelection);
  } else if (shouldMoveAnchor) {
    // Select the whole anchor table.
    const newSelection = selection.clone();
    const [tableMap] = $computeTableMap(anchorCellTable, anchorCellNode, anchorCellNode);
    const firstCell = tableMap[0][0].cell;
    const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
    newSelection.anchor.set(isBackward ? lastCell.getKey() : firstCell.getKey(), isBackward ? lastCell.getChildrenSize() : 0, 'element');
    $setSelection(newSelection);
  } else if (isSameTable) {
    // Handle case when selection spans across multiple cells but still
    // has range selection, then we convert it into table selection
    // For example, this fires when dragging up from first cell, outside of the table, or when clicking a cell
    // then shift-clicking another cell.
    const observerInfo = tableObservers.observers.get(anchorCellTable.getKey());
    if (!!!observerInfo) {
      formatDevErrorMessage(`tableObserver not found for tableKey: ${anchorCellTable.getKey()}`);
    }
    const [tableObserver] = observerInfo;
    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() && $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;
      }
    }
  }
}

/**
 * Ensures that a TableSelection is automatically changed to a RangeSelection when the selection goes outside of the table.
 */
function $fixTableSelectionForSelectedTable(editor, selection) {
  const editorWindow = getEditorWindow(editor);
  const prevSelection = $getPreviousSelection();
  if (!selection.is(prevSelection)) {
    return;
  }
  const tableNode = $getNodeByKeyOrThrow(selection.tableKey);
  // if selection goes outside of the table we need to change it to Range selection
  const domSelection = getDOMSelection(editorWindow);
  if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
    const focusNode = $getNearestNodeFromDOMNode(domSelection.focusNode);
    const isFocusOutside = focusNode && !tableNode.isParentOf(focusNode);
    const anchorNode = $getNearestNodeFromDOMNode(domSelection.anchorNode);
    const isAnchorInside = anchorNode && tableNode.isParentOf(anchorNode);
    if (isFocusOutside && isAnchorInside && domSelection.rangeCount > 0) {
      const newSelection = $createRangeSelectionFromDom(domSelection, editor);
      if (newSelection) {
        newSelection.anchor.set(tableNode.getKey(), selection.isBackward() ? tableNode.getChildrenSize() : 0, 'element');
        domSelection.removeAllRanges();
        $setSelection(newSelection);
      }
    }
  }
}

// Handle keeping the table observer/DOM in sync with the selection.
function $syncTableSelectionState(editor, tableNode, tableObserver) {
  const selection = $getSelection();
  const prevSelection = $getPreviousSelection();
  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);
    }
  }
  if (tableObserver.hasHijackedSelectionStyles && !tableNode.isSelected()) {
    $removeHighlightStyleToTable(editor, tableObserver);
  } else if (!tableObserver.hasHijackedSelectionStyles && tableNode.isSelected()) {
    $addHighlightStyleToTable(editor, 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 getTableObserverFromCellNode(node) {
  const cellNode = getDOMCellFromTarget(node);
  if (cellNode === null) {
    return null;
  }
  let currentNode = cellNode.elem;
  while (currentNode != null) {
    const nodeName = currentNode.nodeName;
    if (nodeName === 'TABLE' && LEXICAL_ELEMENT_KEY in currentNode && !!currentNode[LEXICAL_ELEMENT_KEY]) {
      return {
        cellElement: cellNode,
        tableElement: currentNode,
        tableObserver: currentNode[LEXICAL_ELEMENT_KEY]
      };
    }
    currentNode = currentNode.parentNode;
  }
  return 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 = $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 ($isElementNode(sibling)) {
    return sibling.selectEnd();
  }
  const parentRow = $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 ($isElementNode(child)) {
      return child.selectEnd();
    }
  }
  const parentTable = $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 ($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 = $getNearestNodeFromDOMNode(element);
  if (!$isTableCellNode(node)) {
    formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
  }
  addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
}
function $removeHighlightFromDOM(editor, cell) {
  const element = cell.elem;
  const node = $getNearestNodeFromDOMNode(element);
  if (!$isTableCellNode(node)) {
    formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
  }
  const editorThemeClasses = editor._config.theme;
  removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
}
function $findCellNode(node) {
  const cellNode = $findMatchingParent(node, $isTableCellNode);
  return $isTableCellNode(cellNode) ? cellNode : null;
}
function $findTableNode(node) {
  const tableNode = $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 ($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 = $caretFromPoint(selection.focus, isBackward ? 'previous' : 'next');
  if ($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 $extendCaretToRange(initialFocus).iterNodeCarets('shadowRoot')) {
    if (!($isSiblingCaret(nextCaret) && $isElementNode(nextCaret.origin))) {
      return false;
    }
    lastCaret = nextCaret;
  }
  const lastCaretParent = lastCaret.getParentAtCaret();
  if (!$isTableCellNode(lastCaretParent)) {
    return false;
  }
  const anchorCell = lastCaretParent;
  const focusCaret = $findNextTableCell($getSiblingCaret(anchorCell, lastCaret.direction));
  const anchorCellTable = $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($getSiblingCaret(anchorCellTable, initialFocus.direction));
      $setPointFromCaret(selection.anchor, outerFocusCaret);
      $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 = $normalizeCaret(focusCaret);
    $setPointFromCaret(selection.anchor, innerFocusCaret);
    $setPointFromCaret(selection.focus, innerFocusCaret);
  }
  stopEvent(event);
  return true;
}
function $getTableExitCaret(initialCaret) {
  const adjacent = $getAdjacentChildCaret(initialCaret);
  return $isChildCaret(adjacent) ? $normalizeCaret(adjacent) : initialCaret;
}
function $findNextTableCell(initialCaret) {
  for (const nextCaret of $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 ($isChildCaret(nextCaret)) {
        return $getChildCaret(origin, initialCaret.direction);
      }
    } else if (!$isTableRowNode(origin)) {
      break;
    }
  }
  return null;
}
function $handleArrowKey(editor, event, direction, tableNode, tableObserver, tableObservers) {
  if ((direction === 'up' || direction === 'down') && isTypeaheadMenuInView(editor)) {
    return false;
  }
  const selection = $getSelection();
  if (!$isSelectionInTable(selection, tableNode)) {
    if ($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 = $findMatchingParent(focusNode, n => $isTableNode(n));
          if ($isTableCellNode(focusParentNode)) {
            focusParentNode = $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 ($isElementNode(sibling)) {
              newOffset = sibling.getChildrenSize();
            }
          }
          let newFocusNode = sibling;
          if (direction === 'up') {
            if ($isElementNode(sibling)) {
              const lastCell = sibling.getLastChild();
              newFocusNode = lastCell ? lastCell : sibling;
              newOffset = $isTextNode(newFocusNode) ? newFocusNode.getTextContentSize() : 0;
            }
          }
          const newSelection = selection.clone();
          newSelection.focus.set(newFocusNode.getKey(), newOffset, $isTextNode(newFocusNode) ? 'text' : 'element');
          $setSelection(newSelection);
          stopEvent(event);
          return true;
        } else if ($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 = $findMatchingParent(focusNode, n => $isElementNode(n) && !n.isInline());
          if ($isTableCellNode(focusParentNode)) {
            focusParentNode = $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);
            $setSelection(newSelection);
            return true;
          }
        }
      }
    }
    if (direction === 'down' && $isScrollableTablesActive(editor)) {
      // Enable Firefox workaround
      tableObservers.setShouldCheckSelectionForTable(tableNode.getKey());
    }
    return false;
  }
  if ($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 = $findMatchingParent(anchor.getNode(), $isTableCellNode);
      const focusCellNode = $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, tableObservers);
        }
      }
      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 = 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 = $findMatchingParent(anchor.getNode(), $isTableCellNode);
    const focusCellNode = $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 = $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 = 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 = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
  if (!anchorCellNode) {
    return undefined;
  }
  const parentTable = $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, $getNearestNodeFromDOMNode(startingDOM, editorState));
}

function isHTMLDivElement(element) {
  return isHTMLElement$1(element) && element.nodeName === 'DIV';
}
function updateColgroup(dom, 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) {
    addClassNamesToElement(dom, config.theme.tableRowStriping);
    dom.setAttribute('data-lexical-row-striping', 'true');
  } else {
    removeClassNamesFromElement(dom, config.theme.tableRowStriping);
    dom.removeAttribute('data-lexical-row-striping');
  }
}
function setFrozenColumns(dom, tableElement, config, frozenColumnCount) {
  if (frozenColumnCount > 0) {
    addClassNamesToElement(dom, config.theme.tableFrozenColumn);
    tableElement.setAttribute('data-lexical-frozen-column', 'true');
  } else {
    removeClassNamesFromElement(dom, config.theme.tableFrozenColumn);
    tableElement.removeAttribute('data-lexical-frozen-column');
  }
}
function setFrozenRows(dom, tableElement, config, frozenRowCount) {
  if (frozenRowCount > 0) {
    addClassNamesToElement(dom, config.theme.tableFrozenRow);
    tableElement.setAttribute('data-lexical-frozen-row', 'true');
  } else {
    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);
  }
  removeClassNamesFromElement(dom, ...removeClasses);
  addClassNamesToElement(dom, ...addClasses);
}
const scrollableEditors = new WeakSet();
function $isScrollableTablesActive(editor = $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 ElementNode {
  /** @internal */
  __rowStriping;
  __frozenColumnCount;
  __frozenRowCount;
  __colWidths;
  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;
    this.__colWidths = undefined;
  }
  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);
    setDOMUnmanaged(colGroup);
    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) {
        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);
    }
    const prevColCount = prevNode ? prevNode.getColumnCount() : 0;
    const prevColWidths = prevNode ? prevNode.__colWidths : undefined;
    if (this.getColumnCount() !== prevColCount || this.getColWidths() !== prevColWidths) {
      updateColgroup(tableElement, 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;
  }
  scaleDOMColWidths(dom, scale) {
    const colWidths = this.getColWidths();
    if (!colWidths) {
      return;
    }
    const slot = this.getDOMSlot(dom);
    const tableElement = slot.element;
    updateColgroup(tableElement, this.getColumnCount(), colWidths.map(width => width * scale));
  }
  exportDOM(editor) {
    const superExport = super.exportDOM(editor);
    const {
      element
    } = superExport;
    return {
      after: tableElement => {
        if (superExport.after) {
          tableElement = superExport.after(tableElement);
        }
        if (!isHTMLTableElement(tableElement) && isHTMLElement$1(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) && isHTMLElement$1(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 = $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 => $descendantsMatching(children, $isTableRowNode),
    node: tableNode
  };
}
function $createTableNode() {
  return $applyNodeReplacement(new TableNode());
}
function $isTableNode(node) {
  return node instanceof TableNode;
}

function $insertTable({
  rows,
  columns,
  includeHeaders
}, hasNestedTables) {
  const selection = $getSelection() || $getPreviousSelection();
  if (!selection || !$isRangeSelection(selection)) {
    return false;
  }

  // Prevent nested tables by checking if we're already inside a table
  if (!hasNestedTables && $findTableNode(selection.anchor.getNode())) {
    return false;
  }
  const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders);
  $insertNodeToNearestRoot(tableNode);
  const firstDescendant = tableNode.getFirstDescendant();
  if ($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($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 {
    $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
  $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($createParagraphNode());
      rowNode.append(newCell);
    }
  }
  const colWidths = node.getColWidths();
  const columnCount = node.getColumnCount();
  if (colWidths && colWidths.length !== columnCount) {
    let newColWidths = undefined;
    if (columnCount < colWidths.length) {
      newColWidths = colWidths.slice(0, columnCount);
    } else if (colWidths.length > 0) {
      // Repeat the last column width.
      const fillWidth = colWidths[colWidths.length - 1];
      newColWidths = [...colWidths, ...Array(columnCount - colWidths.length).fill(fillWidth)];
    }
    node.setColWidths(newColWidths);
  }
}
function $tableClickCommand(event) {
  if (event.detail < 3 || !isDOMNode(event.target)) {
    return false;
  }
  const startNode = $getNearestNodeFromDOMNode(event.target);
  if (startNode === null) {
    return false;
  }
  const blockNode = $findMatchingParent(startNode, node => $isElementNode(node) && !node.isInline());
  if (blockNode === null) {
    return false;
  }
  const rootNode = blockNode.getParent();
  if (!$isTableCellNode(rootNode)) {
    return false;
  }
  blockNode.select(0);
  return true;
}
function $tableSelectAllCommand() {
  const selection = $getSelection();
  if (!$isRangeSelection(selection)) {
    return false;
  }

  // Check if the selection is inside a table
  const anchorNode = selection.anchor.getNode();
  const tableNode = $findTableNode(anchorNode);
  if (tableNode === null) {
    return false;
  }

  // CRITICAL: Only intercept if table is the ONLY child of root
  // This is required to reproduce the bug: table must be the only content, no empty paragraphs
  // This prevents breaking other tests that expect RangeSelection when there's content outside table
  const root = $getRoot();
  if (!root.is(tableNode.getParent()) || root.getChildrenSize() !== 1) {
    return false;
  }

  // At this point, table is the only child
  // This is the exact scenario from issue #8074: table is the only content in editor

  // Get the table map to find first and last cells (handles merged cells correctly)
  const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
  if (tableMap.length === 0 || tableMap[0].length === 0) {
    return false;
  }

  // Get the first cell (top-left)
  const firstCellMap = tableMap[0][0];
  if (!firstCellMap || !firstCellMap.cell) {
    return false;
  }

  // Get the last cell (bottom-right)
  const lastRow = tableMap[tableMap.length - 1];
  const lastCellMap = lastRow[lastRow.length - 1];
  if (!lastCellMap || !lastCellMap.cell) {
    return false;
  }

  // Create a TableSelection that selects all cells
  const tableSelection = $createTableSelectionFrom(tableNode, firstCellMap.cell, lastCellMap.cell);
  $setSelection(tableSelection);
  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 {
              $insertFirst$1(row, newCell);
            }
          }
        }
      }
      for (const cell of unmerged) {
        cell.setColSpan(1);
        cell.setRowSpan(1);
      }
    }
  });
}
function registerTableSelectionObserver(editor, hasTabHandler = true) {
  const tableObservers = new TableObservers();
  const initializeTableNode = (tableNode, nodeKey, dom) => {
    const tableElement = getTableElement(tableNode, dom);
    const tableSelection = applyTableHandlers(tableNode, tableElement, editor, hasTabHandler, tableObservers);
    tableObservers.observers.set(nodeKey, [tableSelection, tableElement]);
  };
  return mergeRegister(registerTableWindowHandlers(editor, tableObservers), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
    return $handleTableSelectionChangeCommand(tableObservers, editor);
  }, COMMAND_PRIORITY_HIGH), editor.registerMutationListener(TableNode, nodeMutations => {
    editor.getEditorState().read(() => {
      for (const [nodeKey, mutation] of nodeMutations) {
        const tableSelection = tableObservers.observers.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();
            tableObservers.observers.delete(nodeKey);
            initializeTableNode(tableNode, nodeKey, tableElement);
          }
        } else if (mutation === 'destroyed') {
          if (tableSelection !== undefined) {
            tableSelection[0].removeListeners();
            tableObservers.observers.delete(nodeKey);
          }
        }
      }
    }, {
      editor
    });
  }, {
    skipInitialization: false
  }), () => {
    // 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 tableObservers.observers) {
      tableSelection.removeListeners();
    }
  });
}

/**
 * Register table command listeners 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, options) {
  if (!editor.hasNodes([TableNode])) {
    {
      formatDevErrorMessage(`TablePlugin: TableNode is not registered on editor`);
    }
  }
  const {
    hasNestedTables = signal(false)
  } = options ?? {};
  return mergeRegister(editor.registerCommand(INSERT_TABLE_COMMAND, payload => {
    return $insertTable(payload, hasNestedTables.peek());
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, (payload, dispatchEditor) => {
    if (editor !== dispatchEditor) {
      return false;
    }
    return $tableSelectionInsertClipboardNodesCommand(payload, hasNestedTables);
  }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(SELECT_ALL_COMMAND, $tableSelectAllCommand, COMMAND_PRIORITY_LOW), editor.registerCommand(CLICK_COMMAND, $tableClickCommand, COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(TableNode, $tableTransform), editor.registerNodeTransform(TableRowNode, $tableRowTransform), editor.registerNodeTransform(TableCellNode, $tableCellTransform));
}
function $tableSelectionInsertClipboardNodesCommand(selectionPayload, hasNestedTables) {
  const {
    nodes,
    selection
  } = selectionPayload;
  const hasTables = nodes.some(n => $isTableNode(n) || $dfs(n).some(d => $isTableNode(d.node)));
  if (!hasTables) {
    // Not pasting a table - no special handling required.
    return false;
  }
  const isTableSelection = $isTableSelection(selection);
  const isRangeSelection = $isRangeSelection(selection);
  const isSelectionInsideOfGrid = isRangeSelection && $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n)) !== null && $findMatchingParent(selection.focus.getNode(), n => $isTableCellNode(n)) !== null || isTableSelection;
  if (!isSelectionInsideOfGrid) {
    // Not pasting in a grid - no special handling required.
    return false;
  }

  // When pasting just a table, flatten the table on the destination table, even when nested tables are allowed.
  if (nodes.length === 1 && $isTableNode(nodes[0])) {
    return $insertTableIntoGrid(nodes[0], selection);
  }

  // If nested tables are enabled, allow pasting a table into a single cell.
  if (isRangeSelection && hasNestedTables.peek() && !$isMultiCellTableSelection(selection)) {
    return false;
  }

  // If we reached this point, there's a table in the selection and nested tables are not allowed - reject the paste.
  return true;
}
function $insertTableIntoGrid(tableNode, selection) {
  const anchorAndFocus = selection.getStartEndPoints();
  const isTableSelection = $isTableSelection(selection);
  if (anchorAndFocus === null) {
    return false;
  }
  const [anchor, focus] = anchorAndFocus;
  const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
  const focusCellNode = $findMatchingParent(focus.getNode(), n => $isTableCellNode(n));
  if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableRowNode(anchorRowNode) || !$isTableNode(gridNode)) {
    return false;
  }
  const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(gridNode, anchorCellNode, focusCellNode);
  const [templateGridMap] = $computeTableMapSkipCellCheck(tableNode, 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 backgroundColor = templateCell.getBackgroundColor();
      if (backgroundColor !== null && backgroundColor !== undefined) {
        cell.setBackgroundColor(backgroundColor);
      }
      const originalChildren = cell.getChildren();
      templateCell.getChildren().forEach(child => {
        if ($isTextNode(child)) {
          const paragraphNode = $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;
}
function $isMultiCellTableSelection(selection) {
  if ($isTableSelection(selection) && !selection.focus.getNode().is(selection.anchor.getNode())) {
    return true;
  }
  if ($isRangeSelection(selection) && $isTableCellNode(selection.anchor.getNode()) && !selection.anchor.getNode().is(selection.focus.getNode())) {
    return true;
  }
  return false;
}

/**
 * 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.
 *
 */

/**
 * Configures {@link TableNode}, {@link TableRowNode}, {@link TableCellNode} and
 * registers table behaviors (see {@link TableConfig})
 */
const TableExtension = defineExtension({
  build(editor, config, state) {
    return namedSignals(config);
  },
  config: safeCast({
    hasCellBackgroundColor: true,
    hasCellMerge: true,
    hasHorizontalScroll: true,
    hasNestedTables: false,
    hasTabHandler: true
  }),
  name: '@lexical/table/Table',
  nodes: () => [TableNode, TableRowNode, TableCellNode],
  register(editor, config, state) {
    const stores = state.getOutput();
    return mergeRegister(effect(() => {
      const hasHorizontalScroll = stores.hasHorizontalScroll.value;
      const hadHorizontalScroll = $isScrollableTablesActive(editor);
      if (hadHorizontalScroll !== hasHorizontalScroll) {
        setScrollableTablesActive(editor, hasHorizontalScroll);
        // Registering the transform has the side-effect of marking all existing
        // TableNodes as dirty. The handler is immediately unregistered.
        editor.registerNodeTransform(TableNode, () => {})();
      }
    }), registerTablePlugin(editor, stores), effect(() => registerTableSelectionObserver(editor, stores.hasTabHandler.value)), effect(() => stores.hasCellMerge.value ? undefined : registerTableCellUnmergeTransform(editor)), effect(() => stores.hasCellBackgroundColor.value ? undefined : editor.registerNodeTransform(TableCellNode, node => {
      if (node.getBackgroundColor() !== null) {
        node.setBackgroundColor(null);
      }
    })));
  }
});

export { $computeTableMap, $computeTableMapSkipCellCheck, $createTableCellNode, $createTableNode, $createTableNodeWithDimensions, $createTableRowNode, $createTableSelection, $createTableSelectionFrom, $deleteTableColumn, $deleteTableColumnAtSelection, $deleteTableColumn__EXPERIMENTAL, $deleteTableRowAtSelection, $deleteTableRow__EXPERIMENTAL, $findCellNode, $findTableNode, $getElementForTableNode, $getNodeTriplet, $getTableAndElementByKey, $getTableCellNodeFromLexicalNode, $getTableCellNodeRect, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $getTableRowNodeFromTableCellNodeOrThrow, $insertTableColumn, $insertTableColumnAtSelection, $insertTableColumn__EXPERIMENTAL, $insertTableRow, $insertTableRowAtSelection, $insertTableRow__EXPERIMENTAL, $isScrollableTablesActive, $isSimpleTable, $isTableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, $mergeCells, $moveTableColumn, $removeTableRowAtIndex, $unmergeCell, INSERT_TABLE_COMMAND, TableCellHeaderStates, TableCellNode, TableExtension, TableNode, TableObserver, TableRowNode, applyTableHandlers, getDOMCellFromTarget, getTableElement, getTableObserverFromTableElement, registerTableCellUnmergeTransform, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive };
