--- /dev/null
+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
- Caption text support
- Resize to contents button
- Remove formatting button
+ - Cut/Copy/Paste column
## Main Todo
- 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
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,
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);
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,
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- // TODO
+ const rows = $getTableRowsFromSelection($getSelection());
+ rowClipboard.set(...rows);
});
},
isActive: neverActive,
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 = {