Skip to content

Editor should respect undo (Cmd/Ctrl+Z) after programmatic transcript edits #400

Description

@maboa

The editor has no undo implementation of its own — there is no Cmd/Ctrl+Z handler and no undo stack anywhere in js/. It relies on the browser's native contenteditable undo for #hypertranscript.

Several code paths mutate the transcript DOM directly, outside any user-initiated editing transaction. Direct replaceWith / remove() / textContent = mutations are not recorded on the browser's native undo stack, and typically invalidate what's already on it:

  • normalizeTranscriptSpans() (js/editor-core.js) runs splitWordSpan (span.replaceWith(frag)), mergeJoinedSpans (next.remove()) and reflowLeakedFragments (textContent writes). It's called on blur and again from the debounced input pass, i.e. while the user is typing.
  • replaceMark() (js/find-replace.js) swaps a <mark> for a text node on Replace / Replace All.

The paste handler (js/editor-core.js) is the exception — it uses document.execCommand("insertHTML", …), which does participate in native undo. That's also the only reason paste is undoable today, and execCommand is deprecated.

The practical effect is that undo is unreliable in a way that risks the user's timing data: after a Replace All, or after the debounced normalize pass fires mid-typing, Cmd+Z may do nothing, or may undo an earlier keystroke and leave the DOM in a state the data-m / data-d attributes no longer describe.

Proposal

Make undo a first-class, respected operation. Two broad options:

  1. Explicit undo stack. Snapshot #hypertranscript's HTML plus the current selection before each mutating operation (typing coalesced into runs, normalize passes, replace, replace all, speaker changes), bind Cmd/Ctrl+Z and Shift+Cmd/Ctrl+Z, and restore snapshots. Robust and self-contained; costs memory proportional to transcript size × depth, so the stack needs a bound.
  2. Keep native undo and stop breaking it. Route every programmatic mutation through an undoable path so the browser records it. In practice that means execCommand, which is deprecated and inconsistent across engines — Chrome and Safari differ on what a scripted insertHTML does to the stack.

Option 1 is the more likely answer, but it interacts with the normalize passes: an undo that restores pre-normalize HTML will be re-normalized on the next debounce tick unless the restore suppresses it. Worth designing that interaction explicitly.

Not yet verified

This is written from reading the code, not from testing. Before implementing, it's worth confirming in Chrome and Safari (our priority browsers) exactly what native undo does after each of the mutations above — the failure modes may differ per engine, and mergeJoinedSpans removing a node is likely worse than reflowLeakedFragments rewriting text in place.

Related: #394 (one-word-one-span normalization), #25 (find & replace).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions