Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1df2350
Fix BL-16608 Cleanup toolbox infrastructure (1/7): remove pure dead code
andrew-polk Jul 24, 2026
f3de730
Fix BL-16608 Cleanup toolbox infrastructure (2/7): collapse the React…
andrew-polk Jul 24, 2026
747bcd8
Fix BL-16608 Cleanup toolbox infrastructure (3/7): remove the jQuery …
andrew-polk Jul 24, 2026
f07ffd4
Fix BL-16608 Cleanup toolbox infrastructure (4/7): single source of t…
andrew-polk Jul 25, 2026
6f349c2
Fix BL-16608 Cleanup toolbox infrastructure (5/7): simplify the tool …
andrew-polk Jul 25, 2026
486b9d5
Fix BL-16608 Cleanup toolbox infrastructure (6/7): update docs and tests
andrew-polk Jul 26, 2026
1974820
Fix BL-16608 Cleanup toolbox infrastructure (7/7): extract the page-e…
andrew-polk Jul 26, 2026
b0c5016
Fix BL-16608 Cleanup toolbox infrastructure: don't set an empty url()…
andrew-polk Jul 26, 2026
787175e
Fix BL-16608 Cleanup toolbox infrastructure: update stale toolbox sel…
andrew-polk Jul 27, 2026
1302374
Fix BL-16608 toolbox cleanup: stop switchTool shadowing the module-le…
andrew-polk Jul 27, 2026
a1bd2bb
Fix BL-16608 toolbox cleanup: let the tools deliver their own stylesh…
andrew-polk Jul 27, 2026
9770fd4
Fix BL-16608 toolbox cleanup: stop loading jquery.qtip.css in the too…
andrew-polk Jul 27, 2026
36c3d5a
Fix BL-16608 toolbox cleanup: one React root, each tool an ordinary c…
andrew-polk Jul 27, 2026
a1a65bc
Fix BL-16608 toolbox cleanup: drop the ThemeProviders the single Reac…
andrew-polk Jul 27, 2026
ab84875
Fix BL-16608 toolbox cleanup: finish consolidating the page-access he…
andrew-polk Jul 27, 2026
0b84b2e
Fix BL-16608 toolbox cleanup: carry master's BL-16602 fix into the re…
andrew-polk Jul 27, 2026
280d911
Fix BL-16608 toolbox cleanup: run the toolbox uitests against the rea…
andrew-polk Jul 27, 2026
1eb9d64
Fix BL-16608 toolbox cleanup: give the toolbox's state to React and d…
andrew-polk Jul 28, 2026
5fbeb1b
Fix BL-16608 toolbox cleanup: run each tool's lifecycle from React in…
andrew-polk Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/skills/bloom-canvas-e2e-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Bloom Edit Tab has multiple iframes. Use frame names first:
- Do not treat top `CURRENTPAGE` frame as editable page content.

## Reliable selectors and activation
- Canvas tool tab header: `h3[data-toolid="canvasTool"]`
- Canvas tool tab header (the toolbox is a MUI accordion; the header's icon carries the tool's canonical id): `.MuiAccordionSummary-root:has([data-toolid="canvas"])`
- Canvas tool root: `#canvasToolControls`
- Canvas surface: `.bloom-canvas`
- Created elements: `.bloom-canvas-element`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ export const openCanvasToolTab = async (toolboxFrame: Frame): Promise<void> => {
return;
}

// The toolbox is a MUI accordion (see ToolboxRoot.tsx): each section's clickable header
// is an AccordionSummary, and the icon inside it carries the tool's canonical id.
const canvasToolHeader = toolboxFrame
.locator(
'h3[data-toolid="canvasTool"], h3[data-toolid="canvas"], h3[data-toolid*="canvas"], h3:has-text("Canvas")',
'.MuiAccordionSummary-root:has([data-toolid="canvas"]), .MuiAccordionSummary-root:has-text("Canvas Tool")',
)
.first();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,13 @@ test("S1: Set Destination menu row shows subscription badge when canvas subscrip
paletteItem: "navigation-image-button",
});

// The Canvas tool's section header (a MUI AccordionSummary, identified by the canonical
// tool id on its icon) shows the subscription badge via
// SubscriptionBadgeWithTooltipAndDialog, which renders it as an <img>.
const canvasToolBadgeCount = await canvasTestContext.toolboxFrame
.locator('h3[data-toolid="canvasTool"] .subscription-badge')
.locator(
'.MuiAccordionSummary-root:has([data-toolid="canvas"]) img[src*="bloom-enterprise-badge.svg"]',
)
.count();

if (canvasToolBadgeCount === 0) {
Expand Down
28 changes: 9 additions & 19 deletions src/BloomBrowserUI/bookEdit/js/editableDivUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference path="../../typings/jquery/jquery.d.ts" />
import { get, postString } from "../../utils/bloomApi";
import $ from "jquery";
import { getPageIFrame, getPageIframeBody } from "../../utils/shared";

interface qtipInterface extends JQuery {
qtip(options: string): JQuery;
Expand Down Expand Up @@ -28,9 +29,7 @@ export class EditableDivUtils {
}

public static getElementSelectionIndex(editableDiv: HTMLElement): number {
const page: HTMLIFrameElement | null = <HTMLIFrameElement | null>(
parent.window.document.getElementById("page")
);
const page = getPageIFrame();
if (!page || !page.contentWindow) return -1; // unit testing? Anyway there is no selection, so not in editableDiv.

const selection = page.contentWindow.getSelection();
Expand All @@ -46,9 +45,7 @@ export class EditableDivUtils {
}

public static selectAtOffset(node: Node, offset: number): void {
const page: HTMLIFrameElement | null = <HTMLIFrameElement | null>(
parent.window.document.getElementById("page")
);
const page = getPageIFrame();
if (!page || !page.contentWindow) return;
const iframeWindow: Window = page.contentWindow;
const selection1 = iframeWindow.getSelection();
Expand Down Expand Up @@ -198,22 +195,15 @@ export class EditableDivUtils {
);
}

public static getPageFrame(): HTMLIFrameElement | null {
const doc = window.top?.document;
return doc ? <HTMLIFrameElement>doc.getElementById("page") : null;
}

// The body of the editable page, a root for searching for document content.
public static getPage(): JQuery {
const page = this.getPageFrame();
if (!page || !page.contentWindow) return $();
return $(page.contentWindow.document.body);
}

// look for an existing transform:scale setting and extract the scale. If not found, use 1.0 as starting point.
// If target is supplied, we only want a scale based on the page scaler if the target is inside it.
public static getPageScale(target?: HTMLElement): number {
const page = this.getPage();
// Note: this used to go through a local getPage()/getPageFrame() pair that looked the
// page frame up in window.top rather than parent. For the frames this runs in (the page
// frame and the toolbox frame) those are the same document; see getPageIFrame() in
// shared.ts for why parent is the one we standardized on.
const pageBody = getPageIframeBody();
const page = pageBody ? $(pageBody) : $();
let scaler: HTMLElement | undefined = undefined;
const getScaler = () => page.find("div#page-scaling-container").get(0);
if (target) {
Expand Down
67 changes: 58 additions & 9 deletions src/BloomBrowserUI/bookEdit/toolbox/ReadMe.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
The intended code organization of the toolbox is as follows:
- Files in the root folder (this one, toolbox) should contain only generic code for managing the toolbox as a whole
- Anything that is part of the implementation of a particular tool should be in one of the child folders, of which there is one for each tool in the accordion.
The toolbox is the sidebar of the Edit tab. Every tool in it is a React component.

A partial exception to this is a chunk of code which is either shared by the Decodable Reader and Leveled Reader tool,
or at least has not yet been teased apart. Much of this code is in files or folders with names starting with Reader or containing Synphony.
For now, all such code is in the decodableReader folder (since a slightly larger share of it really belongs there).
Code organization
- Files in this root folder are the generic machinery for managing the toolbox as a whole:
- ToolboxRoot.tsx the React root: one MUI Accordion section per tool, whose body is
what the tool's ITool.renderPanel() returns. There is exactly one
React root for the whole toolbox, and each tool's panel is an
ordinary child of it, so React context (e.g. the MUI theme)
reaches the tools normally.
- useToolLifecycle.ts runs one tool's lifecycle (beginRestoreSettings, showTool,
newPageReady, detachFromPage, hideTool) from an effect, so a tool
runs because it is the current tool of a showing toolbox rather
than because something called it. Each of ToolboxRoot's sections
uses it.
- toolbox.ts the ITool interface and the non-React orchestration: it asks the
server which tools this book has enabled, notices when the page or
book changes, and records what it finds in toolboxState.ts.
- pageEditingMarkup.ts the keystroke-to-markup machinery: the keypress/paste handlers on
the .bloom-editable divs, and the CKEditor bookmark coordination that
keeps the current tool's markup up to date as the user edits without
losing the insertion point.
- toolboxState.ts the toolbox's state — which tools it is offering, which section is
expanded, which tool is running, which tools the book has enabled,
whether the toolbox is showing, and which page it is on — as a plain
external store that the React components subscribe to and toolbox.ts
reads and updates. (Separate module only to avoid an import cycle.)
- toolIds.ts the canonical tool ids, and the single place that knows how a
canonical id maps to the other spellings at our boundaries (the
historical "Tool"/"Check" suffixes in persisted data, and the
English label and l10n key of a tool).
- toolboxToolReactAdaptor.tsx the base class real tools extend; it supplies no-op
lifecycle defaults so a tool implements only what it cares about.
- toolboxBootstrap.ts the toolbox bundle's entry point: renders ToolboxRoot, registers
one instance of each tool, and starts toolbox.ts.
- Anything that is part of the implementation of a particular tool belongs in that tool's
own child folder, one per tool.

It is a goal of our design that code outside the folder of an individual tool should not know about the tool.
Ideally it should be possible to add a new tool to the accordion without modifying any file outside the new folder
that is added, except for modifying toolbox.jade to get the files included.
A partial exception is a chunk of code shared by the Decodable Reader and Leveled Reader
tools, or at least not yet teased apart. Its files and folders have names starting with
Reader or containing Synphony, and for now all of it is in the readers folder.

It is a goal of our design that code outside the folder of an individual tool should not
know about the tool.

To add a new tool
1. Create a folder here whose name is the tool's canonical id (no "Tool" suffix).
2. In it, write a class that extends ToolboxToolReactAdaptor, implementing at least id()
and renderPanel() (which just returns the tool's React element), plus iconPath() if the
section header should show an icon, and whichever lifecycle methods the tool needs (see
the ITool comments in toolbox.ts). You do not wire the lifecycle methods up to anything:
the toolbox calls them for whichever tool is running (useToolLifecycle.ts).
3. Register one instance of it in toolboxBootstrap.ts: ToolBox.registerTool(new MyTool()).
4. Add an XLF entry for the label, whose key follows the convention in toolIds.ts
getToolLabelInfo() (e.g. id "music" gives key "EditTab.Toolbox.MusicTool" and English
"Music Tool"); see .github/skills/xlf-strings/SKILL.md.

That is all. The section header (label, icon, subscription badge), the tool's checkbox in
the "More..." section, and the alphabetical ordering are all derived from the ITool
implementation and its id, so there is no list of tools to update anywhere else.
See also the ToolboxView class comment in src/BloomExe/Edit/ToolboxView.cs.
Loading