Repository navigation
Stop stale Bubble objects from reacting after Comical replaces them - #121
Merged
Merged
Conversation
convertBubbleJsonToCanvas registered a new ContainerData for a parent that was already active without stopping the previous generation of Bubbles from monitoring their content elements. Every time a client turned editing on again for the same canvas (Bloom does this whenever it adds an element), the old Bubbles leaked and kept running adjustSizeAndPosition on every DOM change. That was mostly harmless until a stale *family* bubble woke up after its element's level had been renumbered: findRelatives looks the family up in the current bubble list by the stale cached level, finds nothing, and uniteShapes dereferenced selfAndRelatives[0] -> "Cannot read properties of undefined (reading 'outline')". - convertBubbleJsonToCanvas now stops monitoring for the container it is about to replace. - A Bubble's mutation observer lets go of the content if Comical no longer lists that Bubble (new Comical.isCurrentBubble), instead of redrawing from a stale spec. - uniteShapes returns when findRelatives comes back empty. Found while adding a Layer (z-order) menu to Bloom (BL-15992), which renumbers bubble levels and then calls Comical.update(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
The implementation appears safe to merge, with the non-blocking concern that its observer lifecycle regression is not protected by an automated test. SummaryThis PR prevents replaced
Reviews (1) · Last reviewed commit: "Stop stale Bubble objects from reacting ..." |
JohnThomson
marked this pull request as ready for review
September 17, 2026 21:59
andrew-polk
approved these changes
Sep 17, 2026
andrew-polk
left a comment
Contributor
There was a problem hiding this comment.
@andrew-polk reviewed 2 files and all commit messages.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on JohnThomson).
andrew-polk
approved these changes
Sep 17, 2026
andrew-polk
left a comment
Contributor
There was a problem hiding this comment.
Reviewable status:
complete! all files reviewed, all discussions resolved (waiting on JohnThomson).
JohnThomson
added a commit
to BloomBooks/BloomDesktop
that referenced
this pull request
Sep 17, 2026
…row (BL-15992) Renumbering bubble levels and calling Comical.update() woke stale Bubble objects that ComicalJS had leaked each time editing was turned back on for a canvas; a stale family bubble then found no relatives and threw in uniteShapes, which Bloom showed as a "JavaScript error" toast on any Layer move on a page with a child bubble. comicaljs 0.4.2 (BloomBooks/comical-js#121) stops monitoring the replaced bubbles and guards the callback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
JohnThomson
added a commit
to BloomBooks/BloomDesktop
that referenced
this pull request
Sep 17, 2026
https://issues.bloomlibrary.org/youtrack/issue/BL-15992 Overlapping canvas elements were always stacked in the order they were added, with no way to change it. This adds a Layer submenu to the canvas element menu (the "..." button and the right-click menu), in its own group just before Duplicate and Delete, with Bring Forward (Ctrl+]), Bring to Front (Ctrl+Alt+]), Send Backwards (Ctrl+[) and Send to Back (Ctrl+Alt+[). The submenu uses the MUI LayersOutlined icon and the four item icons attached to the card, drawn as SvgIcon components (LayerIcons.tsx). Stacking order is the DOM order of the elements in the bloom-canvas, while ComicalJS draws and hit-tests bubbles by the `level` in each element's data-bubble spec. The new CanvasElementZOrder module moves the element in the DOM and then renumbers every level to match (background image first, at level 1) before calling Comical.update(). A bubble family (parent plus child bubbles, which share a level) moves as one unit; a family that had become scattered in the DOM is gathered next to its parent first, so every move is exactly one step. On game pages elements only move among their own kind (draggable or fixed), matching the rule that draggables stay on top; a family with any draggable member counts as draggable. The game code that pushes draggables to the top shares the level-renumbering helper. The submenu is hidden for the background image, which must stay behind everything, and disabled when there is only one movable element; the forward items are disabled at the top of the stack and the backward items at the bottom. LocalizableNestedMenuItem now passes `disabled` through so the submenu row itself can be disabled. The keyboard shortcuts accept either the physical US bracket keys or the bracket characters (an AltGr-typed bracket counts as the one-step shortcut), and are left alone when no canvas element is selected or text is being edited. comicaljs is updated to 0.4.2 (BloomBooks/comical-js#121): Comical leaked the previous generation of Bubble objects each time editing was turned back on, and after a level renumbering a stale family bubble threw in uniteShapes, which showed as a "JavaScript error" toast on any Layer move on a page with a child bubble. Five new strings in Bloom.xlf (EditTab.Toolbox.CanvasTool.Layer.*). Also fixes stale identifiers in the canvas toolbox README and logs a papercut about build/agent-vite.ps1 reporting exit code 1 on a successful build. Tests: CanvasElementZOrder.test.ts (moves, families, level renumbering, draggable bands), layerMenuItem.test.ts (menu placement, per-type inclusion, disabled states) and CanvasElementKeyboardProvider.test.ts (shortcuts, layouts). Full front-end suite green; typecheck and eslint clean; isolated Vite production build succeeds; verified by hand in a running Bloom. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
JohnThomson
added a commit
to BloomBooks/BloomDesktop
that referenced
this pull request
Oct 9, 2026
https://issues.bloomlibrary.org/youtrack/issue/BL-15992 Overlapping canvas elements were always stacked in the order they were added, with no way to change it. This adds a Layer submenu to the canvas element menu (the "..." button and the right-click menu), in its own group just before Duplicate and Delete: Bring Forward (Ctrl+]), Bring to Front (Ctrl+Shift+]), Send Backwards (Ctrl+[) and Send to Back (Ctrl+Shift+[). Every canvas element type gets it, tables dragged onto a Canvas page included, except the background image, which must stay behind everything. The submenu is disabled when there is only one movable element; the forward items are disabled at the top of the stack and the backward items at the bottom. LocalizableNestedMenuItem now passes `disabled` through so the submenu row itself can be disabled. The shortcuts accept either the physical US bracket keys or the bracket characters (an AltGr-typed bracket counts as the one-step shortcut), and are left alone when no canvas element is selected or text is being edited. Stacking order is the DOM order of the elements in the bloom-canvas, while ComicalJS draws and hit-tests bubbles by the `level` in each element's data-bubble spec. The new CanvasElementZOrder module moves the element in the DOM and then renumbers every level to match (background image first, at level 1) before calling Comical.update(). A bubble family (parent plus child bubbles, which share a level) moves as one unit; a family that had become scattered in the DOM is gathered next to its parent first, so every move is exactly one step. On game pages elements only move among their own kind (draggable or fixed), matching the rule that draggables stay on top; a family with any draggable member counts as draggable. The game code that pushes draggables to the top shares the level-renumbering helper. comicaljs is updated to 0.4.2 (BloomBooks/comical-js#121): Comical leaked the previous generation of Bubble objects each time editing was turned back on, and after a level renumbering a stale family bubble threw in uniteShapes, which showed as a "JavaScript error" toast on any Layer move on a page with a child bubble. Five new strings in Bloom.xlf (EditTab.Toolbox.CanvasTool.Layer.*). Also fixes stale identifiers in the canvas toolbox README and logs a papercut about build/agent-vite.ps1 reporting exit code 1 on a successful build. Tests: CanvasElementZOrder.test.ts (moves, families, level renumbering, draggable bands), layerMenuItem.test.ts (menu placement, per-type inclusion, disabled states) and CanvasElementKeyboardProvider.test.ts (shortcuts, layouts). The e2e test src/BloomE2E/tests/canvas-layer-order.spec.ts (Notion test case 840) drives it in a real Bloom: the menu's enabled states, every command from the menu and from its shortcut with Comical's levels following the DOM order, a click on two overlapping bubbles selecting the front one, the order surviving a page change, and a bubble family moving as one with no script error. Its helpers are helpers/canvasLayers.ts and helpers/scriptErrors.ts, plus submenu support and Add Child Bubble in helpers/canvasElements.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
Comical.convertBubbleJsonToCanvasregisters a newContainerDatafor a parent that is already active without stopping the previous generation ofBubbleobjects from monitoring their content elements. Every time a client turns editing on again for the same canvas (Bloom does so whenever it adds a canvas element), the old bubbles leak and keep runningadjustSizeAndPositionon every DOM change under the canvas.That was mostly invisible until a stale family bubble (a parent or child speech bubble) woke up after its element's
levelhad been renumbered:findRelativeslooks the family up in the current bubble list by the bubble's stale cached level, finds nothing, anduniteShapesdereferencedselfAndRelatives[0]:Found while adding a Layer (z-order) submenu to Bloom (BL-15992, BloomBooks/BloomDesktop#8372), which renumbers bubble levels and then calls
Comical.update(); it surfaced there as a "A JavaScript error occurred" toast whenever a page had a child bubble.Fix
convertBubbleJsonToCanvasstops monitoring for the container it is about to replace, so old bubbles no longer leak.Comical.isCurrentBubble(this)(new) and lets go of the content if Comical no longer lists that bubble, instead of redrawing from a stale spec.uniteShapesreturns whenfindRelativescomes back empty, as a last line of defence.Verified by patching the same logic into the Comical module of a running Bloom: the family move that reliably threw before no longer does, and live bubbles still unite and redraw.
🤖 Generated with Claude Code
This change is