1 import {BaseSelection, LexicalEditor} from "lexical";
2 import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table";
3 import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
4 import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
5 import {$getParentOfType} from "./nodes";
6 import {$getNodeFromSelection} from "./selection";
7 import {formatSizeValue} from "./dom";
8 import {TableMap} from "./table-map";
10 function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
11 return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
14 export function getTableColumnWidths(table: HTMLTableElement): string[] {
15 const maxColRow = getMaxColRowFromTable(table);
17 const colGroup = table.querySelector('colgroup');
18 let widths: string[] = [];
19 if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
20 widths = extractWidthsFromRow(colGroup);
22 if (widths.filter(Boolean).length === 0 && maxColRow) {
23 widths = extractWidthsFromRow(maxColRow);
29 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
30 const rows = table.querySelectorAll('tr');
31 let maxColCount: number = 0;
32 let maxColRow: HTMLTableRowElement | null = null;
34 for (const row of rows) {
35 if (row.childElementCount > maxColCount) {
37 maxColCount = row.childElementCount;
44 function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
45 return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
48 function extractWidthFromElement(element: HTMLElement): string {
49 let width = element.style.width || element.getAttribute('width');
50 if (width && !Number.isNaN(Number(width))) {
57 export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
58 const rows = node.getChildren() as TableRowNode[];
60 for (const row of rows) {
61 const cellCount = row.getChildren().length;
62 if (cellCount > maxCols) {
67 let colWidths = node.getColWidths();
68 if (colWidths.length === 0 || colWidths.length < maxCols) {
69 colWidths = Array(maxCols).fill('');
72 if (columnIndex + 1 > colWidths.length) {
73 console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
76 colWidths[columnIndex] = formatSizeValue(width);
77 node.setColWidths(colWidths);
80 export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
81 const colWidths = node.getColWidths();
82 if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
83 return Number(colWidths[columnIndex].replace('px', ''));
86 // Otherwise, get from table element
87 const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
89 const maxColRow = getMaxColRowFromTable(table);
90 if (maxColRow && maxColRow.children.length > columnIndex) {
91 const cell = maxColRow.children[columnIndex];
92 return cell.clientWidth;
99 function $getCellColumnIndex(node: CustomTableCellNode): number {
100 const row = node.getParent();
101 if (!$isTableRowNode(row)) {
106 const cells = row.getChildren<CustomTableCellNode>();
107 for (const cell of cells) {
108 let colSpan = cell.getColSpan() || 1;
110 if (cell.getKey() === node.getKey()) {
118 export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
119 const table = $getTableFromCell(cell)
120 const index = $getCellColumnIndex(cell);
122 if (table && index >= 0) {
123 $setTableColumnWidth(table, index, width);
127 export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
128 if ($isTableSelection(selection)) {
129 const nodes = selection.getNodes();
130 return nodes.filter(n => $isCustomTableCellNode(n));
133 const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
134 return cell ? [cell] : [];
137 export function $mergeTableCellsInSelection(selection: TableSelection): void {
138 const selectionShape = selection.getShape();
139 const cells = $getTableCellsFromSelection(selection);
140 if (cells.length === 0) {
144 const table = $getTableFromCell(cells[0]);
149 const tableMap = new TableMap(table);
150 const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
155 // We have to adjust the shape since it won't take into account spans for the head corner position.
156 const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
157 const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
159 const mergeCells = tableMap.getCellsInRange(
160 selectionShape.fromX,
161 selectionShape.fromY,
166 if (mergeCells.length === 0) {
170 const firstCell = mergeCells[0];
171 const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
172 const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
174 for (let i = 1; i < mergeCells.length; i++) {
175 const mergeCell = mergeCells[i];
176 firstCell.append(...mergeCell.getChildren());
180 firstCell.setColSpan(newWidth);
181 firstCell.setRowSpan(newHeight);