Skip to content

Commit b30b845

Browse files
authored
Merge pull request #2894 from drgrice1/dark-mode
Add dark mode support.
2 parents 56f9d02 + f5ad81f commit b30b845

40 files changed

Lines changed: 714 additions & 277 deletions

htdocs/js/GatewayQuiz/gateway.scss

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
/* gateway styles */
22

3-
div.gwMessage {
4-
background-color: #ffeeaa;
5-
box-shadow: 3px 3px 3px darkgray;
6-
margin: 0 0 1rem 0;
7-
padding: 0.25rem;
8-
border-radius: 3px;
9-
}
10-
113
#gwTimer {
124
position: sticky;
135
width: 15em;
@@ -60,6 +52,11 @@ table.attemptResults {
6052
border: 1px solid #ddd;
6153
border-radius: 3px;
6254

55+
[data-bs-theme='dark'] & {
56+
border-color: #555;
57+
background-color: var(--bs-primary-bg-subtle, 'black');
58+
}
59+
6360
h2.gw-problem-number {
6461
display: inline-block;
6562
font-size: 16px;
@@ -85,21 +82,27 @@ table.attemptResults {
8582
}
8683

8784
colgroup.page {
88-
border-left: solid 1pt black;
89-
border-right: solid 1pt black;
85+
border-left: solid 1pt var(--bs-emphasis-color, black);
86+
border-right: solid 1pt var(--bs-emphasis-color, black);
9087
}
9188

9289
.page.active {
9390
background-color: #ffeeaa;
91+
92+
[data-bs-theme='dark'] & {
93+
color: white;
94+
background-color: #80690a;
95+
}
9496
}
9597
}
9698

9799
div.gwDivider {
98100
margin: 0px 0px 10px 0px;
99101
}
100102

101-
/* Override the pg style so that the problem-content is not offset in gateway quizzes. */
103+
/* Override the pg style so that the problem-content is not offset in gateway quizzes and force a light color scheme. */
102104
.problem-content {
105+
color-scheme: light;
103106
padding: unset;
104107
background-color: unset;
105108
border: unset;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
if (MathJax.loader) MathJax.loader.checkVersion('[bs-color-scheme]', '4.1.0', 'extension');
2+
3+
const switchToBSStyle = (obj, key = '@media (prefers-color-scheme: dark)') => {
4+
obj["[data-bs-theme='dark']"] = obj[key];
5+
delete obj[key];
6+
obj["[data-bs-theme='light']"] = structuredClone(obj);
7+
};
8+
9+
for (const [immediate, extension, ready] of [
10+
[
11+
MathJax._.ui?.dialog,
12+
'core',
13+
() => {
14+
const { DraggableDialog } = MathJax._.ui.dialog.DraggableDialog;
15+
switchToBSStyle(DraggableDialog.styles);
16+
17+
// This is a workaround for a bug in MathJax 4.1.0. Delete this for the next version of MathJax.
18+
// See https://github.com/mathjax/MathJax-src/pull/1414.
19+
DraggableDialog.styles["[data-bs-theme='dark']"]['.mjx-dialog a[href]'] =
20+
DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]'];
21+
delete DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]'];
22+
DraggableDialog.styles["[data-bs-theme='dark']"]['.mjx-dialog a[href]:visited'] =
23+
DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]:visited'];
24+
delete DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]:visited'];
25+
}
26+
],
27+
[
28+
MathJax._.a11y?.explorer,
29+
'a11y/explorer',
30+
() => {
31+
const Region = MathJax._.a11y.explorer.Region;
32+
for (const region of ['LiveRegion', 'HoverRegion', 'ToolTip']) {
33+
if (':root' in Region[region].style.styles) {
34+
Region[region].style.styles["[data-bs-theme='light']"] = Region[region].style.styles[':root'];
35+
36+
// The variable --mjx-bg1-color is defined to be 'rgba(var(--mjx-bg-blue), var(--mjx-bg-alpha))'.
37+
// I suspect this is a typo as the variable -mjx-bg-alpha is not defined anywhere. In any case this
38+
// change is needed to get the correct background color on the focused element in the explorer.
39+
Region[region].style.styles["[data-bs-theme='light']"]['--mjx-bg1-color'] =
40+
'rgba(var(--mjx-bg-blue), var(--mjx-bg1-alpha))';
41+
}
42+
Region[region].style.styles["[data-bs-theme='dark']"] =
43+
Region[region].style.styles['@media (prefers-color-scheme: dark)'];
44+
if (':root' in Region[region].style.styles["[data-bs-theme='dark']"]) {
45+
Object.assign(
46+
Region[region].style.styles["[data-bs-theme='dark']"],
47+
Region[region].style.styles["[data-bs-theme='dark']"][':root']
48+
);
49+
delete Region[region].style.styles["[data-bs-theme='dark']"][':root'];
50+
}
51+
Region[region].style.styles['@media (prefers-color-scheme: dark)'] = {};
52+
}
53+
Region.LiveRegion.style.styles['@media (prefers-color-scheme: dark)']['mjx-ignore'] = { ignore: 1 };
54+
MathJax.startup.extendHandler((handler) => {
55+
switchToBSStyle(
56+
handler.documentClass.speechStyles,
57+
'@media (prefers-color-scheme: dark) /* explorer */'
58+
);
59+
return handler;
60+
});
61+
}
62+
],
63+
[
64+
MathJax._.output?.chtml,
65+
'output/chtml',
66+
() => {
67+
const { CHTML } = MathJax._.output.chtml_ts;
68+
switchToBSStyle(CHTML);
69+
const { ChtmlMaction } = MathJax._.output.chtml.Wrappers.maction;
70+
switchToBSStyle(ChtmlMaction.styles, '@media (prefers-color-scheme: dark) /* chtml maction */');
71+
}
72+
],
73+
[
74+
MathJax._.output?.svg,
75+
'output/svg',
76+
() => {
77+
const { SVG } = MathJax._.output.svg_ts;
78+
switchToBSStyle(SVG.commonStyles);
79+
const { SvgMaction } = MathJax._.output.svg.Wrappers.maction;
80+
switchToBSStyle(SvgMaction.styles, '@media (prefers-color-scheme: dark) /* svg maction */');
81+
}
82+
]
83+
]) {
84+
if (immediate) {
85+
ready();
86+
} else {
87+
const config = MathJax.config.loader;
88+
config[extension] ??= {};
89+
config[extension].extraLoads ??= [];
90+
const check = config[extension].checkReady;
91+
config[extension].checkReady = async () => {
92+
if (check) await check();
93+
return ready();
94+
};
95+
}
96+
}

htdocs/js/MathJaxConfig/mathjax-config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ if (!window.MathJax) {
22
window.MathJax = {
33
tex: { packages: { '[+]': webworkConfig?.showMathJaxErrors ? [] : ['noerrors'] } },
44
loader: {
5-
load: ['input/asciimath', '[tex]/noerrors', '[no-dark-mode]'],
6-
paths: { 'no-dark-mode': webworkConfig?.mathJaxDarkModeUrl ?? './no-dark-mode.js' }
5+
load: ['input/asciimath', '[tex]/noerrors', '[bs-color-scheme]'],
6+
paths: { 'bs-color-scheme': webworkConfig?.mathJaxBSColorSchemeUrl ?? './bs-color-scheme.js' }
77
},
88
startup: {
99
ready() {

htdocs/js/MathJaxConfig/no-dark-mode.js

Lines changed: 0 additions & 63 deletions
This file was deleted.

htdocs/js/PGCodeMirror/pgeditor.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.code-mirror-editor {
2-
border: 1px solid #ddd;
2+
border: 1px solid var(--ww-layout-border-color, #ddd);
33
min-height: 400px;
44
overflow: auto;
55
resize: vertical;
@@ -25,7 +25,7 @@
2525

2626
// This style is used if the CodeMirror editor is disabled in localOverrides.conf.
2727
.text-area-editor {
28-
border: 1px solid #ddd;
28+
border: 1px solid var(--ww-layout-border-color, #ddd);
2929
padding: 2px;
3030
height: 550px;
3131
min-height: 400px;

htdocs/js/PGProblemEditor/pgproblemeditor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@
423423
const iframe = document.createElement('iframe');
424424
iframe.title = 'Rendered content';
425425
iframe.id = 'pgedit-render-iframe';
426+
iframe.style.colorScheme = 'light';
426427

427428
// Adjust the height of the iframe when the window is resized and when the iframe loads.
428429
const adjustIFrameHeight = () => {

htdocs/js/RenderProblem/renderproblem.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
iframe = document.createElement('iframe');
7070
iframe.id = `${renderArea.id}_iframe`;
7171
iframe.style.border = 'none';
72+
iframe.style.colorScheme = 'light';
7273
while (renderArea.firstChild) renderArea.firstChild.remove();
7374
renderArea.append(iframe);
7475

htdocs/js/System/color-scheme.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
(() => {
4+
const getPreferredTheme = () => {
5+
const storedTheme = localStorage.getItem('WW.color-scheme');
6+
if (storedTheme) return storedTheme;
7+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
8+
};
9+
10+
let flatpickrDarkTheme;
11+
12+
const setTheme = (theme) => {
13+
const themeValue =
14+
theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
15+
document.documentElement.setAttribute('data-bs-theme', themeValue);
16+
17+
if (!flatpickrDarkTheme) flatpickrDarkTheme = document.getElementById('flatpickr-dark-theme');
18+
if (flatpickrDarkTheme) {
19+
if (themeValue === 'dark') document.head.append(flatpickrDarkTheme);
20+
else flatpickrDarkTheme.remove();
21+
}
22+
};
23+
24+
setTheme(getPreferredTheme());
25+
26+
const showActiveTheme = (theme, focus = false) => {
27+
const themeSwitcher = document.getElementById('color-scheme-chooser');
28+
if (!themeSwitcher) return;
29+
30+
const activeThemeIcon = themeSwitcher.querySelector('.theme-icon-active');
31+
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
32+
33+
for (const element of document.querySelectorAll('[data-bs-theme-value]')) {
34+
element.classList.remove('active');
35+
element.setAttribute('aria-pressed', 'false');
36+
}
37+
38+
btnToActive.classList.add('active');
39+
btnToActive.setAttribute('aria-pressed', 'true');
40+
activeThemeIcon.classList.remove('fa-sun', 'fa-moon', 'fa-circle-half-stroke');
41+
activeThemeIcon.classList.add(
42+
theme === 'light' ? 'fa-sun' : theme === 'dark' ? 'fa-moon' : 'fa-circle-half-stroke'
43+
);
44+
themeSwitcher.setAttribute(
45+
'aria-label',
46+
`${themeSwitcher.title} (${
47+
themeSwitcher.dataset[`${btnToActive.dataset.bsThemeValue}Text`] ?? btnToActive.dataset.bsThemeValue
48+
})`
49+
);
50+
51+
if (focus) themeSwitcher.focus();
52+
};
53+
54+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
55+
const storedTheme = localStorage.getItem('WW.color-scheme');
56+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
57+
const preferredTheme = getPreferredTheme();
58+
setTheme(preferredTheme);
59+
showActiveTheme(preferredTheme);
60+
}
61+
});
62+
63+
window.addEventListener('DOMContentLoaded', () => {
64+
showActiveTheme(getPreferredTheme());
65+
66+
for (const toggle of document.querySelectorAll('[data-bs-theme-value]')) {
67+
toggle.addEventListener('click', () => {
68+
const theme = toggle.getAttribute('data-bs-theme-value');
69+
localStorage.setItem('WW.color-scheme', theme);
70+
setTheme(theme);
71+
showActiveTheme(theme, true);
72+
});
73+
}
74+
});
75+
})();

0 commit comments

Comments
 (0)