Skip to content

Re-mark up the page after an Undo or Redo (BL-16558) - #8201

Merged
StephenMcConnel merged 5 commits into
masterfrom
BL-16558-undo-highlights
Aug 17, 2026
Merged

Re-mark up the page after an Undo or Redo (BL-16558)#8201
StephenMcConnel merged 5 commits into
masterfrom
BL-16558-undo-highlights

Conversation

@JohnThomson

@JohnThomson JohnThomson commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 live Range objects 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 for collapsed / getClientRects()), with the Leveled Reader tool on a page holding an over-long sentence:

Scenario Before
Top-bar Undo button highlight permanently dead (still dead 5.8s later)
Ctrl+Z that restores a range selection (select a word, type over it, undo) permanently dead
Plain Ctrl+Z dead for ~550ms, then repainted

Talking Book turned out to be largely self-healing — the MutationObserver added 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:

  1. No trigger. The top-bar Undo button produces no keystroke in the page, so the keyup-driven markup update never ran at all. Same for the reader tools' own undo stack.
  2. The range-selection guard. selectionStateIsInvalidForMarkup bails when the selection is not collapsed — and an undo restores whatever selection its snapshot was taken with.
  3. The longpress flag. jquery.longpress sets isLongPressEvaluating on 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() in toolbox.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.

handlePageEditing now 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's page iframe and a real Selection, 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 Reviewable

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.

JohnThomson and others added 5 commits August 13, 2026 12:59
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>
Comment thread src/BloomBrowserUI/bookEdit/toolbox/toolbox.ts
Comment thread src/BloomBrowserUI/bookEdit/toolbox/toolbox.ts
Comment thread src/BloomBrowserUI/bookEdit/workspaceRoot.ts
@JohnThomson

Copy link
Copy Markdown
Contributor Author

(Claude Opus 5) Consulted Devin on 2026-08-13, up to commit 3c857036a8423478da38dd724b82610a03a2fe83.

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:

  • Bug — the two new helpers in handlePageEditing used const foo = () => where src/BloomBrowserUI/AGENTS.md asks for function syntax. Fixed in 3c85703; Devin's final pass confirms it as resolved. Never needed a thread.
  • Two independent markup timers mean two markup passes can now overlapRe-mark up the page after an Undo or Redo (BL-16558) #8201 (comment). Left open for John: part of it is a known limitation recorded in the commit message (on Ctrl+Z with Talking Book open, the keyup discards the undo pass's async result, leaving today's 500ms behaviour).
  • Marking up after a reader-model undo can clear the redo stackRe-mark up the page after an Undo or Redo (BL-16558) #8201 (comment). Left open for John: the mechanism is real but confined to the reader tools' own undo stack, which this card already has a parked question about removing.
  • Undo now marks up with a non-collapsed selection, inserting two bookmark spansRe-mark up the page after an Undo or Redo (BL-16558) #8201 (comment). Checked in a running Bloom: both bookmarks survive and the range selection is restored intact, so this is resolved.
  • Two of the informational notes led to fixes anyway (8ff980d and 659c806, both about when the undo throttle records a pass). The rest describe deliberate choices — e.g. that image and origami undo get no markup refresh because they don't rewrite text.

CI (pr-automation) is green. No comment-posting review bot is active on this repo: CodeRabbit is installed but .coderabbit.yml sets auto_review.enabled: false, and no bot has posted on any recent PR here.

@JohnThomson
JohnThomson marked this pull request as ready for review August 13, 2026 19:58

@StephenMcConnel StephenMcConnel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@StephenMcConnel reviewed 6 files and all commit messages, and resolved 3 discussions.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on JohnThomson).

@StephenMcConnel
StephenMcConnel merged commit a13ecbd into master Aug 17, 2026
2 checks passed
@StephenMcConnel
StephenMcConnel deleted the BL-16558-undo-highlights branch August 17, 2026 22:20
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.

2 participants