mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-01-03 23:42:28 +03:00
Lexical: Improved table row copy/paste
Added safeguarding/matching of source/target sizes to prevent broken tables.
This commit is contained in:
56
resources/js/wysiwyg/utils/node-clipboard.ts
Normal file
56
resources/js/wysiwyg/utils/node-clipboard.ts
Normal 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): T[] {
|
||||
return this.store.map(json => unserializeNodeRecursive(editor, json)).filter((node) => {
|
||||
return node !== null;
|
||||
}) as T[];
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.store.length;
|
||||
}
|
||||
}
|
||||
97
resources/js/wysiwyg/utils/table-copy-paste.ts
Normal file
97
resources/js/wysiwyg/utils/table-copy-paste.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {NodeClipboard} from "./node-clipboard";
|
||||
import {CustomTableRowNode} from "../nodes/custom-table-row";
|
||||
import {$getTableFromSelection, $getTableRowsFromSelection} from "./tables";
|
||||
import {$getSelection, LexicalEditor} from "lexical";
|
||||
import {$createCustomTableCellNode, $isCustomTableCellNode} from "../nodes/custom-table-cell";
|
||||
import {CustomTableNode} from "../nodes/custom-table";
|
||||
import {TableMap} from "./table-map";
|
||||
|
||||
const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
|
||||
|
||||
export function isRowClipboardEmpty(): boolean {
|
||||
return rowClipboard.size() === 0;
|
||||
}
|
||||
|
||||
export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
|
||||
let commonRowSize: number|null = null;
|
||||
|
||||
for (const row of rows) {
|
||||
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
|
||||
let rowSize = 0;
|
||||
for (const cell of cells) {
|
||||
rowSize += cell.getColSpan() || 1;
|
||||
if (cell.getRowSpan() > 1) {
|
||||
throw Error('Cannot copy rows with merged cells');
|
||||
}
|
||||
}
|
||||
|
||||
if (commonRowSize === null) {
|
||||
commonRowSize = rowSize;
|
||||
} else if (commonRowSize !== rowSize) {
|
||||
throw Error('Cannot copy rows with inconsistent sizes');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void {
|
||||
const tableColCount = (new TableMap(targetTable)).columnCount;
|
||||
for (const row of rows) {
|
||||
const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
|
||||
let rowSize = 0;
|
||||
for (const cell of cells) {
|
||||
rowSize += cell.getColSpan() || 1;
|
||||
}
|
||||
|
||||
if (rowSize > tableColCount) {
|
||||
throw Error('Cannot paste rows that are wider than target table');
|
||||
}
|
||||
|
||||
while (rowSize < tableColCount) {
|
||||
row.append($createCustomTableCellNode());
|
||||
rowSize++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function $cutSelectedRowsToClipboard(): void {
|
||||
const rows = $getTableRowsFromSelection($getSelection());
|
||||
validateRowsToCopy(rows);
|
||||
rowClipboard.set(...rows);
|
||||
for (const row of rows) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function $copySelectedRowsToClipboard(): void {
|
||||
const rows = $getTableRowsFromSelection($getSelection());
|
||||
validateRowsToCopy(rows);
|
||||
rowClipboard.set(...rows);
|
||||
}
|
||||
|
||||
export function $pasteClipboardRowsBefore(editor: LexicalEditor): void {
|
||||
const selection = $getSelection();
|
||||
const rows = $getTableRowsFromSelection(selection);
|
||||
const table = $getTableFromSelection(selection);
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (lastRow && table) {
|
||||
const clipboardRows = rowClipboard.get(editor);
|
||||
validateRowsToPaste(clipboardRows, table);
|
||||
for (const row of clipboardRows) {
|
||||
lastRow.insertBefore(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function $pasteRowsAfter(editor: LexicalEditor): void {
|
||||
const selection = $getSelection();
|
||||
const rows = $getTableRowsFromSelection(selection);
|
||||
const table = $getTableFromSelection(selection);
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (lastRow && table) {
|
||||
const clipboardRows = rowClipboard.get(editor).reverse();
|
||||
validateRowsToPaste(clipboardRows, table);
|
||||
for (const row of clipboardRows) {
|
||||
lastRow.insertAfter(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,4 +93,4 @@ export class TableMap {
|
||||
|
||||
return [...cells.values()];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user