]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Finished off core cell properties functionality
authorDan Brown <redacted>
Mon, 5 Aug 2024 17:49:17 +0000 (18:49 +0100)
committerDan Brown <redacted>
Mon, 5 Aug 2024 17:49:17 +0000 (18:49 +0100)
resources/js/wysiwyg/nodes/custom-table.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/forms/tables.ts
resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts
resources/js/wysiwyg/utils/dom.ts
resources/js/wysiwyg/utils/tables.ts [new file with mode: 0644]

index 32f3ec4fa571a851d5ab047581bdd745e39d7c77..99351d8527a4ca53c9f810a3bcaa68e5f2a8086d 100644 (file)
@@ -1,8 +1,9 @@
-import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table";
-import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalEditor, LexicalNode, Spread} from "lexical";
+import {SerializedTableNode, TableNode} from "@lexical/table";
+import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical";
 import {EditorConfig} from "lexical/LexicalEditor";
 
 import {el} from "../utils/dom";
+import {getTableColumnWidths} from "../utils/tables";
 
 export type SerializedCustomTableNode = Spread<{
     id: string;
@@ -111,49 +112,6 @@ export class CustomTableNode extends TableNode {
     }
 }
 
-function getTableColumnWidths(table: HTMLTableElement): string[] {
-    const maxColRow = getMaxColRowFromTable(table);
-
-    const colGroup = table.querySelector('colgroup');
-    let widths: string[] = [];
-    if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
-        widths = extractWidthsFromRow(colGroup);
-    }
-    if (widths.filter(Boolean).length === 0 && maxColRow) {
-        widths = extractWidthsFromRow(maxColRow);
-    }
-
-    return widths;
-}
-
-function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement|null {
-    const rows = table.querySelectorAll('tr');
-    let maxColCount: number = 0;
-    let maxColRow: HTMLTableRowElement|null = null;
-
-    for (const row of rows) {
-        if (row.childElementCount > maxColCount) {
-            maxColRow = row;
-            maxColCount = row.childElementCount;
-        }
-    }
-
-    return maxColRow;
-}
-
-function extractWidthsFromRow(row: HTMLTableRowElement|HTMLTableColElement) {
-    return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
-}
-
-function extractWidthFromElement(element: HTMLElement): string {
-    let width = element.style.width || element.getAttribute('width');
-    if (width && !Number.isNaN(Number(width))) {
-        width = width + 'px';
-    }
-
-    return width || '';
-}
-
 export function $createCustomTableNode(): CustomTableNode {
     return new CustomTableNode();
 }
@@ -161,45 +119,3 @@ export function $createCustomTableNode(): CustomTableNode {
 export function $isCustomTableNode(node: LexicalNode | null | undefined): node is CustomTableNode {
     return node instanceof CustomTableNode;
 }
-
-export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number): void {
-    const rows = node.getChildren() as TableRowNode[];
-    let maxCols = 0;
-    for (const row of rows) {
-        const cellCount = row.getChildren().length;
-        if (cellCount > maxCols) {
-            maxCols = cellCount;
-        }
-    }
-
-    let colWidths = node.getColWidths();
-    if (colWidths.length === 0 || colWidths.length < maxCols) {
-        colWidths = Array(maxCols).fill('');
-    }
-
-    if (columnIndex + 1 > colWidths.length) {
-        console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
-    }
-
-    colWidths[columnIndex] = width + 'px';
-    node.setColWidths(colWidths);
-}
-
-export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
-    const colWidths = node.getColWidths();
-    if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
-        return Number(colWidths[columnIndex].replace('px', ''));
-    }
-
-    // Otherwise, get from table element
-    const table = editor.getElementByKey(node.__key) as HTMLTableElement|null;
-    if (table) {
-        const maxColRow = getMaxColRowFromTable(table);
-        if (maxColRow && maxColRow.children.length > columnIndex) {
-            const cell = maxColRow.children[columnIndex];
-            return cell.clientWidth;
-        }
-    }
-
-    return 0;
-}
\ No newline at end of file
index d925711e17b5692a063c8942ec7d92839e1c73f4..086ca14622d5e8ef88a2c01a788caee8c3220afa 100644 (file)
@@ -3,7 +3,7 @@
 ## In progress
 
 - Table features
-  - Cell properties form logic
+  - CustomTableCellNode importDOM logic
   - Merge cell action
   - Row properties form logic
   - Table properties form logic
index 291b355e7084d0b8bc97c639cfd07931500cde9e..1d637b0eea7e7928f4d34d01a597381473ebf717 100644 (file)
@@ -5,11 +5,11 @@ import {
     EditorSelectFormFieldDefinition
 } from "../../framework/forms";
 import {EditorUiContext} from "../../framework/core";
-import {$isCustomTableCellNode, CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
+import {CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
 import {EditorFormModal} from "../../framework/modals";
-import {$getNodeFromSelection} from "../../../utils/selection";
 import {$getSelection, ElementFormatType} from "lexical";
-import {TableCellHeaderStates} from "@lexical/table";
+import {$getTableCellsFromSelection, $setTableCellColumnWidth} from "../../../utils/tables";
+import {formatSizeValue} from "../../../utils/dom";
 
 const borderStyleInput: EditorSelectFormFieldDefinition = {
     label: 'Border style',
@@ -61,7 +61,7 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito
         width: '', // TODO
         height: styles.get('height') || '',
         type: cell.getTag(),
-        h_align: '', // TODO
+        h_align: cell.getFormatType(),
         v_align: styles.get('vertical-align') || '',
         border_width: styles.get('border-width') || '',
         border_style: styles.get('border-style') || '',
@@ -74,18 +74,19 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito
 export const cellProperties: EditorFormDefinition = {
     submitText: 'Save',
     async action(formData, context: EditorUiContext) {
-        // TODO - Set for cell selection range
         context.editor.update(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
-            if ($isCustomTableCellNode(cell)) {
-                // TODO - Set width
-                cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
+            const cells = $getTableCellsFromSelection($getSelection());
+            for (const cell of cells) {
+                const width = formData.get('width')?.toString() || '';
+
+                $setTableCellColumnWidth(cell, width);
                 cell.updateTag(formData.get('type')?.toString() || '');
+                cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
 
                 const styles = cell.getStyles();
-                styles.set('height', formData.get('height')?.toString() || '');
+                styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
                 styles.set('vertical-align', formData.get('v_align')?.toString() || '');
-                styles.set('border-width', formData.get('border_width')?.toString() || '');
+                styles.set('border-width', formatSizeValue(formData.get('border_width')?.toString() || ''));
                 styles.set('border-style', formData.get('border_style')?.toString() || '');
                 styles.set('border-color', formData.get('border_color')?.toString() || '');
                 styles.set('background-color', formData.get('background_color')?.toString() || '');
index f312294c59d45a7030524140b62b6f563bf22ee4..37f1b6f01ee8ea2feb3b7fc4da47b3bbcdf17786 100644 (file)
@@ -1,8 +1,9 @@
 import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
 import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
-import {$getTableColumnWidth, $setTableColumnWidth, CustomTableNode} from "../../../nodes/custom-table";
+import {CustomTableNode} from "../../../nodes/custom-table";
 import {TableRowNode} from "@lexical/table";
 import {el} from "../../../utils/dom";
+import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
 
 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
 
index dc0872e89c81c8c82d39f7f3f7ad8f5c7e1e3be8..7426ac5925290dd6d43c1d2863293194e5cd7a76 100644 (file)
@@ -21,4 +21,12 @@ export function el(tag: string, attrs: Record<string, string | null> = {}, child
 export function htmlToDom(html: string): Document {
     const parser = new DOMParser();
     return parser.parseFromString(html, 'text/html');
+}
+
+export function formatSizeValue(size: number | string, defaultSuffix: string = 'px'): string {
+    if (typeof size === 'number' || /^-?\d+$/.test(size)) {
+        return `${size}${defaultSuffix}`;
+    }
+
+    return size;
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts
new file mode 100644 (file)
index 0000000..959c8a4
--- /dev/null
@@ -0,0 +1,134 @@
+import {BaseSelection, LexicalEditor} from "lexical";
+import {$isTableRowNode, $isTableSelection, TableRowNode} from "@lexical/table";
+import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
+import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
+import {$getParentOfType} from "./nodes";
+import {$getNodeFromSelection} from "./selection";
+import {formatSizeValue} from "./dom";
+
+function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
+    return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
+}
+
+export function getTableColumnWidths(table: HTMLTableElement): string[] {
+    const maxColRow = getMaxColRowFromTable(table);
+
+    const colGroup = table.querySelector('colgroup');
+    let widths: string[] = [];
+    if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
+        widths = extractWidthsFromRow(colGroup);
+    }
+    if (widths.filter(Boolean).length === 0 && maxColRow) {
+        widths = extractWidthsFromRow(maxColRow);
+    }
+
+    return widths;
+}
+
+function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
+    const rows = table.querySelectorAll('tr');
+    let maxColCount: number = 0;
+    let maxColRow: HTMLTableRowElement | null = null;
+
+    for (const row of rows) {
+        if (row.childElementCount > maxColCount) {
+            maxColRow = row;
+            maxColCount = row.childElementCount;
+        }
+    }
+
+    return maxColRow;
+}
+
+function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
+    return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
+}
+
+function extractWidthFromElement(element: HTMLElement): string {
+    let width = element.style.width || element.getAttribute('width');
+    if (width && !Number.isNaN(Number(width))) {
+        width = width + 'px';
+    }
+
+    return width || '';
+}
+
+export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
+    const rows = node.getChildren() as TableRowNode[];
+    let maxCols = 0;
+    for (const row of rows) {
+        const cellCount = row.getChildren().length;
+        if (cellCount > maxCols) {
+            maxCols = cellCount;
+        }
+    }
+
+    let colWidths = node.getColWidths();
+    if (colWidths.length === 0 || colWidths.length < maxCols) {
+        colWidths = Array(maxCols).fill('');
+    }
+
+    if (columnIndex + 1 > colWidths.length) {
+        console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
+    }
+
+    colWidths[columnIndex] = formatSizeValue(width);
+    node.setColWidths(colWidths);
+}
+
+export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
+    const colWidths = node.getColWidths();
+    if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
+        return Number(colWidths[columnIndex].replace('px', ''));
+    }
+
+    // Otherwise, get from table element
+    const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
+    if (table) {
+        const maxColRow = getMaxColRowFromTable(table);
+        if (maxColRow && maxColRow.children.length > columnIndex) {
+            const cell = maxColRow.children[columnIndex];
+            return cell.clientWidth;
+        }
+    }
+
+    return 0;
+}
+
+function $getCellColumnIndex(node: CustomTableCellNode): number {
+    const row = node.getParent();
+    if (!$isTableRowNode(row)) {
+        return -1;
+    }
+
+    let index = 0;
+    const cells = row.getChildren<CustomTableCellNode>();
+    for (const cell of cells) {
+        let colSpan = cell.getColSpan() || 1;
+        index += colSpan;
+        if (cell.getKey() === node.getKey()) {
+            break;
+        }
+    }
+
+    return index - 1;
+}
+
+export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
+    const table = $getTableFromCell(cell)
+    const index = $getCellColumnIndex(cell);
+
+    if (table && index >= 0) {
+        $setTableColumnWidth(table, index, width);
+    }
+}
+
+export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[]  {
+    if ($isTableSelection(selection)) {
+        const nodes = selection.getNodes();
+        return nodes.filter(n => $isCustomTableCellNode(n));
+    }
+
+    const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
+    return cell ? [cell] : [];
+}
\ No newline at end of file