Re-mark up the page after an Undo or Redo (BL-16558) - #8201
Merged
Conversation
Since the reader and Talking Book highlights became ::highlight() pseudo-elements painted over live Ranges, an undo silently kills them: it does not edit text in place, it writes a saved snapshot over the whole editable, rebuilding every text node in the box. Only a markup pass can re-create the Ranges, and three separate things stopped one from running. Measured against a running Bloom (driving the Edit tab over CDP and inspecting CSS.highlights, checking each Range for collapsed / getClientRects), with the Leveled Reader tool on a page holding an over-long sentence: - the top bar's Undo button left the highlight permanently dead; - so did a Ctrl+Z that restored a range selection (select a word, type over it, undo); - a plain Ctrl+Z blanked it for about half a second. Talking Book turned out to be largely self-healing, because the MutationObserver added for BL-15300 repairs its highlights in place, so the reader tools were where this showed. Its markup was equally stale after a top bar Undo, though, since nothing re-ran it. The three causes were: nothing triggers a markup pass when the undo came from the top bar button (there is no keystroke); the pass gives up when the selection is not collapsed, and an undo restores whatever selection its snapshot was taken with; and jquery.longpress sets isLongPressEvaluating on EVERY keydown and clears it on keyup, so Ctrl+Z, which does its work on the keydown, always found the flag set. So every undo path we own now calls one entry point, updateMarkupAfterUndoOrRedo(): ckeditor's own undo/redo commands (via a new afterCommandExec hook), the top bar button's two text-changing branches, and the reader tools' Ctrl+Z/Ctrl+Y interception. handlePageEditing() takes a trigger - "editing", "paste" or "undoOrRedo" - in place of a retry count, and the trigger is what the undo-specific behavior hangs off: skip the longpress flag, tolerate a range selection, retry when there is no usable selection yet (the top bar button has no keystroke to bring a second chance), and run at once on a private timer so the keyup that ends Ctrl+Z cannot cancel the update it just asked for. A held Ctrl+Z auto-repeats, so those immediate passes are spaced at least 150ms apart rather than re-marking the page on every repeat. Not unit-tested, for the reason already recorded in handlePageEditing: exercising it needs a live ckeditor instance on the editable plus the parent window's page iframe and a real Selection, none of which stand up in jsdom. Verified in the running app instead - after the change, top bar Undo, Ctrl+Z, Ctrl+Z over a selection and Ctrl+Y all keep the highlight alive, with sampling every 10ms finding it dead at most for a sub-frame blip. Known limitation, unchanged here: on Ctrl+Z with the Talking Book tool open, the keyup that follows bumps keydownEventCounter, so the undo's own (async) markup action is discarded and the keyup's pass does the work 500ms later, as it does today. Making the undo pass immune to that would risk applying markup computed from pre-keystroke text - it is built from a clone taken before the await - which is exactly what that counter guards against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's review of the previous commit noticed that lastUndoRedoMarkupStartTime was stamped at the top of mainTask, before we know whether the pass will do anything. An attempt that bails because the selection isn't usable - and each of its 100ms retries - therefore counted as "the last markup pass", so a genuine undo arriving within 150ms waited its turn behind work that never happened. Stamp it once we are past every early return instead, which is also what the throttle is for: spacing out real re-markups of the page during an auto-repeating Ctrl+Z. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's re-review pointed out that the previous commit moved the timestamp past the early returns but not past the two guards after them: the pass still does nothing if the caret isn't in a bloom-editable, or if that box has no ckeditor attached (some arithmetic template boxes). So put the stamp where the markup is actually about to happen, and say so. Behaviour difference is small either way - at worst a following undo's repaint waited up to 150ms behind a pass that did nothing - but the comment claimed more than the placement delivered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/BloomBrowserUI/AGENTS.md asks for typescript "function" syntax over const foo = () => functions; Devin's review caught that the two helpers added to handlePageEditing were arrow constants. (The pre-existing mainTask above them is left alone.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 13, 2026
JohnThomson
commented
Aug 13, 2026
JohnThomson
commented
Aug 13, 2026
Contributor
Author
|
(Claude Opus 5) Consulted Devin on 2026-08-13, up to commit Devin reviewed all four commits on this branch as they were pushed. Across those runs it raised one bug and three things to investigate, plus eight informational notes:
CI ( |
JohnThomson
marked this pull request as ready for review
August 13, 2026 19:58
StephenMcConnel
approved these changes
Aug 17, 2026
StephenMcConnel
left a comment
Contributor
There was a problem hiding this comment.
@StephenMcConnel reviewed 6 files and all commit messages, and resolved 3 discussions.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on JohnThomson).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #8123: the reader and Talking Book highlights were not being adjusted after an Undo or Redo changed the text.
Since BL-16558 the tools paint their highlights as
::highlight()pseudo-elements over liveRangeobjects rather than by wrapping text in spans. An undo does not edit text in place — it writes a whole saved snapshot over the editable, rebuilding every text node in the box — so every highlight there dies at that moment: still registered, painting nothing. Only a markup pass re-creates them, and three separate things stopped one from running.Reproduced against a running Bloom (driving the Edit tab over CDP and inspecting
CSS.highlights, checking each Range forcollapsed/getClientRects()), with the Leveled Reader tool on a page holding an over-long sentence:Talking Book turned out to be largely self-healing — the
MutationObserveradded for BL-15300 repairs its current/split highlights in place — so the reader tools were where this showed. Its markup was equally stale after a top-bar Undo, though, since nothing re-ran it.The three causes:
selectionStateIsInvalidForMarkupbails when the selection is not collapsed — and an undo restores whatever selection its snapshot was taken with.jquery.longpresssetsisLongPressEvaluatingon every keydown and clears it on keyup. Ctrl+Z does its work on the keydown, so an update requested from there always found the flag set and returned.What changed
One entry point,
updateMarkupAfterUndoOrRedo()intoolbox.ts, called from every undo path we own: CKEditor's own undo/redo commands (attachToCkEditor), the top bar's two text-changing branches (handleUndo), and the reader tools' Ctrl+Z/Ctrl+Y interception.handlePageEditingnow takes a trigger ("editing" | "paste" | "undoOrRedo") instead of a retry count. The trigger is what the four undo-specific behaviours hang off: skip the longpress flag, tolerate a range selection, run immediately instead of after the 500ms typing debounce, and wait on its own timer so the keyup that ends Ctrl+Z cannot cancel it.After
Top-bar Undo, Ctrl+Z, Ctrl+Z-over-a-selection and Ctrl+Y all keep the highlight alive. Sampling every 10ms found it dead only once, for ~18ms (sub-frame), on the reader-model branch. Talking Book unchanged: all four split highlights stay live through both paths.
Not unit-tested, for the reason already recorded in
handlePageEditing: exercising it needs a live CKEditor instance on the editable plus the parent window'spageiframe and a realSelection, none of which stand up in jsdom. Verified in the running app instead, as above.Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16558
Devin review
This change is
This fixes a tester-reported regression on the card
The problem reported in BL-16558 comment 102-75392 — select highlighted text in a decodable reader, delete it, click the top-bar Undo, and all the highlighting is gone until you click outside the box — is exactly the top-bar-Undo path above. That report was against 6.5.1307 Alpha, and it was noted there as a regression from 6.4.107 Beta.