Skip to content

Iteration/table ux - #16

Merged
sumbad merged 18 commits into
masterfrom
iteration/table-ux
Oct 23, 2026
Merged

sumbad merged 18 commits into
masterfrom
iteration/table-ux

Conversation

@sumbad

@sumbad sumbad commented Oct 23, 2026

Copy link
Copy Markdown
Member

No description provided.

sumbad added 17 commits October 22, 2026 20:13
Cell/row copy, cut and paste between tables per the owner decisions
(issue table-clipboard):

- paste of a copied cell block onto a cell selection replaces the
  rectangle's content; target cell attributes (fill, widths, borders,
  spans) are preserved, the grid tiles Excel-style over the selection
- paste at a caret inserts copied rows after the caret row, aligned to
  the target column count (extra source columns dropped, missing ones
  become empty cells); a single copied cell replaces the caret cell
- Cmd+X on a full-row selection removes those rows (Excel semantics),
  on a partial rectangle it clears the contents; the block is written
  to the system clipboard first
- a cell drag ends with the editor focused, so the clipboard shortcuts
  reach the editor at all (the circle mousedown is preventDefault-ed)
- cells built from clipboard-parsed sources sanitize their attrs
  (dataColor may arrive as a non-string from the parseHTML quirk)

The paste override runs before prosemirror-tables' handlePaste, whose
pastedCells path lost the table structure of our clipboard round-trip
and nested the fragment into a cell.
Desktop (trusted keyboard/mouse in the dev vault, CDP :9223) and
Android emulator (:9224) probes for issue table-clipboard — the
desktop one replays the full user flow: rect copy/paste between
tables, row insert at the caret, Excel-semantics Cmd+X, undo, and the
circle-drag focus fix.
…ests

- row insertion extends merges crossing the insertion point (addRow
  convention) and fills only the free columns; source cells never
  recycle — missing columns become empty cells
- rect-paste tiling consumes the source grid in order (cursor advances
  by the placed cell's colspan), so a merged target cell no longer
  skips source cells
- extractCellGrid unwraps nested single-table copies without the
  arbitrary depth cap
- cut handles every cell selection including 1x1 (copy + clear)
- tests: whole-table paste (the original bug shape), merged-target
  tiling, header cells, rowspan-crossed row insertion
Unique note per run (a restored workspace tab may hold a stale doc for
a reused path), explicit cleanup — the leaf is detached before the
verify note is deleted (rule-open-file-safety).
tableCell/tableHeader parseHTML emitted dataColor: [] for every cell
parsed from clipboard HTML; cells inserted from a paste carried that
array into saved documents and the color rendering (safeColorValue →
.trim) crashed the editor on the next open — the note became unopenable
(issue table-clipboard report, 💪 one arm 2.note).

- safeColorValue rejects non-strings; renderHTML checks the type before
  reading the attr (tableCell and tableHeader)
- parseHTML returns a string or null instead of an array
- new read-time repair normalizeCellColorAttrsJSON (wired into
  parseNoteDoc and UmNotepad.prepareNote like the trailing-paragraph
  repair) strips the broken values from already-saved files on open
Manual-review round two (issue table-clipboard):

- pasting a column-shaped copy (single-cell-wide, multi-row) at a caret
  inserts a new column right of the caret column and fills it with the
  copied cells row by row — it used to fall into the row-insert path
  and grow the table downward
- a circle press released without any movement now selects the anchor
  cell (desktop) — a plain click places a caret and the drag needs a
  second cell, so a single cell could not be selected; the touch long-
  press path commits the same way on release without movement
Issue table-clipboard-buttons. The structural table bar (docked in the
toolbar on BOTH platforms) gains clipboard buttons — on mobile there is
no Cmd+C and the cells-format bubble is desktop-only:

- copyTableSelection/cutTableSelection accept a cell selection or the
  caret cell (1×1 — the only single-cell affordance on desktop once the
  circle is gone); cut keeps the Excel semantics
- pasteFromSystemClipboard reads the system clipboard inside the click
  gesture (rich with a plain-text fallback) and feeds it through the
  editor's own paste pipeline via a synthetic paste event; the Paste
  button is hidden when no clipboard read API exists
- icons: copy (from the toolbar set), cut, paste
Issue table-duplicate-row-column. duplicateRow clones the selection's
rows right below themselves; duplicateColumn clones the selection's
last column into a new column right of it (row types preserved). Merges
crossing the paste line extend (the addRow/addColumn convention);
merges crossing the selection boundary are cloned unmerged, with spans
clamped to the copy region — structurally valid in every case. Buttons
(Duplicate row / Duplicate column) sit with the structural row/column
actions of the table bar.
Issue table-selection-native-drag. On desktop the selection circle and
the custom mouse gesture are gone: a click places the caret, a drag
builds the cell rectangle (prosemirror-tables' own handler, Notion
style), and the caret cell acts as a 1x1 scope for the toolbar Copy/
Cut/Paste buttons (previous commit). The circle, the long-press and the
drag gesture remain on mobile, where the circle is the touch target and
posAtCoords degrades (cellDragFreeze machinery unchanged).
Review round of table-selection-native-drag (owner: the circle is not
needed on mobile either — long-press is already the gesture there, and
mobile.css had hidden the circle visually anyway):

- the overlay circle decoration, its CSS and the circle-touch drag
  (runCellDrag/dragToCellHead) are removed; mobile selection is the
  long-press drag (pointer events, direct-DOM picked rectangle), which
  never needed the circle
- the mobile mousedown handler is reduced to swallowing Android's
  compatibility mouse events after a touch drag
- desktop: holding the mouse button still on a cell selects it (the
  mouse twin of the long-press) and moving while held extends the
  rectangle; a plain click stays a caret, an immediate drag stays the
  native rectangle. The dispatch sets no tableEditingKey meta —
  prosemirror-tables' own mouse-drag tracking is still active and reads
  that state as a numeric anchor
…uttons

The table bar grew from 12 to 17 buttons (Copy/Cut/Paste + Duplicate
row/column of the table-ux iteration) — the docked-bar e2e test pinned
the old count. Pin the new count and assert the three clipboard
buttons are present.
@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 0/5

[Medium impact] The changes affect table editing and note loading across desktop and touch users.

Fix the cut failure path, merged-cell edits, and cross-table dragging before merging.

Findings

  1. P1 Cut deletes before copying succeeds ▶
  2. P1 Copied rows gain extra columns ▶
  3. P1 Column fills replace merged cells ▶
  4. P1 Paste skips copied cells ▶
  5. P1 Drag crosses table boundaries ▶

Summary

This PR adds table clipboard buttons, row and column duplication, and desktop long-press selection. It also repairs stored cell colors and removes the old selection circle.

  • Table users can copy, cut, and paste cells with table-aware results.
  • Rows and columns can be duplicated beside the originals.
  • Desktop and touch users select table cells without a corner handle.
  • Table cells keep color attributes safe when pasted and read.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Table clipboard action] --> B{Copy, cut, or paste?}
  B -->|Copy| C[Write system clipboard]
  B -->|Cut| D[Start clipboard write]
  D --> E[Delete rows or clear cells]
  B -->|Paste| F{Cell selection?}
  F -->|Yes| G[Replace selected cell contents]
  F -->|No| H{Copied shape}
  H -->|One cell| I[Replace caret cell contents]
  H -->|One column| J[Insert and fill column]
  H -->|Other grid| K[Insert rows]
Loading

Reviews (1) · Last reviewed commit: "docs(changelog): unreleased — table clip..." · Reviewed by Greptile

Comment thread src/texto/extensions/table/helpers/cellClipboard.ts Outdated
Comment thread src/texto/extensions/table/commands.ts
Comment thread src/texto/extensions/table/helpers/cellClipboard.ts
Comment thread src/texto/extensions/table/helpers/cellClipboard.ts Outdated
Comment thread src/texto/extensions/table/helpers/handleCellSelection.ts
…lls, same-table drag

Five review findings on the table-ux iteration (PR #16):

- cut (both the Mod-X path and the toolbar button) removes content only
  after the clipboard write CONFIRMED; a denied clipboard leaves the
  content intact and copyToSystemClipboard reports success
- duplicateRow: columns covered by a merge extended across the insertion
  line get no cell in the inserted copies — the extended merge spans
  them (was: the clone carried a second merged cell and the table grid
  desynced, caught by the new TableMap shape assertion)
- insertColumnAfterCaret: fill skips slots covered by a rowspan from
  above — filling them clobbered the extended merge's content
- rect paste: the source cursor advances by one PHYSICAL cell (a
  colspan on a source node no longer skips its neighbours)
- desktop long-press extension ignores cells from other tables — the
  selection rectangle cannot cross a table boundary
@sumbad

sumbad commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Thanks for the careful review — all five findings addressed in 19ac63b, one turned out to be a false positive in its original framing:

  1. Cut deletes before copying succeeds — fixed. Both cut paths (the Mod-X handler and the toolbar button) now dispatch the removal only after the clipboard write confirms (copyToSystemClipboard reports success). A denied clipboard leaves the content intact. Regression test added: a denied clipboard leaves the content intact.

  2. Copied rows gain extra columns — fixed, and the reported desync was real: the clone carried a second merged cell into a column already covered by the extended merge. Columns covered by an extended merge now get no cell in the inserted copies (mirroring addRow). A TableMap shape assertion (width/height consistency) pins it in commands.duplicate.test.ts.

  3. Column fills replace merged cells — fixed. insertColumnAfterCaret now skips fill slots covered by a rowspan from above — the same guard duplicateColumn already had.

  4. Paste skips copied cells — fixed. The source cursor advances by one physical cell per placement; a colspan on a source node no longer skips its neighbours. This matters for internal drag&drop, where spans survive — DOM-clipboard pastes always parse spans as 1.

  5. Drag crosses table boundaries — fixed. The desktop long-press extension ignores cells from other tables: the head must belong to the anchor's table, so the rectangle cannot cross a table boundary. (The mobile pointer path was already bounded by domCellAtPoint(cellTable(anchorDom), …).)

Verified locally: 678 unit tests green — new tests for findings 1, 2 and 4 — and the full e2e suite 48/48.

@sumbad
sumbad merged commit 62133c3 into master Oct 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant