Skip to content

Commit 8a539ff

Browse files
committed
hyperbook: follow the page's dark-mode-toggle
A hyperbook carries a <dark-mode-toggle> its reader can flip, and an IDE embedded in the page should be in the same colours as the text around it rather than a dark rectangle in a light page. With no `theme` in the config, the theme now comes from that element - its `mode` property, falling back to the attribute for the moment before the custom element has been upgraded - and follows it live through the component's `colorschemechange` event, so flipping the toggle re-themes the IDE without a reload. It re-reads once whenDefined() resolves as well, so a toggle whose script loads after the IDE still gets honoured. An explicitly configured theme still wins, and a page with no toggle says nothing about its colours, so the IDE stays dark there.
1 parent f7283d9 commit 8a539ff

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

‎src/client/embedded/MainEmbedded.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { EmbeddedSlider } from "./EmbeddedSlider.js";
2424
import { JOScript } from "./EmbeddedStarter.js";
2525
import { WriteQueryManager } from "./WriteQueryManager.js";
2626
import { ThemeManager } from "../main/gui/ThemeManager.js";
27+
import { followPageTheme, PageTheme, pageTheme } from "./PageTheme.js";
2728

2829
import gridUrl from '/include/graphics/grid.svg';
2930

@@ -39,14 +40,19 @@ type JavaOnlineConfig = {
3940
enableFileAccess?: boolean,
4041

4142
/**
42-
* "dark" (the default) or "light" - the same two themes the full IDE offers
43-
* under Einstellungen. Anything else falls back to "dark".
43+
* "dark" or "light" - the same two themes the full IDE offers under
44+
* Einstellungen.
45+
*
46+
* Left out, the IDE takes the theme from the page: a hyperbook has a
47+
* `<dark-mode-toggle>` its reader can flip, and the IDE follows it, live
48+
* (see PageTheme.ts). A page without such a toggle says nothing about its
49+
* colours, and the IDE stays dark, as embedded IDEs have always been.
4450
*
4551
* The theme's colours are custom properties on the document root, so they
4652
* are the page's rather than one IDE's: where a page holds several embedded
4753
* IDEs they all end up in the theme of the one that was initialised last.
4854
*/
49-
theme?: "dark" | "light",
55+
theme?: PageTheme,
5056
}
5157

5258
export class MainEmbedded implements MainBase {
@@ -160,6 +166,8 @@ export class MainEmbedded implements MainBase {
160166

161167
/** Owns this IDE's colours; see the `theme` config option. */
162168
themeManager: ThemeManager;
169+
/** True while no theme was configured, so the page's toggle decides. */
170+
private themeFollowsPage: boolean = false;
163171

164172
constructor($div: JQuery<HTMLElement>, private scriptList: JOScript[]) {
165173

@@ -170,6 +178,7 @@ export class MainEmbedded implements MainBase {
170178
// wrong colours first
171179
this.themeManager = new ThemeManager();
172180
this.themeManager.switchTheme(this.config.theme!);
181+
if (this.themeFollowsPage) followPageTheme(theme => this.applyTheme(theme));
173182

174183
this.writeQueryManager = new WriteQueryManager(this, (this.config.databaseURL == null ? "Empty database" : this.config.databaseURL) + this.config.id);
175184

@@ -243,6 +252,13 @@ export class MainEmbedded implements MainBase {
243252
}
244253

245254

255+
/** Re-theme on the fly when the reader flips the page's dark-mode toggle. */
256+
private applyTheme(theme: PageTheme) {
257+
if (theme == this.config.theme) return;
258+
this.config.theme = theme;
259+
this.themeManager.switchTheme(theme);
260+
}
261+
246262
readConfig($div: JQuery<HTMLElement>) {
247263
let configJson: string | object = $div.data("sql-online");
248264
if (configJson != null && typeof configJson == "string") {
@@ -256,8 +272,11 @@ export class MainEmbedded implements MainBase {
256272
if (this.config.withErrorList == null) this.config.withErrorList = true;
257273
if (this.config.withBottomPanel == null) this.config.withBottomPanel = true;
258274

259-
// a misspelt theme should leave the IDE readable rather than half-styled
260-
if (this.config.theme != "light") this.config.theme = "dark";
275+
// A theme named in the config wins; otherwise the page says which one it
276+
// is in and the IDE follows. A misspelt name counts as "not named" - that
277+
// leaves the IDE readable rather than half-styled.
278+
this.themeFollowsPage = this.config.theme != "light" && this.config.theme != "dark";
279+
if (this.themeFollowsPage) this.config.theme = pageTheme() ?? "dark";
261280

262281
if (!(this.config.withOutput || this.config.withFileList || this.config.withErrorList)) {
263282
this.config.withBottomPanel = false;

‎src/client/embedded/PageTheme.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Which theme the page around an embedded IDE is in.
3+
*
4+
* Hyperbook — and anything else built on GoogleChromeLabs' `<dark-mode-toggle>`
5+
* — puts a toggle on the page and lets the reader choose. An IDE embedded in
6+
* such a page should be in the same colours as the text around it rather than a
7+
* dark rectangle in the middle of a light page, so where the page says which it
8+
* is, the IDE follows.
9+
*
10+
* The element carries the reader's choice in its `mode` property ("dark" or
11+
* "light") and announces changes with a `colorschemechange` event, which is all
12+
* this module reads. Nothing here falls back to prefers-color-scheme: the
13+
* toggle has already resolved that for the page it sits on, and a page without
14+
* a toggle has said nothing about its colours at all.
15+
*/
16+
17+
const TOGGLE = "dark-mode-toggle";
18+
19+
export type PageTheme = "dark" | "light";
20+
21+
type ToggleElement = Element & { mode?: string };
22+
23+
function toggleElement(): ToggleElement | null {
24+
return document.querySelector<ToggleElement>(TOGGLE);
25+
}
26+
27+
/**
28+
* The page's theme, or undefined where the page does not say.
29+
*
30+
* The property is read before the attribute because that is where the element
31+
* keeps the reader's choice; the attribute covers the moment before the custom
32+
* element has been upgraded, when the markup is on the page but its script has
33+
* not run yet.
34+
*/
35+
export function pageTheme(): PageTheme | undefined {
36+
const toggle = toggleElement();
37+
if (!toggle) return undefined;
38+
const mode = toggle.mode ?? toggle.getAttribute("mode");
39+
return mode == "dark" || mode == "light" ? mode : undefined;
40+
}
41+
42+
/**
43+
* Report the page's theme whenever the reader changes it — and once as soon as
44+
* the toggle has been upgraded, because a toggle whose script loads after ours
45+
* has no `mode` to read at the time the IDE starts up.
46+
*
47+
* There is nothing to unsubscribe from: an embedded IDE lives as long as the
48+
* page it is written into.
49+
*/
50+
export function followPageTheme(onChange: (theme: PageTheme) => void): void {
51+
const toggle = toggleElement();
52+
if (!toggle) return;
53+
54+
const report = () => {
55+
const theme = pageTheme();
56+
if (theme) onChange(theme);
57+
};
58+
59+
toggle.addEventListener("colorschemechange", report);
60+
// stays pending forever on a page that never defines the element, which is
61+
// the same as never reporting - and the listener above never fires either
62+
customElements?.whenDefined(TOGGLE).then(report).catch(() => { });
63+
}

0 commit comments

Comments
 (0)