diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b665bc7..7731f8d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ The release run heads these entries with the version and opens a fresh - `TextFile::edit`, `save` and `save_to_memory`, also in Python, Java, Objective-C and Swift. `write_edited` is deprecated. +- **Fix**: one undo in the document view's editor took back only part of an + edit that took several steps, such as Enter inside a run, and showed states + between the steps that the reader never made. `odr.editing.undo()` and + `redo()` now take back and replay the whole edit. + ## v7.0.0 - 2026-09-13 - **Breaking**: `DocumentPath`, `Element::document_path()` and diff --git a/src/odr/internal/html/frontend/document.js b/src/odr/internal/html/frontend/document.js index 6670c540b..0c63e6440 100644 --- a/src/odr/internal/html/frontend/document.js +++ b/src/odr/internal/html/frontend/document.js @@ -98,10 +98,14 @@ var done = []; var undone = []; + // undo and redo take all steps of one gesture together + var gesture = 0; + function perform(step) { if (step === null) { return null; } + step.gesture = gesture; step.apply(); done.push(step); undone.length = 0; @@ -745,6 +749,7 @@ /// paragraph holding no run, the mark waits for the next typed text. /// Formatting sits behind the scope gate whole. function format(style, at) { + gesture += 1; if (!odr.editing.isEnabled()) { refuse(null, "readOnly", at); return false; @@ -1334,6 +1339,7 @@ }); root.addEventListener("compositionend", function () { + gesture += 1; var run = composing; composing = null; if (!odr.editing.isEnabled()) { @@ -1354,6 +1360,7 @@ }); root.addEventListener("beforeinput", function (event) { + gesture += 1; var type = event.inputType; var at = rangeOf(event); @@ -1520,9 +1527,12 @@ if (done.length === 0) { return false; } - var step = done.pop(); - step.revert(); - undone.push(step); + var taken = done[done.length - 1].gesture; + while (done.length > 0 && done[done.length - 1].gesture === taken) { + var step = done.pop(); + step.revert(); + undone.push(step); + } odr.editing.changed(); return true; }, @@ -1530,9 +1540,12 @@ if (undone.length === 0) { return false; } - var step = undone.pop(); - step.apply(); - done.push(step); + var given = undone[undone.length - 1].gesture; + while (undone.length > 0 && undone[undone.length - 1].gesture === given) { + var step = undone.pop(); + step.apply(); + done.push(step); + } odr.editing.changed(); return true; }, diff --git a/test/browser/text/tests.html b/test/browser/text/tests.html index 72cf297e0..169f3a60a 100644 --- a/test/browser/text/tests.html +++ b/test/browser/text/tests.html @@ -375,11 +375,19 @@ "and the paragraph is one again", texts() === "first run a link and a tail|bold|third|" ); - check("undo goes on to the run that was cut", odr.editing.undo() === true); - check("and to the run that was shortened", odr.editing.undo() === true); - check("until there is nothing left", odr.editing.undo() === false); + check("in one step, so nothing is left", odr.editing.undo() === false); check("with the page as the renderer wrote it", texts() === "first run a link and a tail|bold|third|"); check("and the log empty", ops().length === 0); + check("redo splits it again", odr.editing.redo() === true && ops().length > 0); + check("in one step too", odr.editing.redo() === false); + + reset(); + select(run(11).firstChild, 5); + input("insertText", "A"); + select(run(11).firstChild, 6); + input("insertText", "B"); + check("two edits are two steps", odr.editing.undo() === true); + check("and the first one stays", run(11).textContent === "firstA run "); reset(); select(run(21).firstChild, 0);