1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-10-29 23:22:34 +01:00

Lexical: Kinda made row copy/paste work

This commit is contained in:
Dan Brown 2024-08-09 21:58:45 +01:00
parent db4208a7eb
commit abbfd42a6c
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 94 additions and 13 deletions

View File

@ -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;
}
}

View File

@ -7,6 +7,7 @@
- Caption text support
- Resize to contents button
- Remove formatting button
- Cut/Copy/Paste column
## Main Todo
@ -33,3 +34,5 @@
- 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).
- Table row copy/paste does not handle merged cells
- TinyMCE fills gaps with the cells that would be visually in the row

View 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 = {