1 import {BaseSelection, LexicalEditor} from "lexical";
6 $isTableSelection, TableCellNode, TableNode,
9 } from "@lexical/table";
10 import {$getParentOfType} from "./nodes";
11 import {$getNodeFromSelection} from "./selection";
12 import {formatSizeValue} from "./dom";
13 import {TableMap} from "./table-map";
15 function $getTableFromCell(cell: TableCellNode): TableNode|null {
16 return $getParentOfType(cell, $isTableNode) as TableNode|null;
19 export function getTableColumnWidths(table: HTMLTableElement): string[] {
20 const maxColRow = getMaxColRowFromTable(table);
22 const colGroup = table.querySelector('colgroup');
23 let widths: string[] = [];
24 if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
25 widths = extractWidthsFromRow(colGroup);
27 if (widths.filter(Boolean).length === 0 && maxColRow) {
28 widths = extractWidthsFromRow(maxColRow);
34 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
35 const rows = table.querySelectorAll('tr');
36 let maxColCount: number = 0;
37 let maxColRow: HTMLTableRowElement | null = null;
39 for (const row of rows) {
40 if (row.childElementCount > maxColCount) {
42 maxColCount = row.childElementCount;
49 function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
50 return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
53 function extractWidthFromElement(element: HTMLElement): string {
54 let width = element.style.width || element.getAttribute('width');
55 if (width && !Number.isNaN(Number(width))) {
62 export function $setTableColumnWidth(node: TableNode, columnIndex: number, width: number|string): void {
63 const rows = node.getChildren() as TableRowNode[];
65 for (const row of rows) {
66 const cellCount = row.getChildren().length;
67 if (cellCount > maxCols) {
72 let colWidths = node.getColWidths();
73 if (colWidths.length === 0 || colWidths.length < maxCols) {
74 colWidths = Array(maxCols).fill('');
77 if (columnIndex + 1 > colWidths.length) {
78 console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
81 colWidths[columnIndex] = formatSizeValue(width);
82 node.setColWidths(colWidths);
85 export function $getTableColumnWidth(editor: LexicalEditor, node: TableNode, columnIndex: number): number {
86 const colWidths = node.getColWidths();
87 if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
88 return Number(colWidths[columnIndex].replace('px', ''));
91 // Otherwise, get from table element
92 const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
94 const maxColRow = getMaxColRowFromTable(table);
95 if (maxColRow && maxColRow.children.length > columnIndex) {
96 const cell = maxColRow.children[columnIndex];
97 return cell.clientWidth;
104 function $getCellColumnIndex(node: TableCellNode): number {
105 const row = node.getParent();
106 if (!$isTableRowNode(row)) {
111 const cells = row.getChildren<TableCellNode>();
112 for (const cell of cells) {
113 let colSpan = cell.getColSpan() || 1;
115 if (cell.getKey() === node.getKey()) {
123 export function $setTableCellColumnWidth(cell: TableCellNode, width: string): void {
124 const table = $getTableFromCell(cell)
125 const index = $getCellColumnIndex(cell);
127 if (table && index >= 0) {
128 $setTableColumnWidth(table, index, width);
132 export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellNode): string {
133 const table = $getTableFromCell(cell)
134 const index = $getCellColumnIndex(cell);
139 const widths = table.getColWidths();
140 return (widths.length > index) ? widths[index] : '';
143 export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
144 if ($isTableSelection(selection)) {
145 const nodes = selection.getNodes();
146 return nodes.filter(n => $isTableCellNode(n));
149 const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode;
150 return cell ? [cell] : [];
153 export function $mergeTableCellsInSelection(selection: TableSelection): void {
154 const selectionShape = selection.getShape();
155 const cells = $getTableCellsFromSelection(selection);
156 if (cells.length === 0) {
160 const table = $getTableFromCell(cells[0]);
165 const tableMap = new TableMap(table);
166 const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
171 // We have to adjust the shape since it won't take into account spans for the head corner position.
172 const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
173 const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
175 const mergeCells = tableMap.getCellsInRange({
176 fromX: selectionShape.fromX,
177 fromY: selectionShape.fromY,
182 if (mergeCells.length === 0) {
186 const firstCell = mergeCells[0];
187 const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
188 const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
190 for (let i = 1; i < mergeCells.length; i++) {
191 const mergeCell = mergeCells[i];
192 firstCell.append(...mergeCell.getChildren());
196 firstCell.setColSpan(newWidth);
197 firstCell.setRowSpan(newHeight);
200 export function $getTableRowsFromSelection(selection: BaseSelection|null): TableRowNode[] {
201 const cells = $getTableCellsFromSelection(selection);
202 const rowsByKey: Record<string, TableRowNode> = {};
203 for (const cell of cells) {
204 const row = cell.getParent();
205 if ($isTableRowNode(row)) {
206 rowsByKey[row.getKey()] = row;
210 return Object.values(rowsByKey);
213 export function $getTableFromSelection(selection: BaseSelection|null): TableNode|null {
214 const cells = $getTableCellsFromSelection(selection);
215 if (cells.length === 0) {
219 const table = $getParentOfType(cells[0], $isTableNode);
220 if ($isTableNode(table)) {
227 export function $clearTableSizes(table: TableNode): void {
228 table.setColWidths([]);
230 // TODO - Extra form things once table properties and extra things
233 for (const row of table.getChildren()) {
234 if (!$isTableRowNode(row)) {
238 const rowStyles = row.getStyles();
239 rowStyles.delete('height');
240 rowStyles.delete('width');
241 row.setStyles(rowStyles);
243 const cells = row.getChildren().filter(c => $isTableCellNode(c));
244 for (const cell of cells) {
245 const cellStyles = cell.getStyles();
246 cellStyles.delete('height');
247 cellStyles.delete('width');
248 cell.setStyles(cellStyles);
254 export function $clearTableFormatting(table: TableNode): void {
255 table.setColWidths([]);
256 table.setStyles(new Map);
258 for (const row of table.getChildren()) {
259 if (!$isTableRowNode(row)) {
263 row.setStyles(new Map);
265 const cells = row.getChildren().filter(c => $isTableCellNode(c));
266 for (const cell of cells) {
267 cell.setStyles(new Map);
274 * Perform the given callback for each cell in the given table.
275 * Returning false from the callback stops the function early.
277 export function $forEachTableCell(table: TableNode, callback: (c: TableCellNode) => void|false): void {
278 outer: for (const row of table.getChildren()) {
279 if (!$isTableRowNode(row)) {
282 const cells = row.getChildren();
283 for (const cell of cells) {
284 if (!$isTableCellNode(cell)) {
287 const result = callback(cell);
288 if (result === false) {
295 export function $getCellPaddingForTable(table: TableNode): string {
296 let padding: string|null = null;
298 $forEachTableCell(table, (cell: TableCellNode) => {
299 const cellPadding = cell.getStyles().get('padding') || ''
300 if (padding === null) {
301 padding = cellPadding;
304 if (cellPadding !== padding) {
310 return padding || '';