Skip to content

Commit 6232698

Browse files
committed
More post-review fixes
1 parent 984e42a commit 6232698

18 files changed

Lines changed: 799 additions & 412 deletions

File tree

src/BloomBrowserUI/bookEdit/toolbox/ToolboxRoot.tsx

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ const LiveToolBodyHost: React.FunctionComponent<{ element: HTMLDivElement }> = (
284284
></div>
285285
);
286286
};
287-
288287
// Legacy toolbox HTML files contain wrappers and scripts; for React rendering we
289288
// only inject the first meaningful tool-body div.
290289
const extractFirstToolContentDivAsHtml = (rawHtml: string): string => {
@@ -407,71 +406,72 @@ export const ToolboxRoot: React.FunctionComponent = () => {
407406
// Some tool content elements may appear shortly after we build sections.
408407
// Keep trying unresolved live tools until their existing DOM is available.
409408
React.useEffect(() => {
410-
const sectionsToUpgrade = sections
411-
.filter(
412-
(section) =>
413-
!!section.legacyToolBodyHtml &&
414-
!section.liveToolBodyElement &&
415-
!!getLiveToolBodyElement(section.id),
416-
)
417-
.map((section) => section.id);
418-
419-
if (sectionsToUpgrade.length === 0) {
420-
return;
421-
}
422-
423-
setSections((previousSections) =>
424-
previousSections.map((section) => {
425-
if (!sectionsToUpgrade.includes(section.id)) {
409+
// To avoid unnecessary re-renders, only update sections when we actually find a new live element.
410+
setSections((previousSections) => {
411+
let changed = false;
412+
const nextSections = previousSections.map((section) => {
413+
// If it's not legacy, we don't need to 'hydrate' it (look for the body in the legacy accordion).
414+
// If it already has a live element, we don't need to look for it again.
415+
// So such sections just copy into the new array unchanged.
416+
if (
417+
!section.legacyToolBodyHtml ||
418+
section.liveToolBodyElement
419+
) {
426420
return section;
427421
}
428422

423+
// If we still don't have a live element, keep the one that is waiting to be hydrated.
429424
const liveToolBodyElement = getLiveToolBodyElement(section.id);
430425
if (!liveToolBodyElement) {
431426
return section;
432427
}
433428

429+
changed = true;
434430
hydratedToolIds.current.add(section.id);
435431
return {
436432
...section,
437433
liveToolBodyElement,
438434
legacyToolBodyHtml: undefined,
439435
};
440-
}),
441-
);
436+
});
437+
438+
// If we didn't change anything, return the old array so React doesn't re-render unnecessarily.
439+
return changed ? nextSections : previousSections;
440+
});
442441
}, [sections]);
443442

444443
// If a previously adopted live node is no longer connected, clear our reference
445444
// so later hydration can look for a replacement node under #toolbox.
446445
// Note: getLiveToolBodyElement() cannot rediscover the same detached element; it
447446
// only finds currently attached nodes in the legacy toolbox container.
448447
React.useEffect(() => {
449-
const disconnectedToolIds = sections
450-
.filter(
451-
(section) =>
452-
!!section.liveToolBodyElement &&
453-
!section.liveToolBodyElement.isConnected,
454-
)
455-
.map((section) => section.id);
456-
457-
if (disconnectedToolIds.length === 0) {
458-
return;
459-
}
448+
setSections((previousSections) => {
449+
let changed = false;
450+
const disconnectedToolIds: string[] = [];
451+
// Similar logic so that only if we actually find a newly disconnected element
452+
// do we return a different object and cause a re-render.
453+
const nextSections = previousSections.map((section) => {
454+
if (
455+
!section.liveToolBodyElement ||
456+
section.liveToolBodyElement.isConnected
457+
) {
458+
return section;
459+
}
460460

461-
disconnectedToolIds.forEach((toolId) => {
462-
hydratedToolIds.current.delete(toolId);
463-
});
461+
changed = true;
462+
disconnectedToolIds.push(section.id);
463+
return {
464+
...section,
465+
liveToolBodyElement: undefined,
466+
};
467+
});
464468

465-
setSections((previousSections) =>
466-
previousSections.map((section) =>
467-
disconnectedToolIds.includes(section.id)
468-
? {
469-
...section,
470-
liveToolBodyElement: undefined,
471-
}
472-
: section,
473-
),
474-
);
469+
disconnectedToolIds.forEach((toolId) => {
470+
hydratedToolIds.current.delete(toolId);
471+
});
472+
473+
return changed ? nextSections : previousSections;
474+
});
475475
}, [sections]);
476476

477477
// Keep unresolved sections hydrated over time because legacy tool roots are

src/BloomBrowserUI/bookEdit/toolbox/readers/decodableReader/decodableReaderToolboxTool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export class DecodableReaderToolboxTool implements ITool {
200200
getTheOneReaderToolsModel().setMarkupType(
201201
isToggleOff(isForLeveled) ? 0 : 1,
202202
);
203+
getTheOneReaderToolsModel().updateControlContents();
203204
// usually updateMarkup will do this, unless we are coming from showTool
204205
getTheOneReaderToolsModel().doMarkup();
205206
}

src/BloomBrowserUI/bookEdit/toolbox/readers/leveledReader/leveledReaderToolboxTool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// <reference path="../../toolbox.ts" />
1+
/// <reference path="../../toolbox.ts" />
22
import { getTheOneReaderToolsModel } from "../readerToolsModel";
33
import {
44
beginInitializeLeveledReaderTool,
@@ -80,6 +80,7 @@ export class LeveledReaderToolboxTool implements ITool {
8080
getTheOneReaderToolsModel().setMarkupType(
8181
isToggleOff(isForLeveled) ? 0 : 2,
8282
);
83+
getTheOneReaderToolsModel().updateControlContents();
8384
// usually updateMarkup will do this, unless we are coming from showTool
8485
getTheOneReaderToolsModel().doMarkup();
8586
}

src/BloomBrowserUI/bookEdit/toolbox/readers/readerTools.ts

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,61 @@ interface textMarkup extends JQueryStatic {
3535
window.addEventListener("message", processDLRMessage, false);
3636

3737
let readerToolsInitialized: boolean = false;
38+
let lastReaderToolSettingsContent: string | undefined;
39+
const maxReaderSettingsLoadAttempts = 5;
40+
41+
// I'm not sure how copilot came to add this normalization. It claims that it is
42+
// useful defensiveness against some uncertainty about whether Axios will return
43+
// a string or an object.
44+
function normalizeReaderSettings(rawSettings: unknown): ReaderSettings {
45+
if (typeof rawSettings === "string") {
46+
return JSON.parse(rawSettings) as ReaderSettings;
47+
}
48+
return rawSettings as ReaderSettings;
49+
}
50+
51+
function tryNormalizeReaderSettings(
52+
rawSettings: unknown,
53+
): ReaderSettings | undefined {
54+
try {
55+
const settings = normalizeReaderSettings(rawSettings);
56+
if (!settings) {
57+
return undefined;
58+
}
59+
return settings;
60+
} catch {
61+
return undefined;
62+
}
63+
}
64+
65+
function loadReaderSettingsWithRetry(
66+
attemptsRemaining: number,
67+
onLoaded: (settings: ReaderSettings) => void,
68+
onFailed: () => void,
69+
): void {
70+
get("readers/io/readerToolSettings", (settingsFileContent) => {
71+
const normalizedSettings = tryNormalizeReaderSettings(
72+
settingsFileContent.data,
73+
);
74+
if (normalizedSettings) {
75+
onLoaded(normalizedSettings);
76+
return;
77+
}
78+
79+
if (attemptsRemaining > 1) {
80+
window.setTimeout(() => {
81+
loadReaderSettingsWithRetry(
82+
attemptsRemaining - 1,
83+
onLoaded,
84+
onFailed,
85+
);
86+
}, 150);
87+
return;
88+
}
89+
90+
onFailed();
91+
});
92+
}
3893

3994
function getSetupDialogWindow(): Window | null {
4095
return (<HTMLIFrameElement>(
@@ -226,18 +281,50 @@ export function beginInitializeLeveledReaderTool(): JQueryPromise<void> {
226281
export function beginLoadSynphonySettings(): JQueryPromise<void> {
227282
// make sure synphony is initialized
228283
const result = $.Deferred<void>();
284+
get("collection/defaultFont", (result) => setDefaultFont(result.data));
229285
if (readerToolsInitialized) {
230-
result.resolve();
286+
// If we already initialized the reader tools, we still need to read the current data,
287+
// since now that we're using a single browser window for the whole workspace,
288+
// we could change books without reloading the window, and there is some dependence
289+
// of the data on the current book. So we read it one more time, and do some cleanup
290+
// if it is different from what we had before.
291+
loadReaderSettingsWithRetry(
292+
maxReaderSettingsLoadAttempts,
293+
(normalizedSettings) => {
294+
const newSettingsContent = JSON.stringify(normalizedSettings);
295+
const shouldRefresh =
296+
newSettingsContent !== lastReaderToolSettingsContent;
297+
if (!shouldRefresh) {
298+
result.resolve();
299+
return;
300+
}
301+
beginRefreshEverything(normalizedSettings).then(() => {
302+
lastReaderToolSettingsContent = newSettingsContent;
303+
result.resolve();
304+
});
305+
},
306+
() => {
307+
readerToolsInitialized = false;
308+
result.resolve();
309+
},
310+
);
231311
return result;
232312
}
233313
readerToolsInitialized = true;
234314

235-
get("collection/defaultFont", (result) => setDefaultFont(result.data));
236-
get("readers/io/readerToolSettings", (settingsFileContent) => {
237-
initializeSynphony(settingsFileContent.data);
238-
//console.log("done synphony init");
239-
result.resolve();
240-
});
315+
loadReaderSettingsWithRetry(
316+
maxReaderSettingsLoadAttempts,
317+
(normalizedSettings) => {
318+
lastReaderToolSettingsContent = JSON.stringify(normalizedSettings);
319+
initializeSynphony(normalizedSettings);
320+
//console.log("done synphony init");
321+
result.resolve();
322+
},
323+
() => {
324+
readerToolsInitialized = false;
325+
result.resolve();
326+
},
327+
);
241328
return result;
242329
}
243330

@@ -248,7 +335,9 @@ export function beginLoadSynphonySettings(): JQueryPromise<void> {
248335
* @param settingsFileContent The content of the standard JSON) file that stores the Synphony settings for the collection.
249336
* @global {getTheOneReaderToolsModel()) ReaderToolsModel
250337
*/
251-
function initializeSynphony(settingsFileContent: string): void {
338+
function initializeSynphony(
339+
settingsFileContent: ReaderSettings | string,
340+
): void {
252341
const synphony = new ReadersSynphonyWrapper();
253342
synphony.loadSettings(settingsFileContent);
254343
getTheOneReaderToolsModel().setSynphony(synphony);
@@ -460,6 +549,16 @@ export function resizeWordList(startTimeout: boolean = true): void {
460549
if (div.length === 0) return; // if not found, the tool was closed
461550

462551
const wordList: JQuery = div.find("#wordList");
552+
const wordListParent = wordList.parent();
553+
const wordListParentElement = wordListParent.get(0);
554+
if (
555+
!wordListParentElement ||
556+
!wordListParentElement.isConnected ||
557+
!wordListParentElement.ownerDocument?.defaultView
558+
) {
559+
return;
560+
}
561+
463562
const currentHeight: number = div.height();
464563
const currentWidth: number = wordList.width();
465564

@@ -477,7 +576,12 @@ export function resizeWordList(startTimeout: boolean = true): void {
477576
readerToolsModel.previousHeight = currentHeight;
478577
readerToolsModel.previousWidth = currentWidth;
479578

480-
const top = wordList.parent().position().top;
579+
const parentPosition = wordListParent.position();
580+
if (!parentPosition) {
581+
return;
582+
}
583+
584+
const top = parentPosition.top;
481585

482586
const synphony = readerToolsModel.synphony;
483587
if (synphony.source) {
@@ -504,7 +608,7 @@ export function resizeWordList(startTimeout: boolean = true): void {
504608

505609
if (ht < 50) ht = 50;
506610

507-
wordList.parent().css("height", Math.floor(ht) + "px");
611+
wordListParent.css("height", Math.floor(ht) + "px");
508612
}
509613
}
510614

@@ -515,13 +619,19 @@ export function resizeWordList(startTimeout: boolean = true): void {
515619
}
516620

517621
export function createToggle(isForLeveled: boolean) {
622+
const container = document.getElementById(
623+
`${isForLeveled ? "leveled" : "decodable"}-reader-tool-toggle-react-container`,
624+
);
625+
if (!container) {
626+
return;
627+
}
628+
629+
// ReaderToolSwitch uses defaultChecked. Remount so it picks up current page
630+
// reader classes whenever we switch books/pages and reactivate the tool.
631+
ReactDOM.unmountComponentAtNode(container);
518632
ReactDOM.render(
519633
React.createElement(ReaderToolSwitch, { isForLeveled }),
520-
document.getElementById(
521-
`${
522-
isForLeveled ? "leveled" : "decodable"
523-
}-reader-tool-toggle-react-container`,
524-
),
634+
container,
525635
);
526636
}
527637

0 commit comments

Comments
 (0)