Refactor canvas elements: registry-driven controls#7621
Refactor canvas elements: registry-driven controls#7621
Conversation
CanvasElementManager had grown too large and UI affordances (context menu + mini toolbar) were being assembled imperatively, which made ordering/section dividers hard to reason about and encouraged cross-bundle imports. This change introduces a declarative canvas element registry that drives which buttons and menus are available per element type. It also makes context menu/mini-toolbar composition deterministic: fixed section ordering, exactly one divider/spacer between non-empty sections, and Duplicate/Delete always last. To reduce runtime import-cycle risk across the edit view + toolbox bundles, DOM selectors/constants move to a dependency-light module (canvasElementConstants) while canvasElementUtils is narrowed to a cross-frame bridge (getCanvasElementManager) with type-only imports. CanvasElementManager is partially decomposed into focused helper modules (Geometry/Positioning/Alternates) plus public-function wrappers, and related call sites were updated. Misc hardening: safer MUI Menu anchoring, avoid non-null assertions, fix closest() selector typo, and remove duplicate pxToNumber helper. Follow-ups in this series: - Make mini-toolbar + menu more declarative and consistent - Make `toolbarButtons` the sole source of truth for the mini-toolbar (including explicit spacers) and normalize spacer runs. - Share menu + toolbar definitions via a single command registry to keep icons/tooltips/click behavior in sync. - Replace “Set Up Hyperlink” with the “Set Destination” command in this context, and do not show either on simple image elements.
c0fc612 to
e95f491
Compare
- Delegate addCanvasElement* to CanvasElementFactories (toolbox drop + templates)\n- Move paste-image flow to CanvasElementClipboard\n- Move duplication + audio file copy helper to CanvasElementDuplication\n- Keep CanvasElementManager as orchestrator; behavior validated with live toolbox→page drag/drop\n- Track checkpoints + line count in REFACTOR_PLAN.md
- Switch CanvasToolControls to import CanvasElementManager/ITextColorInfo as types only\n- Move ITextColorInfo to dependency-light CanvasElementSharedTypes and re-export from CanvasElementManager\n- Update CanvasElementFactories to use shared ITextColorInfo type\n- Mark Phase B4 complete in REFACTOR_PLAN.md
Extract resize/crop/side-handle/move-crop drag logic into CanvasElementHandleDragInteractions and wire CanvasElementManager/SelectionUi to delegate to it. This keeps CanvasElementManager focused on orchestration and reduces its size.
Move language-alternate behaviors into CanvasElementAlternates and extract draggable/target ordering+cleanup into CanvasElementDraggableIntegration. CanvasElementManager now delegates to these modules and continues to expose compatibility wrappers for existing callers/tests.
Extract origami splitter drag + comic editing suspend/resume logic into CanvasElementEditingSuspension and delegate from CanvasElementManager.
Extract bloom-canvas size-change child adjustment logic into CanvasElementCanvasResizeAdjustments and delegate from CanvasElementManager.
Backport selected master-side fixes into the refactored canvasElementManager structure while preserving refactor boundaries. BL-15247, BL-15657, BL-15695, BL-15719, BL-15752, BL-15754, BL-15757, BL-15791, BL-15831 Includes motion-tool guard behavior, splitter interaction suppression, canvas-tool activation flow updates, background image sizing/race fixes, expand-to-fill enablement logic, missing-image metadata handling, and image-fit attribute propagation. Also exposes requestPageContent delay helpers through editable page exports and adds editablePageUtils helper used by split-pane and manager code.
# Conflicts: # src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts # src/BloomBrowserUI/bookEdit/js/bloomEditing.ts # src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx # src/BloomBrowserUI/bookEdit/js/editablePageUtils.ts # src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts # src/BloomBrowserUI/lib/split-pane/split-pane.ts
|
Previously, hatton (John Hatton) wrote…
John Hatton chose not to take on this issue in this PR. |
| private dynamic GetCreativeCommonsInfo(SIL.Core.ClearShare.LicenseInfo ccLicense) | ||
| { | ||
| dynamic dynamicCcLicense = ccLicense; | ||
| return new | ||
| { | ||
| token = ccLicense.Token, | ||
| allowCommercial = ccLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(ccLicense.DerivativeRule), | ||
| intergovernmentalVersion = ccLicense.IntergovernmentalOrganizationQualifier, | ||
| token = dynamicCcLicense.Token, | ||
| allowCommercial = dynamicCcLicense.CommercialUseAllowed ? "yes" : "no", | ||
| allowDerivatives = GetCcDerivativeRulesAsString(dynamicCcLicense.DerivativeRule), | ||
| intergovernmentalVersion = dynamicCcLicense.IntergovernmentalOrganizationQualifier, | ||
| }; | ||
| } |
There was a problem hiding this comment.
John Hatton chose not to take on this issue in this PR.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson reviewed 3 files and made 4 comments.
Reviewable status: 20 of 197 files reviewed, 35 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts line 1 at r19 (raw file):
// Cross-frame bridge utilities for canvas element code.
I asked an AI how it would improve naming and code organization, and it's top suggestion was that this file wants a more specific name; it's not really a generic place to put utils. It suggested canvasElementBridge. I see it's not a new file, but while we're moving everything and breaking our history, it might be worth doing.
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManagerPublicFunctions.ts line 5 at r19 (raw file):
// // This file exists to keep CanvasElementManager.ts smaller and to reduce coupling // between the page bundle and toolbox UI code.
Seems very similar in purpose to canvasElementUtils (which might become canvasElementBridge). Since it's quite small I'd be inclined to merge them, unless we can improve the names enough to make it clear what functions to look for (and add) in each.
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 1 at r19 (raw file):
import { Bubble, Comical, TailSpec } from "comicaljs";
File name is awkward. Just canvasElementResizeAdjustment?
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 19 at r19 (raw file):
} export class CanvasElementCanvasResizeAdjustments {
I don't see why you want a class here. We make one instance of it in one place for the purpose of calling one function in one place...and that function just provides a wrapper so that its three callers can process it as a function of CanvasElementManager rather than importing it directly. Looks to me as if it just adds complexity.
There's no reason for pxToNumber to be a member variable. Why not just import it directly from canvasElementCssUtils? Instead, CanvasElementManager imports it as pxToNumberFromCssUtils, re-wraps it as its own function pxToNumber, and then passes that to the constructor of this object...
adjustBackgroundImageSize is harder to analyze, because it has also become a member function of a new class, so I'd have to analyze whether there's a good reason for that. But I doubt it...again, one instance is created, and the instance variables look like functions that are created to wrap calls to member functions of classes created to wrap other functions...
This is not trivial complexity. For example, suppose I'm reading some code that calls pxToNumber, and want to see what it does. With a direct import, ctrl-click takes me straight there. (I may not even need to go...VS Code might well pop up the comment that describes the function.) With the code your AI has written,
- ctrl-click takes me to a member variable
- I will do one search to find what sets that, and get to the constructor
- I will search for references to the constructor to find the one place it is called
- I will look at that call and figure out what it passes as the pxToNumber argument
- I will follow that to find the local function in CAM
- And then follow that to the function I wanted to see
That's at least six times as much work! And I very much doubt that it will be easier for an AI to figure out, either.
Unless there's a very good reason, I would be greatly in favor of undoing all this business of wrapping functions in singleton classes. For a long time we've been moving away from creating classes unless they really store important state. We're not in C#, where every function has to belong to some class. Modules provide all the namespace management we need. This feels like a move backwards and to increased complexity.
# Conflicts: # src/BloomBrowserUI/bookEdit/js/CanvasElementContextControls.tsx # src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts # src/BloomBrowserUI/bookEdit/js/workspaceFrames.ts # src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts # src/BloomBrowserUI/bookEdit/toolbox/games/GamePromptDialog.tsx # src/BloomBrowserUI/bookEdit/toolbox/games/GameTool.tsx # src/BloomBrowserUI/utils/bloomMessageBoxSupport.ts
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Renamed the toolbox-to-page bridge from |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] I did not merge these two bridge modules, but I did rename them so their responsibilities are explicit: |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Renamed |
|
Previously, JohnThomson (John Thomson) wrote…
[codex] Removed the singleton-class indirection here. The resize logic now lives in a stateless |
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 1 comment.
Reviewable status: 6 of 201 files reviewed, 33 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementCanvasResizeAdjustments.ts line 19 at r19 (raw file):
Previously, hatton (John Hatton) wrote…
[codex] Removed the singleton-class indirection here. The resize logic now lives in a stateless
adjustCanvasElementChildrenIfSizeChanged()module function inCanvasElementResizeAdjustments.ts, andCanvasElementManagernow calls it directly withadjustBackgroundImageSizeplus the sharedpxToNumberFromCssUtilshelper instead of constructing a wrapper class instance. This is in b20370c.
Good start, but we should just import pxToNumber, not require the caller to supply it. There's only one version of this function in our codebase (I hope).
Also, I'm hoping we can get rid of a lot more of these classes that were apparently just made to wrap functions better imported.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 1 comment.
Reviewable status: 6 of 201 files reviewed, 34 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManager.ts line 149 at r20 (raw file):
setupControlFrame: this.setupControlFrame.bind(this), }); this.backgroundImageManager = new CanvasElementBackgroundImageManager({
It appears to me that most of the classes we instantiate here are just wrappers for functions.
- We started with functions X, Y, and Z in CAM which call P and Q
- We moved X, Y, and Z to a new file A, and P and Q to other files B and C
- We should have made the first new file import P and Q, and anything that needs them import X, Y, and Z. If the functions needed access to some state that lives in the CAM singleton, they should make use of a reliable function that finds the one instance (in the right iframe).
- Instead, we made a class in each new file. X, Y, and Z became public methods of Aclass, and P and Q are now public methods of BClass and CClass.
- Now, we need an instance of each of these classes, so the methods can be called. So CAM has to make instances of AClass, BClass, and CClass.
- But, X, Y, and Z still can't call P and Q, because they are member functions of Bclass and CClass, and we don't want every caller creating instances.
- So class A is made to have member variables that point at functions P and Q
- And CAM gets new functions P and Q, which just wrap calls to BInstance.P and CInstance.Q
- And then passes it's own P and Q wrappers to the constructor for A.
Something that should have been three simple imports has become a mess of classes, member variables, wrapper functions, constructors, and passing functions all over the place.
Now, most of these methods started out as member functions of CAM. So it will take a bit of fiddling to share them instead using import and export. Maybe, some of the new classes really deserve to become objects that own some of the CAM state. But we should minimize this pattern where, instead of seeing 'this.X' and being able to ctrl-click and go right to the implementation, I have to go a member variable that holds function X, then to the constructor that initializes it, then to the caller of that constructor, then to the local function that wraps a call to a member of another class, and then finally to the code I want to see. That's a huge downgrade of readability and increase in complexity.
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4]: I went back and did the broader cleanup. Besides the earlier |
|
Previously, JohnThomson (John Thomson) wrote…
[GPT-5.4]: I agreed with the |
hatton
left a comment
There was a problem hiding this comment.
[codex] I changed the registry’s image fallback to use getImageFromCanvasElement(ctx.canvasElement) instead of probing a canvas element through getImageFromContainer(...). That keeps the compatibility behavior but makes the intent explicit at the call site John flagged.
@hatton made 2 comments.
Reviewable status: 6 of 201 files reviewed, 34 unresolved discussions (waiting on JohnThomson).
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasControlRegistry.ts line 729 at r16 (raw file):
: could this go at the start of the file?
[Hatton] Not necessarily without complicating things. Unlike c#, ts seems to sensitive to order. I don't want to discuss this one any more.
src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasControlAvailabilityRules.ts
Show resolved
Hide resolved
|
[GPT-5.4] I followed up on the latest remaining bot-reported issue directly in the branch. The recursive |
# Conflicts: # src/BloomBrowserUI/bookEdit/js/CanvasElementContextControls.tsx # src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts # src/BloomBrowserUI/bookEdit/js/bloomEditing.ts # src/BloomBrowserUI/bookEdit/toolbox/canvas/CanvasToolControls.tsx # src/BloomBrowserUI/bookEdit/toolbox/canvas/customXmatterPage.tsx # src/BloomBrowserUI/package.json # src/BloomBrowserUI/yarn.lock # src/BloomExe/Edit/EditingModel.cs # src/BloomExe/Edit/EditingView.cs
Resolve the canvas refactor merge conflicts by keeping the new canvasElementManager path, adopting master changes for the shared above-page controls path, and preserving canvas tool activation persistence. Update the robust-file pre-commit hook to fail early with a clear dependency message when required shell tools are missing, and to allow explicitly documented FileStream exceptions via the marker robustfile-hook: allow FileStream. Restore RequestInfo to use the intended shared-delete FileStream helper under that narrow exemption. Include the formatter-only AppearanceSettingsTests change left by the hook so the worktree stays clean. Validated with yarn lint, focused vitest runs for canvas manager and page settings, and the robust-file hook check.
| getString( | ||
| `editView/getDataBookValue?lang=${editable.getAttribute("lang")}&dataBook=${fieldType.dataBook}`, | ||
| (content) => { | ||
| const temp = document.createElement("div"); | ||
| temp.innerHTML = content || ""; | ||
| if (temp.textContent.trim() !== "") { | ||
| editable.innerHTML = content; | ||
| } | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🔴 Missing wrapWithRequestPageContentDelay around async DOM mutation in canvasControlTextMenuItems.ts
The new makeFieldTypeMenuItem in canvasControlTextMenuItems.ts uses getString() (an async API call) and then sets editable.innerHTML = content in the callback, without wrapping the operation in wrapWithRequestPageContentDelay. The old code in the deleted CanvasElementContextControls.tsx:988-1024 explicitly wrapped this identical operation with wrapWithRequestPageContentDelay(…, "setCanvasFieldValueFromDataBook"). This violates the repository's src/BloomBrowserUI/AGENTS.md rule: "When code makes changes to the editable page dom using asynchronous operations, it should use wrapWithRequestPageContentDelay." Without this wrapper, a concurrent requestPageContent call could save the page before the async field-type content has been written, losing the user's field-type change.
Prompt for agents
The getString() call at canvasControlTextMenuItems.ts:347-357 asynchronously fetches a data-book value and then mutates editable.innerHTML. The old implementation in the deleted CanvasElementContextControls.tsx wrapped this exact pattern in wrapWithRequestPageContentDelay to prevent page-content save races. Import wrapWithRequestPageContentDelay from bloomEditing.ts and wrap the getString callback's DOM mutation, similar to how the old setEditableContentFromKnownDataBookValueIfAny function did it. The delay ID was "setCanvasFieldValueFromDataBook".
Was this helpful? React with 👍 or 👎 to provide feedback.
| internal HtmlDom GetXmlDocumentForEditScreenWebPage(string pageUrl, string pageListUrl) | ||
| { | ||
| var path = FileLocationUtilities.GetFileDistributedWithApplication( | ||
| Path.Combine( | ||
| BloomFileLocator.BrowserRoot, | ||
| "bookEdit", | ||
| ReactControl.ShouldUseViteDev() | ||
| ? "WorkspaceRoot.vite-dev.html" | ||
| : "WorkspaceRoot.html" | ||
| ) | ||
| ); | ||
| // {simulatedPageFileInBookFolder} is placed in the template file where we want the source file for the 'page' iframe. | ||
| // We don't really make a file for the page, the contents are just saved in our local server. | ||
| // But we give it a url that makes it seem to be in the book folder so local urls work. | ||
| // See BloomServer.MakeInMemoryHtmlFileInBookFolder() for more details. | ||
| var frameText = RobustFile | ||
| .ReadAllText(path, Encoding.UTF8) | ||
| .Replace("{simulatedPageFileInBookFolder}", pageUrl) | ||
| .Replace("{simulatedPageListFile}", pageListUrl); | ||
| var dom = new HtmlDom(XmlHtmlConverter.GetXmlDomFromHtml(frameText)); | ||
|
|
||
| if (_currentlyDisplayedBook.BookInfo.ToolboxIsOpen) | ||
| { | ||
| // Make the toolbox initially visible. | ||
| // What we have to do to accomplish this is pretty non-intuitive. It's a consequence of the way | ||
| // the pure-drawer CSS achieves the open/close effect. This input is a check-box, so clicking it | ||
| // changes the state of things in a way that all the other CSS can depend on. | ||
| var toolboxCheckBox = dom.SelectSingleNode("//input[@id='pure-toggle-right']"); | ||
| toolboxCheckBox?.SetAttribute("checked", "true"); | ||
| } | ||
|
|
||
| return dom; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Return the top-level document that should be displayed in the browser for the current page. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public HtmlDom GetXmlDocumentForEditScreenWebPage() | ||
| { | ||
| var path = FileLocationUtilities.GetFileDistributedWithApplication( | ||
| Path.Combine( | ||
| BloomFileLocator.BrowserRoot, | ||
| "bookEdit", | ||
| ReactControl.ShouldUseViteDev() | ||
| ? "WorkspaceRoot.vite-dev.html" | ||
| : "WorkspaceRoot.html" | ||
| ) | ||
| ); | ||
| // {simulatedPageFileInBookFolder} is placed in the template file where we want the source file for the 'page' iframe. | ||
| // We don't really make a file for the page, the contents are just saved in our local server. | ||
| // But we give it a url that makes it seem to be in the book folder so local urls work. | ||
| // See BloomServer.MakeInMemoryHtmlFileInBookFolder() for more details. | ||
| var frameText = RobustFile | ||
| .ReadAllText(path, Encoding.UTF8) | ||
| .Replace("{simulatedPageFileInBookFolder}", GetUrlForCurrentPage()) | ||
| .Replace("{simulatedPageListFile}", GetUrlForPageListFile()); | ||
| var dom = new HtmlDom(XmlHtmlConverter.GetXmlDomFromHtml(frameText)); | ||
|
|
||
| if (_currentlyDisplayedBook.BookInfo.ToolboxIsOpen) | ||
| { | ||
| // Make the toolbox initially visible. | ||
| // What we have to do to accomplish this is pretty non-intuitive. It's a consequence of the way | ||
| // the pure-drawer CSS achieves the open/close effect. This input is a check-box, so clicking it | ||
| // changes the state of things in a way that all the other CSS can depend on. | ||
| var toolboxCheckBox = dom.SelectSingleNode("//input[@id='pure-toggle-right']"); | ||
| toolboxCheckBox?.SetAttribute("checked", "true"); | ||
| } | ||
|
|
||
| return dom; | ||
| } |
There was a problem hiding this comment.
🚩 EditingModel.cs adds two new GetXmlDocumentForEditScreenWebPage overloads that are never called
Two new methods were added to EditingModel.cs (lines 1353-1423): an internal overload taking pageUrl and pageListUrl parameters, and a public parameterless overload. Neither is called anywhere in the codebase (confirmed by grep). They duplicate the logic already in EditingView.cs for constructing the workspace root HTML. These appear to be scaffolding for a future change or were added by mistake. They are dead code and can be removed to avoid confusion.
Was this helpful? React with 👍 or 👎 to provide feedback.
| function getCanvasElementManagerForE2e(): CanvasElementManager { | ||
| if (!theOneCanvasElementManager) { | ||
| throw new Error("CanvasElementManager is not available."); | ||
| } | ||
|
|
||
| return theOneCanvasElementManager; | ||
| } | ||
|
|
||
| function getCanvasElementsForE2e(): HTMLElement[] { | ||
| return Array.from( | ||
| document.querySelectorAll(kCanvasElementSelector), | ||
| ) as HTMLElement[]; | ||
| } | ||
|
|
||
| let originalCanExpandToFillSpaceForE2e: (() => boolean) | undefined; | ||
|
|
||
| function e2eSetActiveCanvasElementByIndex(index: number): boolean { | ||
| const element = getCanvasElementsForE2e()[index]; | ||
| if (!element) { | ||
| return false; | ||
| } | ||
|
|
||
| getCanvasElementManagerForE2e().setActiveElement(element); | ||
| return true; | ||
| } | ||
|
|
||
| function e2eSetActivePatriarchBubbleOrFirstCanvasElement(): boolean { | ||
| const manager = getCanvasElementManagerForE2e(); | ||
| const patriarchBubble = manager.getPatriarchBubbleOfActiveElement?.(); | ||
| const patriarchContent = patriarchBubble?.content as | ||
| | HTMLElement | ||
| | undefined; | ||
| if (patriarchContent) { | ||
| manager.setActiveElement(patriarchContent); | ||
| return true; | ||
| } | ||
|
|
||
| const firstCanvasElement = getCanvasElementsForE2e()[0]; | ||
| if (!firstCanvasElement) { | ||
| return false; | ||
| } | ||
|
|
||
| manager.setActiveElement(firstCanvasElement); | ||
| return true; | ||
| } | ||
|
|
||
| function e2eDeleteLastCanvasElement(): void { | ||
| const elements = getCanvasElementsForE2e(); | ||
| const lastElement = elements[elements.length - 1]; | ||
| if (!lastElement) { | ||
| return; | ||
| } | ||
|
|
||
| const manager = getCanvasElementManagerForE2e(); | ||
| manager.setActiveElement(lastElement); | ||
| manager.deleteCurrentCanvasElement(); | ||
| } | ||
|
|
||
| function e2eDuplicateActiveCanvasElement(): void { | ||
| getCanvasElementManagerForE2e().duplicateCanvasElement(); | ||
| } | ||
|
|
||
| function e2eDeleteActiveCanvasElement(): void { | ||
| getCanvasElementManagerForE2e().deleteCurrentCanvasElement(); | ||
| } | ||
|
|
||
| function e2eClearActiveCanvasElement(): void { | ||
| getCanvasElementManagerForE2e().setActiveElement(undefined); | ||
| } | ||
|
|
||
| function e2eSetActiveCanvasElementBackgroundColor( | ||
| color: string, | ||
| opacity: number, | ||
| ): void { | ||
| getCanvasElementManagerForE2e().setBackgroundColor([color], opacity); | ||
| } | ||
|
|
||
| function e2eGetActiveCanvasElementStyleSummary(): { | ||
| textColor: string; | ||
| outerBorderColor: string; | ||
| backgroundColors: string[]; | ||
| } { | ||
| const manager = getCanvasElementManagerForE2e(); | ||
| const textColorInfo = manager.getTextColorInformation?.(); | ||
| const bubbleSpec = manager.getSelectedItemBubbleSpec?.(); | ||
|
|
||
| return { | ||
| textColor: textColorInfo?.color ?? "", | ||
| outerBorderColor: bubbleSpec?.outerBorderColor ?? "", | ||
| backgroundColors: bubbleSpec?.backgroundColors ?? [], | ||
| }; | ||
| } | ||
|
|
||
| function e2eResetActiveCanvasElementCropping(): void { | ||
| getCanvasElementManagerForE2e().resetCropping?.(); | ||
| } | ||
|
|
||
| function e2eCanExpandActiveCanvasElementToFillSpace(): boolean { | ||
| return getCanvasElementManagerForE2e().canExpandToFillSpace(); | ||
| } | ||
|
|
||
| function e2eOverrideCanExpandToFillSpace(value: boolean): boolean { | ||
| const manager = getCanvasElementManagerForE2e(); | ||
| if (!originalCanExpandToFillSpaceForE2e) { | ||
| originalCanExpandToFillSpaceForE2e = | ||
| manager.canExpandToFillSpace.bind(manager); | ||
| } | ||
|
|
||
| manager.canExpandToFillSpace = () => value; | ||
| return true; | ||
| } | ||
|
|
||
| function e2eClearCanExpandToFillSpaceOverride(): void { | ||
| if (!originalCanExpandToFillSpaceForE2e) { | ||
| return; | ||
| } | ||
|
|
||
| const manager = getCanvasElementManagerForE2e(); | ||
| manager.canExpandToFillSpace = originalCanExpandToFillSpaceForE2e; | ||
| originalCanExpandToFillSpaceForE2e = undefined; | ||
| } |
There was a problem hiding this comment.
🚩 e2e test helpers and page-bundle e2e API surface shipped in production code
The editablePage.ts file now exports ~12 e2e* functions (lines 167-287) that directly manipulate CanvasElementManager internals (delete, duplicate, set active, override canExpandToFillSpace, etc.). These are exposed on the editablePageBundle window object and shipped in production builds. While not a correctness bug, they provide an attack surface for in-page scripts and add dead code to production. Consider gating these behind a build-time flag or environment check so they are stripped from release builds.
Was this helpful? React with 👍 or 👎 to provide feedback.
JohnThomson
left a comment
There was a problem hiding this comment.
@JohnThomson made 2 comments and resolved 31 discussions.
Reviewable status: 4 of 207 files reviewed, 6 unresolved discussions (waiting on hatton).
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManager.ts line 75 at r16 (raw file):
Previously, hatton (John Hatton) wrote…
[GPT-5.4] Cleaned this up in the current branch. CanvasElementManager now imports CanvasElementAlternates and CanvasElementPositioning as namespaces and calls those modules directly, instead of aliasing each import and wrapping it back to the original name. That keeps the public CanvasElementManager entry points where we still need them, but removes the rename-back noise you called out.
It fixed some of them, no doubt; but for example I still see seven methods imported with different names from CanvasElementSelectionUi, and at least some of them are then re-wrapped with the original names. Some do more than a completely trivial call of the renamed method, but it still smells. Maybe there's a good reason, but it's too hard to follow the call chain across files in Reviewable.
src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementManager.ts line 149 at r20 (raw file):
Previously, hatton (John Hatton) wrote…
[GPT-5.4]: I went back and did the broader cleanup. Besides the earlier
pxToNumbersimplification,CanvasElementSelectionUi.tsandCanvasElementBackgroundImageManager.tsare now direct helper modules used fromCanvasElementManager, so there are two fewer constructor-injected wrapper classes / host objects in the path. I leftCanvasElementHandleDragInteractions.ts,CanvasElementPointerInteractions.ts, andCanvasElementEditingSuspension.tsas classes because they carry substantial mutable interaction state and event lifecycle, and flattening those looked more likely to make the code harder to follow rather than easier.
Still feels like a mess here. Maybe an interactive review will make sense of it.
This PR delivers the Canvas refactor to a registry-driven control architecture and carries it through with behavior fixes and expanded end-to-end coverage.
What Changed
CanvasElementManagerlogic with focused modules underjs/canvasElementManager(selection UI, pointer/drag interactions, geometry/positioning, clipboard, duplication, alternates, background image handling, editing suspension, etc.).Functional Fixes Included
Test and Tooling Work
This change is