import {CustomTableNode} from "../nodes/custom-table"; import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell"; import {$isTableRowNode} from "@lexical/table"; export type CellRange = { fromX: number; fromY: number; toX: number; toY: number; } export class TableMap { rowCount: number = 0; columnCount: number = 0; // Represents an array (rows*columns in length) of cell nodes from top-left to // bottom right. Cells may repeat where merged and covering multiple spaces. cells: CustomTableCellNode[] = []; constructor(table: CustomTableNode) { this.buildCellMap(table); } protected buildCellMap(table: CustomTableNode) { const rowsAndCells: CustomTableCellNode[][] = []; const setCell = (x: number, y: number, cell: CustomTableCellNode) => { if (typeof rowsAndCells[y] === 'undefined') { rowsAndCells[y] = []; } rowsAndCells[y][x] = cell; }; const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]); const rowNodes = table.getChildren().filter(r => $isTableRowNode(r)); for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) { const rowNode = rowNodes[rowIndex]; const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c)); let targetColIndex: number = 0; for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) { const cellNode = cellNodes[cellIndex]; const colspan = cellNode.getColSpan() || 1; const rowSpan = cellNode.getRowSpan() || 1; for (let x = targetColIndex; x < targetColIndex + colspan; x++) { for (let y = rowIndex; y < rowIndex + rowSpan; y++) { while (cellFilled(x, y)) { targetColIndex += 1; x += 1; } setCell(x, y, cellNode); } } targetColIndex += colspan; } } this.rowCount = rowsAndCells.length; this.columnCount = Math.max(...rowsAndCells.map(r => r.length)); const cells = []; let lastCell: CustomTableCellNode = rowsAndCells[0][0]; for (let y = 0; y < this.rowCount; y++) { for (let x = 0; x < this.columnCount; x++) { if (!rowsAndCells[y] || !rowsAndCells[y][x]) { cells.push(lastCell); } else { cells.push(rowsAndCells[y][x]); lastCell = rowsAndCells[y][x]; } } } this.cells = cells; } public getCellAtPosition(x: number, y: number): CustomTableCellNode { const position = (y * this.columnCount) + x; if (position >= this.cells.length) { throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`); } return this.cells[position]; } public getCellsInRange(range: CellRange): CustomTableCellNode[] { const minX = Math.max(Math.min(range.fromX, range.toX), 0); const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1); const minY = Math.max(Math.min(range.fromY, range.toY), 0); const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1); const cells = new Set(); for (let y = minY; y <= maxY; y++) { for (let x = minX; x <= maxX; x++) { cells.add(this.getCellAtPosition(x, y)); } } return [...cells.values()]; } public getCellsInColumn(columnIndex: number): CustomTableCellNode[] { return this.getCellsInRange({ fromX: columnIndex, toX: columnIndex, fromY: 0, toY: this.rowCount - 1, }); } public getRangeForCell(cell: CustomTableCellNode): CellRange|null { let range: CellRange|null = null; const cellKey = cell.getKey(); for (let y = 0; y < this.rowCount; y++) { for (let x = 0; x < this.columnCount; x++) { const index = (y * this.columnCount) + x; const lCell = this.cells[index]; if (lCell.getKey() === cellKey) { if (range === null) { range = {fromX: x, toX: x, fromY: y, toY: y}; } else { range.fromX = Math.min(range.fromX, x); range.toX = Math.max(range.toX, x); range.fromY = Math.min(range.fromY, y); range.toY = Math.max(range.toY, y); } } } } return range; } }