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";
5 import {$getParentOfType} from "./nodes";
6 import {$getNodeFromSelection} from "./selection";
7 import {formatSizeValue} from "./dom";
8 import {TableMap} from "./table-map";
9 import {$isCustomTableRowNode, CustomTableRowNode} from "../nodes/custom-table-row";
11 function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
12 return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
15 export function getTableColumnWidths(table: HTMLTableElement): string[] {
16 const maxColRow = getMaxColRowFromTable(table);
18 const colGroup = table.querySelector('colgroup');
19 let widths: string[] = [];
20 if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
21 widths = extractWidthsFromRow(colGroup);
23 if (widths.filter(Boolean).length === 0 && maxColRow) {
24 widths = extractWidthsFromRow(maxColRow);
30 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
31 const rows = table.querySelectorAll('tr');
32 let maxColCount: number = 0;
33 let maxColRow: HTMLTableRowElement | null = null;
35 for (const row of rows) {
36 if (row.childElementCount > maxColCount) {
38 maxColCount = row.childElementCount;
45 function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
46 return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
49 function extractWidthFromElement(element: HTMLElement): string {
50 let width = element.style.width || element.getAttribute('width');
51 if (width && !Number.isNaN(Number(width))) {
58 export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
59 const rows = node.getChildren() as TableRowNode[];
61 for (const row of rows) {
62 const cellCount = row.getChildren().length;
63 if (cellCount > maxCols) {
68 let colWidths = node.getColWidths();
69 if (colWidths.length === 0 || colWidths.length < maxCols) {
70 colWidths = Array(maxCols).fill('');
73 if (columnIndex + 1 > colWidths.length) {
74 console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
77 colWidths[columnIndex] = formatSizeValue(width);
78 node.setColWidths(colWidths);
81 export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
82 const colWidths = node.getColWidths();
83 if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
84 return Number(colWidths[columnIndex].replace('px', ''));
87 // Otherwise, get from table element
88 const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
90 const maxColRow = getMaxColRowFromTable(table);
91 if (maxColRow && maxColRow.children.length > columnIndex) {
92 const cell = maxColRow.children[columnIndex];
93 return cell.clientWidth;
100 function $getCellColumnIndex(node: CustomTableCellNode): number {
101 const row = node.getParent();
102 if (!$isTableRowNode(row)) {
107 const cells = row.getChildren<CustomTableCellNode>();
108 for (const cell of cells) {
109 let colSpan = cell.getColSpan() || 1;
111 if (cell.getKey() === node.getKey()) {
119 export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
120 const table = $getTableFromCell(cell)
121 const index = $getCellColumnIndex(cell);
123 if (table && index >= 0) {
124 $setTableColumnWidth(table, index, width);
128 export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTableCellNode): string {
129 const table = $getTableFromCell(cell)
130 const index = $getCellColumnIndex(cell);
135 const widths = table.getColWidths();
136 return (widths.length > index) ? widths[index] : '';
139 export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
140 if ($isTableSelection(selection)) {
141 const nodes = selection.getNodes();
142 return nodes.filter(n => $isCustomTableCellNode(n));
145 const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
146 return cell ? [cell] : [];
149 export function $mergeTableCellsInSelection(selection: TableSelection): void {
150 const selectionShape = selection.getShape();
151 const cells = $getTableCellsFromSelection(selection);
152 if (cells.length === 0) {
156 const table = $getTableFromCell(cells[0]);
161 const tableMap = new TableMap(table);
162 const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
167 // We have to adjust the shape since it won't take into account spans for the head corner position.
168 const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
169 const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
171 const mergeCells = tableMap.getCellsInRange({
172 fromX: selectionShape.fromX,
173 fromY: selectionShape.fromY,
178 if (mergeCells.length === 0) {
182 const firstCell = mergeCells[0];
183 const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
184 const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
186 for (let i = 1; i < mergeCells.length; i++) {
187 const mergeCell = mergeCells[i];
188 firstCell.append(...mergeCell.getChildren());
192 firstCell.setColSpan(newWidth);
193 firstCell.setRowSpan(newHeight);
196 export function $getTableRowsFromSelection(selection: BaseSelection|null): CustomTableRowNode[] {
197 const cells = $getTableCellsFromSelection(selection);
198 const rowsByKey: Record<string, CustomTableRowNode> = {};
199 for (const cell of cells) {
200 const row = cell.getParent();
201 if ($isCustomTableRowNode(row)) {
202 rowsByKey[row.getKey()] = row;
206 return Object.values(rowsByKey);
209 export function $getTableFromSelection(selection: BaseSelection|null): CustomTableNode|null {
210 const cells = $getTableCellsFromSelection(selection);
211 if (cells.length === 0) {
215 const table = $getParentOfType(cells[0], $isCustomTableNode);
216 if ($isCustomTableNode(table)) {
223 export function $clearTableSizes(table: CustomTableNode): void {
224 table.setColWidths([]);
226 // TODO - Extra form things once table properties and extra things
229 for (const row of table.getChildren()) {
230 if (!$isCustomTableRowNode(row)) {
234 const rowStyles = row.getStyles();
235 rowStyles.delete('height');
236 rowStyles.delete('width');
237 row.setStyles(rowStyles);
239 const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
240 for (const cell of cells) {
241 const cellStyles = cell.getStyles();
242 cellStyles.delete('height');
243 cellStyles.delete('width');
244 cell.setStyles(cellStyles);
250 export function $clearTableFormatting(table: CustomTableNode): void {
251 table.setColWidths([]);
252 table.setStyles(new Map);
254 for (const row of table.getChildren()) {
255 if (!$isCustomTableRowNode(row)) {
259 row.setStyles(new Map);
262 const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
263 for (const cell of cells) {
264 cell.setStyles(new Map);
272 * Perform the given callback for each cell in the given table.
273 * Returning false from the callback stops the function early.
275 export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTableCellNode) => void|false): void {
276 outer: for (const row of table.getChildren()) {
277 if (!$isCustomTableRowNode(row)) {
280 const cells = row.getChildren();
281 for (const cell of cells) {
282 if (!$isCustomTableCellNode(cell)) {
285 const result = callback(cell);
286 if (result === false) {
293 export function $getCellPaddingForTable(table: CustomTableNode): string {
294 let padding: string|null = null;
296 $forEachTableCell(table, (cell: CustomTableCellNode) => {
297 const cellPadding = cell.getStyles().get('padding') || ''
298 if (padding === null) {
299 padding = cellPadding;
302 if (cellPadding !== padding) {
308 return padding || '';