Skip to content

Table drag: view.tablePos is never remapped through tr.mapping, throws on a concurrent edit mid-drag #2921

Description

@mustafa-yilmaz

Summary

While building a table-drag visual-feedback example (#2920), a reviewer asked about behavior when two users edit a table at the same time. Investigating turned up a crash bug in TableHandlesExtension itself, unrelated to that PR's own code.

Root cause

In packages/core/src/extensions/TableHandles/TableHandles.ts, TableHandlesView.tablePos is set once per mousemove:

this.tablePos = pmNodeInfo.posBeforeNode + 1;

It's then used directly, without ever being remapped through tr.mapping, in several places - notably the drop-cursor decorations():

const tableResolvedPos = state.doc.resolve(view.tablePos + 1);

mousemove doesn't fire on the dragged-over element while a native HTML5 drag is in progress, so tablePos (and the state.block content snapshot used later by dropHandler) can't refresh during a drag. If any transaction changes the document elsewhere while a drag is active - a concurrent local edit, or another collaborator's change over Yjs - tablePos goes stale.

Reproduction

  1. Start dragging a table row (mousedown on the row handle, then move over another row without releasing).
  2. While the drag is still in progress, dispatch any transaction that changes the document before the table (e.g. editor._tiptapEditor.view.dispatch(editor._tiptapEditor.view.state.tr.insertText("X", 1, 1)) to insert into a heading above the table).
  3. The next dragover recomputes the drop-cursor decoration from the now-stale tablePos, which resolves into the wrong node and throws:
RangeError: Index 1 out of range for <"...heading text...">
  at posAtIndex (prosemirror-model)
  at TableHandles.ts:695 (decorations)

This throws out of viewDecorationsupdateStateInnerdispatchTransaction, i.e. it breaks the transaction dispatch for the other user's edit, not just the drag.

Suggested fix

Same pattern already used correctly elsewhere for stored positions across transactions: remap tablePos (and any other stored position/snapshot used mid-drag) through tr.mapping in an appendTransaction/plugin apply, rather than only refreshing it on mousemove. Happy to open a PR for this if useful, but wanted to report it as its own issue since it's unrelated to the table-drag-visualization example in #2920 where it was found.

Environment

Found against main while working from a local build of packages/core/packages/react/packages/mantine.


Update — after #2920 (225c1ccf4)

Recording where this stands, since #2920 changes the symptom but not the cause.

The crash path is now guarded. #2920 moves the drag decoration build into getTableDragDecorations and calls it inside a single try/catch in the plugin's decorations prop, covering every resolve/posAtIndex in both the highlight and drop-cursor branches. It also verifies the resolved node is actually a table before indexing into it. With that in place the reproduction above no longer throws — the decorations are skipped for that state, so the other user's transaction dispatches normally instead of dispatchTransaction blowing up.

What this issue still covers: view.tablePos is captured on mousemove and never refreshed against the current document. That is the whole of it.

Correction. An earlier version of this section also claimed view.state.block goes stale and that dropHandler therefore works from a stale snapshot, so a concurrent edit could land a wrong drop. That was wrong, and I withdraw it. TableHandlesView.update() already re-resolves the block on every view update:

this.state.block = this.editor.getBlock(this.state.block.id)!;

tablePos is the only piece that isn't refreshed, which is what the title of this issue says. Apologies for the noise.

So the impact splits cleanly by version. Before #2920: the RangeError above, which escapes dispatchTransaction. After #2920: the guard catches it, so the drop cursor silently stops rendering for the rest of the drag while the drop itself still lands correctly. A visual bug rather than a correctness one — but still worth fixing, since the guard is a backstop, not a fix.

Note the stack trace above cites TableHandles.ts:695; that line number predates #2920 and has since moved.

Per @nperez0111's comment below, this will come as its own PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug:P3Medium: Noticeable but non-blocking issues.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions