Skip to content

Commit 3dd43e2

Browse files
committed
BL-15300 Switch TBT highlighting to pseudoelement
1 parent 879c9e2 commit 3dd43e2

4 files changed

Lines changed: 348 additions & 14 deletions

File tree

src/BloomBrowserUI/bookEdit/toolbox/talkingBook/audioRecording.less

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,25 @@ div.ui-audioCurrent p {
7676
position: unset; // BL-11633, works around Chromium bug
7777
}
7878

79+
::highlight(bloom-audio-split-1) {
80+
background-color: #bfedf3;
81+
}
82+
83+
::highlight(bloom-audio-split-2) {
84+
background-color: #7fdae6;
85+
}
86+
87+
::highlight(bloom-audio-split-3) {
88+
background-color: #29c2d6;
89+
}
90+
7991
.ui-audioCurrent.bloom-postAudioSplit[data-audiorecordingmode="TextBox"]:not(
8092
.ui-suppressHighlight
8193
):not(.ui-disableHighlight) {
8294
// Special highlighting after the Split button completes to show it completed.
83-
// Note: This highlighting is expected to persist across sessions, but to be hidden (displayed with the yellow color) while each segment is playing.
84-
// This is accomplished because this rule temporarily drops out of effect when .ui-audioCurrent is moved to the span as that segment plays.
85-
// (The rule requires a span BELOW the .ui-audioCurrent, so it drops out of effect the span IS the .ui-audioCurrent).
86-
span:nth-child(3n + 1 of .bloom-highlightSegment) {
87-
background-color: #bfedf3;
88-
}
89-
90-
span:nth-child(3n + 2 of .bloom-highlightSegment) {
91-
background-color: #7fdae6;
92-
}
93-
94-
span:nth-child(3n + 3 of .bloom-highlightSegment) {
95-
background-color: #29c2d6;
96-
}
95+
// The actual colors are now applied with named ::highlight() rules populated from TS.
96+
// Note: This highlighting is expected to persist across sessions, but to be hidden
97+
// (displayed with the yellow color) while each segment is playing.
9798

9899
span {
99100
position: unset; // BL-11633, works around Chromium bug

src/BloomBrowserUI/bookEdit/toolbox/talkingBook/audioRecording.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
} from "../../../react_components/featureStatus";
7070
import { animateStyleName } from "../../../utils/shared";
7171
import jQuery from "jquery";
72+
import { AudioTextHighlightManager } from "./audioTextHighlightManager";
7273

7374
enum Status {
7475
Disabled, // Can't use button now (e.g., Play when there is no recording)
@@ -207,6 +208,7 @@ export default class AudioRecording implements IAudioRecorder {
207208

208209
private playbackOrderCache: IPlaybackOrderInfo[] = [];
209210
private disablingOverlay: HTMLDivElement;
211+
private audioTextHighlightManager = new AudioTextHighlightManager();
210212

211213
constructor(maySetHighlight: boolean = true) {
212214
this.audioSplitButton = <HTMLButtonElement>(
@@ -898,6 +900,10 @@ export default class AudioRecording implements IAudioRecorder {
898900
if (pageDocBody) {
899901
this.removeAudioCurrent(pageDocBody);
900902
}
903+
904+
this.audioTextHighlightManager.clearManagedHighlights(
905+
pageDocBody ?? undefined,
906+
);
901907
}
902908

903909
private removeAudioCurrent(parentElement: Element) {
@@ -997,6 +1003,7 @@ export default class AudioRecording implements IAudioRecorder {
9971003

9981004
if (oldElement === newElement && !forceRedisplay) {
9991005
// No need to do much, and better not to so we can avoid any temporary flashes as the highlight is removed and re-applied
1006+
this.refreshAudioTextHighlights(newElement);
10001007
return;
10011008
}
10021009

@@ -1069,6 +1076,21 @@ export default class AudioRecording implements IAudioRecorder {
10691076
);
10701077
}
10711078
}
1079+
1080+
this.refreshAudioTextHighlights(newElement);
1081+
}
1082+
1083+
private refreshAudioTextHighlights(currentHighlight?: Element | null) {
1084+
const activeHighlight = currentHighlight ?? this.getCurrentHighlight();
1085+
const currentTextBox = activeHighlight
1086+
? ((this.getTextBoxOfElement(
1087+
activeHighlight,
1088+
) as HTMLElement | null) ?? null)
1089+
: null;
1090+
this.audioTextHighlightManager.refreshSplitHighlights(
1091+
activeHighlight,
1092+
currentTextBox,
1093+
);
10721094
}
10731095

10741096
// Scrolls an element into view.
@@ -4375,6 +4397,7 @@ export default class AudioRecording implements IAudioRecorder {
43754397
const currentTextBox = this.getCurrentTextBox();
43764398
if (currentTextBox) {
43774399
currentTextBox.classList.add("bloom-postAudioSplit");
4400+
this.refreshAudioTextHighlights(currentTextBox);
43784401
}
43794402
}
43804403

@@ -4384,6 +4407,10 @@ export default class AudioRecording implements IAudioRecorder {
43844407
currentTextBox.classList.remove("bloom-postAudioSplit");
43854408
currentTextBox.removeAttribute("data-audioRecordingEndTimes");
43864409
}
4410+
4411+
this.audioTextHighlightManager.clearManagedHighlights(
4412+
currentTextBox ?? undefined,
4413+
);
43874414
}
43884415

43894416
private getElementsToUpdateForCursor(): (Element | null)[] {
@@ -4823,6 +4850,7 @@ export default class AudioRecording implements IAudioRecorder {
48234850
}
48244851
});
48254852
this.nodesToRestoreAfterPlayEnded.clear();
4853+
this.refreshAudioTextHighlights();
48264854
}
48274855
}
48284856

src/BloomBrowserUI/bookEdit/toolbox/talkingBook/audioRecordingSpec.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,70 @@ import { RecordingMode } from "./recordingMode";
1111
import axios from "axios";
1212
import $ from "jquery";
1313
import { mockReplies } from "../../../utils/bloomApi";
14+
import { splitHighlightNames } from "./audioTextHighlightManager";
15+
16+
class FakeHighlight {
17+
public ranges: Range[];
18+
19+
public constructor(...ranges: Range[]) {
20+
this.ranges = ranges;
21+
}
22+
}
23+
24+
type FakeHighlightRegistry = Map<string, FakeHighlight>;
25+
type TestCssWithHighlights = {
26+
highlights?: FakeHighlightRegistry;
27+
};
28+
29+
const installCustomHighlightPolyfill = (targetWindow: Window) => {
30+
const targetWindowWithCss = targetWindow as Window & {
31+
CSS?: TestCssWithHighlights;
32+
};
33+
if (!targetWindowWithCss.CSS) {
34+
targetWindowWithCss.CSS = {};
35+
}
36+
37+
const cssWithHighlights = targetWindowWithCss.CSS;
38+
cssWithHighlights.highlights = new Map<string, FakeHighlight>();
39+
(
40+
targetWindow as Window & {
41+
Highlight?: typeof FakeHighlight;
42+
}
43+
).Highlight = FakeHighlight;
44+
};
45+
46+
const getPageWindow = (): Window | undefined => {
47+
const iframe = parent.window.document.getElementById(
48+
"page",
49+
) as HTMLIFrameElement | null;
50+
return iframe?.contentWindow ?? undefined;
51+
};
52+
53+
const getCustomHighlightsRegistry = (): FakeHighlightRegistry => {
54+
const targetWindow = getPageWindow() ?? globalThis.window;
55+
const cssWithHighlights = (
56+
targetWindow as Window & {
57+
CSS?: TestCssWithHighlights;
58+
}
59+
).CSS;
60+
if (!cssWithHighlights?.highlights) {
61+
throw new Error(
62+
"Expected CSS.highlights test polyfill to be installed",
63+
);
64+
}
65+
66+
return cssWithHighlights.highlights;
67+
};
68+
69+
const getSplitHighlightTexts = (): string[][] => {
70+
const registry = getCustomHighlightsRegistry();
71+
return splitHighlightNames.map((name) => {
72+
const highlight = registry.get(name);
73+
return highlight
74+
? highlight.ranges.map((range) => range.toString())
75+
: [];
76+
});
77+
};
1478

1579
// Notes:
1680
// For any async tests:
@@ -30,13 +94,21 @@ import { mockReplies } from "../../../utils/bloomApi";
3094

3195
describe("audio recording tests", () => {
3296
beforeAll(async () => {
97+
installCustomHighlightPolyfill(globalThis.window);
98+
3399
await setupForAudioRecordingTests();
100+
101+
const pageWindow = getPageWindow();
102+
if (pageWindow) {
103+
installCustomHighlightPolyfill(pageWindow);
104+
}
34105
});
35106

36107
afterEach(() => {
37108
// Clean up any pending timers to prevent "parent is not defined" errors
38109
// when tests finish before timers fire
39110
theOneAudioRecorder?.clearTimeouts();
111+
getCustomHighlightsRegistry().clear();
40112
});
41113

42114
// In an earlier version of our API, checkForAnyRecording was designed to fail (404) if there was no recording.
@@ -2059,6 +2131,63 @@ describe("audio recording tests", () => {
20592131
});
20602132
});
20612133
});
2134+
2135+
describe("- custom split highlights", () => {
2136+
it("registers the split highlight color groups for the current text box", () => {
2137+
SetupIFrameFromHtml(
2138+
'<div id="page1"><div class="bloom-translationGroup"><div id="box1" class="bloom-editable audio-sentence ui-audioCurrent bloom-postAudioSplit" data-audiorecordingmode="TextBox" data-audiorecordingendtimes="1.0 2.0 3.0"><p><span id="span1" class="bloom-highlightSegment">One.</span><span id="span2" class="bloom-highlightSegment">Two.</span><span id="span3" class="bloom-highlightSegment">Three.</span><span id="span4" class="bloom-highlightSegment">Four.</span></p></div></div></div>',
2139+
);
2140+
2141+
const recording = new AudioRecording();
2142+
recording.markAudioSplit();
2143+
2144+
expect(getSplitHighlightTexts()).toEqual([
2145+
["One.", "Four."],
2146+
["Two."],
2147+
["Three."],
2148+
]);
2149+
});
2150+
2151+
it("clears split highlights while playback moves to an individual segment", async () => {
2152+
SetupIFrameFromHtml(
2153+
'<div id="page1"><div class="bloom-translationGroup"><div id="box1" class="bloom-editable audio-sentence ui-audioCurrent bloom-postAudioSplit" data-audiorecordingmode="TextBox" data-audiorecordingendtimes="1.0 2.0"><p><span id="span1" class="bloom-highlightSegment">One.</span><span id="span2" class="bloom-highlightSegment">Two.</span></p></div></div></div>',
2154+
);
2155+
2156+
const recording = new AudioRecording();
2157+
recording.markAudioSplit();
2158+
2159+
const setHighlightToAsync = (
2160+
recording as unknown as {
2161+
setHighlightToAsync(args: {
2162+
newElement: Element;
2163+
shouldScrollToElement: boolean;
2164+
}): Promise<void>;
2165+
}
2166+
).setHighlightToAsync.bind(recording);
2167+
2168+
await setHighlightToAsync({
2169+
newElement: getFrameElementById("page", "span1")!,
2170+
shouldScrollToElement: false,
2171+
});
2172+
2173+
expect(getSplitHighlightTexts()).toEqual([[], [], []]);
2174+
});
2175+
2176+
it("uses only ui-enableHighlight descendants when a segment disables its own background", () => {
2177+
SetupIFrameFromHtml(
2178+
'<div id="page1"><div class="bloom-translationGroup"><div id="box1" class="bloom-editable audio-sentence ui-audioCurrent bloom-postAudioSplit" data-audiorecordingmode="TextBox" data-audiorecordingendtimes="1.0 2.0"><p><span id="span1" class="bloom-highlightSegment">One.</span><span id="span2" class="bloom-highlightSegment ui-disableHighlight"><span class="ui-enableHighlight">Two</span>&nbsp;&nbsp; <span class="ui-enableHighlight">Three</span></span></p></div></div></div>',
2179+
);
2180+
2181+
const recording = new AudioRecording();
2182+
recording.markAudioSplit();
2183+
2184+
expect(getSplitHighlightTexts()).toEqual([
2185+
["One."],
2186+
["Two", "Three"],
2187+
[],
2188+
]);
2189+
});
2190+
});
20622191
});
20632192

20642193
function StripEmptyClasses(html) {

0 commit comments

Comments
 (0)