]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Kinda made row copy/paste work
authorDan Brown <redacted>
Fri, 9 Aug 2024 20:58:45 +0000 (21:58 +0100)
committerDan Brown <redacted>
Fri, 9 Aug 2024 20:58:45 +0000 (21:58 +0100)
resources/js/wysiwyg/services/node-clipboard.ts [new file with mode: 0644]
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/buttons/tables.ts

diff --git a/resources/js/wysiwyg/services/node-clipboard.ts b/resources/js/wysiwyg/services/node-clipboard.ts
new file mode 100644 (file)
index 0000000..7d880db
--- /dev/null
@@ -0,0 +1,56 @@
+import {$isElementNode, LexicalEditor, LexicalNode, SerializedLexicalNode} from "lexical";
+
+type SerializedLexicalNodeWithChildren = {
+    node: SerializedLexicalNode,
+    children: SerializedLexicalNodeWithChildren[],
+};
+
+function serializeNodeRecursive(node: LexicalNode): SerializedLexicalNodeWithChildren {
+    const childNodes = $isElementNode(node) ? node.getChildren() : [];
+    return {
+        node: node.exportJSON(),
+        children: childNodes.map(n => serializeNodeRecursive(n)),
+    };
+}
+
+function unserializeNodeRecursive(editor: LexicalEditor, {node, children}: SerializedLexicalNodeWithChildren): LexicalNode|null {
+    const instance = editor._nodes.get(node.type)?.klass.importJSON(node);
+    if (!instance) {
+        return null;
+    }
+
+    const childNodes = children.map(child => unserializeNodeRecursive(editor, child));
+    for (const child of childNodes) {
+        if (child && $isElementNode(instance)) {
+            instance.append(child);
+        }
+    }
+
+    return instance;
+}
+
+export class NodeClipboard<T extends LexicalNode> {
+    nodeClass: {importJSON: (s: SerializedLexicalNode) => T};
+    protected store: SerializedLexicalNodeWithChildren[] = [];
+
+    constructor(nodeClass: {importJSON: (s: any) => T}) {
+        this.nodeClass = nodeClass;
+    }
+
+    set(...nodes: LexicalNode[]): void {
+        this.store.splice(0, this.store.length);
+        for (const node of nodes) {
+            this.store.push(serializeNodeRecursive(node));
+        }
+    }
+
+    get(editor: LexicalEditor): LexicalNode[] {
+        return this.store.map(json => unserializeNodeRecursive(editor, json)).filter((node) => {
+            return node !== null;
+        });
+    }
+
+    size(): number {
+        return this.store.length;
+    }
+}
\ No newline at end of file
index cf24ad677f63df42c9283d8e1115ec09902f0fa5..b6325688e582eaf36d3913ef9088ffed8bb28334 100644 (file)
@@ -7,6 +7,7 @@
     - Caption text support 
   - Resize to contents button
   - Remove formatting button
+  - Cut/Copy/Paste column
 
 ## Main Todo
 
@@ -32,4 +33,6 @@
 - Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
 - Removing link around image via button deletes image, not just link 
 - `SELECTION_CHANGE_COMMAND` not fired when clicking out of a table cell. Prevents toolbar hiding on table unselect.
-- Template drag/drop not handled when outside core editor area (ignored in margin area).
\ No newline at end of file
+- Template drag/drop not handled when outside core editor area (ignored in margin area).
+- Table row copy/paste does not handle merged cells
+  - TinyMCE fills gaps with the  cells that would be visually in the row
\ No newline at end of file
index 50353961f0ff47933a6e3d0da3ec7dad0851209e..c98f6c02f37f091f28c8a6adc42470752faf4a3d 100644 (file)
@@ -8,7 +8,7 @@ import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg
 import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
 import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
 import {EditorUiContext} from "../../framework/core";
-import {$getSelection, BaseSelection} from "lexical";
+import {$createNodeSelection, $createRangeSelection, $getSelection, BaseSelection} from "lexical";
 import {$isCustomTableNode} from "../../../nodes/custom-table";
 import {
     $deleteTableColumn__EXPERIMENTAL,
@@ -21,8 +21,11 @@ import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/
 import {$getParentOfType} from "../../../utils/nodes";
 import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
 import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
-import {$mergeTableCellsInSelection} from "../../../utils/tables";
-import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
+import {$getTableRowsFromSelection, $mergeTableCellsInSelection} from "../../../utils/tables";
+import {$isCustomTableRowNode, CustomTableRowNode} from "../../../nodes/custom-table-row";
+import {NodeClipboard} from "../../../services/node-clipboard";
+import {r} from "@codemirror/legacy-modes/mode/r";
+import {$generateHtmlFromNodes} from "@lexical/html";
 
 const neverActive = (): boolean => false;
 const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
@@ -177,12 +180,18 @@ export const rowProperties: EditorButtonDefinition = {
     isDisabled: cellNotSelected,
 };
 
+const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
+
 export const cutRow: EditorButtonDefinition = {
     label: 'Cut row',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            const rows = $getTableRowsFromSelection($getSelection());
+            rowClipboard.set(...rows);
+            for (const row of rows) {
+                row.remove();
+            }
         });
     },
     isActive: neverActive,
@@ -194,7 +203,8 @@ export const copyRow: EditorButtonDefinition = {
     format: 'long',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            // TODO
+            const rows = $getTableRowsFromSelection($getSelection());
+            rowClipboard.set(...rows);
         });
     },
     isActive: neverActive,
@@ -205,24 +215,36 @@ export const pasteRowBefore: EditorButtonDefinition = {
     label: 'Paste row before',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            const rows = $getTableRowsFromSelection($getSelection());
+            const lastRow = rows[rows.length - 1];
+            if (lastRow) {
+                for (const row of rowClipboard.get(context.editor)) {
+                    lastRow.insertBefore(row);
+                }
+            }
         });
     },
     isActive: neverActive,
-    isDisabled: cellNotSelected,
+    isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
 };
 
 export const pasteRowAfter: EditorButtonDefinition = {
     label: 'Paste row after',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            const rows = $getTableRowsFromSelection($getSelection());
+            const lastRow = rows[rows.length - 1];
+            if (lastRow) {
+                for (const row of rowClipboard.get(context.editor).reverse()) {
+                    lastRow.insertAfter(row);
+                }
+            }
         });
     },
     isActive: neverActive,
-    isDisabled: cellNotSelected,
+    isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
 };
 
 export const cutColumn: EditorButtonDefinition = {