From ec678ed495e8e0f20d96b6af3206521626013263 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sun, 28 Jun 2026 10:30:00 +0200 Subject: [PATCH] feat: table cell splitting/merging --- CHANGELOG.md | 4 +- src/lib/components/Editor.svelte | 29 ++- src/lib/components/TableSplitDialog.svelte | 152 ++++++++++++++ src/lib/components/TableToolbar.svelte | 42 +++- src/lib/editor/extensions.ts | 3 + src/lib/editor/extensions/tableSplit.ts | 230 +++++++++++++++++++++ src/lib/export/odt.ts | 12 +- tests/roundtrip.test.ts | 44 +++- tests/unit/table-split.test.ts | 95 +++++++++ 9 files changed, 603 insertions(+), 8 deletions(-) create mode 100644 src/lib/components/TableSplitDialog.svelte create mode 100644 src/lib/editor/extensions/tableSplit.ts create mode 100644 tests/unit/table-split.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de7b1a..100e208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ - Tab stops (real tab character) and manual line breaks (Shift+Enter) **Insert** -- Tables: insert via size picker, Word-style row/column drag-resize, add / delete rows & columns, delete table, cell borders; a table splits cleanly across page boundaries +- Tables: insert via size picker, Word-style row/column drag-resize, add / delete rows & columns, delete table, cell borders, merge cells and split cells (N×M, Word/LibreOffice-style); a table splits cleanly across page boundaries - Images: inline or floating with text wrap (left / right / top-bottom), resize handles, rotation, live size badge; insert via toolbar, drag-and-drop, or paste - Special characters picker - Hyperlinks: create / edit / remove (toolbar + Ctrl+K), Ctrl/Cmd+click to open, hover hint showing the URL; ODF `text:a` round-trip @@ -65,7 +65,7 @@ ### Not yet implemented Planned word-processor features (to match Word/LibreOffice): -- Table cells: merge / split, cell background/shading, header-row toggle +- Table cells: cell background/shading, header-row toggle - Footnotes / endnotes (currently dropped on import) - Clear formatting (remove all marks) - Multi-column (newspaper) layout diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 4cad4d3..877025a 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -8,6 +8,7 @@ import { spellErrorAt } from '../editor/extensions/spellCheck'; import { spellController } from '../spell/controller'; import TableToolbar from './TableToolbar.svelte'; + import TableSplitDialog from './TableSplitDialog.svelte'; import ImageToolbar from './ImageToolbar.svelte'; import type { WrapMode } from '../editor/extensions/image'; import { NodeSelection } from '@tiptap/pm/state'; @@ -179,6 +180,12 @@ // Shown when the selection is inside a table; positioned just above that table. let tableUi = $state<{ visible: boolean; top: number; left: number }>({ visible: false, top: 0, left: 0 }); let tableUiRaf = 0; + // Word-style "Split Cells…" popover, opened from the table toolbar. + let splitDialogOpen = $state(false); + // Drop the dialog if the selection leaves the table (toolbar hidden). + $effect(() => { + if (!tableUi.visible && splitDialogOpen) splitDialogOpen = false; + }); // The DOM element of the table containing the current selection, or null. // nodeDOM(before(table)) returns the wrapper div the table node view renders. @@ -628,8 +635,26 @@ /> - {#if tableUi.visible} - + {#if tableUi.visible && !splitDialogOpen} + (splitDialogOpen = true)} + /> + {/if} + {#if splitDialogOpen && tableUi.visible} + { + editor?.chain().focus().splitCellInto(cols, rows).run(); + splitDialogOpen = false; + }} + onClose={() => (splitDialogOpen = false)} + /> {/if} {#if imageUi.visible} diff --git a/src/lib/components/TableSplitDialog.svelte b/src/lib/components/TableSplitDialog.svelte new file mode 100644 index 0000000..f5822e3 --- /dev/null +++ b/src/lib/components/TableSplitDialog.svelte @@ -0,0 +1,152 @@ + + +{#if open} + +{/if} + + diff --git a/src/lib/components/TableToolbar.svelte b/src/lib/components/TableToolbar.svelte index aa01c86..3eafb45 100644 --- a/src/lib/components/TableToolbar.svelte +++ b/src/lib/components/TableToolbar.svelte @@ -5,10 +5,14 @@ editor, top, left, + tick, + onSplit, }: { editor: Editor | null; top: number; left: number; + tick: number; + onSplit: () => void; } = $props(); // Run a table command without disturbing the cell selection: preventDefault on @@ -18,6 +22,9 @@ if (!editor) return; cmd(editor.chain().focus()).run(); } + + // Merge needs a multi-cell selection; re-evaluated per transaction via `tick`. + const canMerge = $derived(tick >= 0 && !!editor && editor.can().mergeCells());
+ + + + + +