diff --git a/resources/js/wysiwyg/nodes/custom-table-cell.ts b/resources/js/wysiwyg/nodes/custom-table-cell.ts index 15c305dcb..793302cfe 100644 --- a/resources/js/wysiwyg/nodes/custom-table-cell.ts +++ b/resources/js/wysiwyg/nodes/custom-table-cell.ts @@ -235,7 +235,7 @@ export function $convertTableCellNodeElement( export function $createCustomTableCellNode( - headerState: TableCellHeaderState, + headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width?: number, ): CustomTableCellNode { diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index f339a6ed4..dcc866888 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -2,7 +2,7 @@ ## In progress -// +- Table Cut/Copy/Paste column ## Main Todo @@ -10,7 +10,6 @@ - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts) - Media resize support (like images) - Table caption text support -- Table Cut/Copy/Paste column - Mac: Shortcut support via command. ## Secondary Todo diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts index 6242f0b1d..1a9ffb0d3 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -27,8 +27,12 @@ import { $getTableRowsFromSelection, $mergeTableCellsInSelection } from "../../../utils/tables"; -import {$isCustomTableRowNode, CustomTableRowNode} from "../../../nodes/custom-table-row"; -import {NodeClipboard} from "../../../services/node-clipboard"; +import {$isCustomTableRowNode} from "../../../nodes/custom-table-row"; +import { + $copySelectedRowsToClipboard, + $cutSelectedRowsToClipboard, + $pasteClipboardRowsBefore, $pasteRowsAfter, isRowClipboardEmpty +} from "../../../utils/table-copy-paste"; const neverActive = (): boolean => false; const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode); @@ -168,17 +172,15 @@ export const rowProperties: EditorButtonDefinition = { isDisabled: cellNotSelected, }; -const rowClipboard: NodeClipboard = new NodeClipboard(CustomTableRowNode); - export const cutRow: EditorButtonDefinition = { label: 'Cut row', format: 'long', action(context: EditorUiContext) { context.editor.update(() => { - const rows = $getTableRowsFromSelection($getSelection()); - rowClipboard.set(...rows); - for (const row of rows) { - row.remove(); + try { + $cutSelectedRowsToClipboard(); + } catch (e: any) { + context.error(e.toString()); } }); }, @@ -191,8 +193,11 @@ export const copyRow: EditorButtonDefinition = { format: 'long', action(context: EditorUiContext) { context.editor.getEditorState().read(() => { - const rows = $getTableRowsFromSelection($getSelection()); - rowClipboard.set(...rows); + try { + $copySelectedRowsToClipboard(); + } catch (e: any) { + context.error(e.toString()); + } }); }, isActive: neverActive, @@ -204,17 +209,15 @@ export const pasteRowBefore: EditorButtonDefinition = { format: 'long', action(context: EditorUiContext) { 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); - } + try { + $pasteClipboardRowsBefore(context.editor); + } catch (e: any) { + context.error(e.toString()); } }); }, isActive: neverActive, - isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0, + isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(), }; export const pasteRowAfter: EditorButtonDefinition = { @@ -222,17 +225,15 @@ export const pasteRowAfter: EditorButtonDefinition = { format: 'long', action(context: EditorUiContext) { 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); - } + try { + $pasteRowsAfter(context.editor); + } catch (e: any) { + context.error(e.toString()); } }); }, isActive: neverActive, - isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0, + isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(), }; export const cutColumn: EditorButtonDefinition = { diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index b6fe52dcd..a04f3c74a 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -14,6 +14,7 @@ export type EditorUiContext = { containerDOM: HTMLElement; // DOM element which contains all editor elements scrollDOM: HTMLElement; // DOM element which is the main content scroll container translate: (text: string) => string; // Translate function + error: (text: string) => void; // Error reporting function manager: EditorUIManager; // UI Manager instance for this editor options: Record; // General user options which may be used by sub elements }; diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 116d6e1fc..bfa76bb82 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -20,7 +20,10 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro editorDOM: element, scrollDOM: scrollContainer, manager, - translate: (text: string): string => text, + translate: (text: string): string => text, // TODO - Implement + error(error: string): void { + window.$events.error(error); // TODO - Translate + }, options, }; manager.setContext(context); diff --git a/resources/js/wysiwyg/services/node-clipboard.ts b/resources/js/wysiwyg/utils/node-clipboard.ts similarity index 96% rename from resources/js/wysiwyg/services/node-clipboard.ts rename to resources/js/wysiwyg/utils/node-clipboard.ts index 7d880db98..385c4c46c 100644 --- a/resources/js/wysiwyg/services/node-clipboard.ts +++ b/resources/js/wysiwyg/utils/node-clipboard.ts @@ -44,10 +44,10 @@ export class NodeClipboard { } } - get(editor: LexicalEditor): LexicalNode[] { + get(editor: LexicalEditor): T[] { return this.store.map(json => unserializeNodeRecursive(editor, json)).filter((node) => { return node !== null; - }); + }) as T[]; } size(): number { diff --git a/resources/js/wysiwyg/utils/table-copy-paste.ts b/resources/js/wysiwyg/utils/table-copy-paste.ts new file mode 100644 index 000000000..ae8ef3d35 --- /dev/null +++ b/resources/js/wysiwyg/utils/table-copy-paste.ts @@ -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 = new NodeClipboard(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); + } + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/table-map.ts b/resources/js/wysiwyg/utils/table-map.ts index 2b7eba62c..bc9721d96 100644 --- a/resources/js/wysiwyg/utils/table-map.ts +++ b/resources/js/wysiwyg/utils/table-map.ts @@ -93,4 +93,4 @@ export class TableMap { return [...cells.values()]; } -} +} \ No newline at end of file