Lexical: Kinda made row copy/paste work
This commit is contained in:
parent
db4208a7eb
commit
abbfd42a6c
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
- Caption text support
|
- Caption text support
|
||||||
- Resize to contents button
|
- Resize to contents button
|
||||||
- Remove formatting button
|
- Remove formatting button
|
||||||
|
- Cut/Copy/Paste column
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
|
@ -32,4 +33,6 @@
|
||||||
- Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
|
- 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
|
- 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.
|
- `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).
|
- 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
|
|
@ -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 insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
||||||
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$getSelection, BaseSelection} from "lexical";
|
import {$createNodeSelection, $createRangeSelection, $getSelection, BaseSelection} from "lexical";
|
||||||
import {$isCustomTableNode} from "../../../nodes/custom-table";
|
import {$isCustomTableNode} from "../../../nodes/custom-table";
|
||||||
import {
|
import {
|
||||||
$deleteTableColumn__EXPERIMENTAL,
|
$deleteTableColumn__EXPERIMENTAL,
|
||||||
|
@ -21,8 +21,11 @@ import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/
|
||||||
import {$getParentOfType} from "../../../utils/nodes";
|
import {$getParentOfType} from "../../../utils/nodes";
|
||||||
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
|
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
|
||||||
import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
|
import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
|
||||||
import {$mergeTableCellsInSelection} from "../../../utils/tables";
|
import {$getTableRowsFromSelection, $mergeTableCellsInSelection} from "../../../utils/tables";
|
||||||
import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
|
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 neverActive = (): boolean => false;
|
||||||
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
||||||
|
@ -177,12 +180,18 @@ export const rowProperties: EditorButtonDefinition = {
|
||||||
isDisabled: cellNotSelected,
|
isDisabled: cellNotSelected,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
|
||||||
|
|
||||||
export const cutRow: EditorButtonDefinition = {
|
export const cutRow: EditorButtonDefinition = {
|
||||||
label: 'Cut row',
|
label: 'Cut row',
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.update(() => {
|
||||||
// TODO
|
const rows = $getTableRowsFromSelection($getSelection());
|
||||||
|
rowClipboard.set(...rows);
|
||||||
|
for (const row of rows) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isActive: neverActive,
|
isActive: neverActive,
|
||||||
|
@ -194,7 +203,8 @@ export const copyRow: EditorButtonDefinition = {
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
// TODO
|
const rows = $getTableRowsFromSelection($getSelection());
|
||||||
|
rowClipboard.set(...rows);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isActive: neverActive,
|
isActive: neverActive,
|
||||||
|
@ -205,24 +215,36 @@ export const pasteRowBefore: EditorButtonDefinition = {
|
||||||
label: 'Paste row before',
|
label: 'Paste row before',
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.update(() => {
|
||||||
// TODO
|
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,
|
isActive: neverActive,
|
||||||
isDisabled: cellNotSelected,
|
isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pasteRowAfter: EditorButtonDefinition = {
|
export const pasteRowAfter: EditorButtonDefinition = {
|
||||||
label: 'Paste row after',
|
label: 'Paste row after',
|
||||||
format: 'long',
|
format: 'long',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.update(() => {
|
||||||
// TODO
|
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,
|
isActive: neverActive,
|
||||||
isDisabled: cellNotSelected,
|
isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cutColumn: EditorButtonDefinition = {
|
export const cutColumn: EditorButtonDefinition = {
|
||||||
|
|
Loading…
Reference in New Issue