Skip to content

Commit e4257ff

Browse files
committed
Add dark mode support.
This uses the `data-bs-theme` attribute the same as webwork2 does. Since PG also uses bootstrap components this is necessary to get those to honor dark mode without a lot of effort. Drag and drop "buckets" are forced to light mode so that the colors it currently use don't cause contrast issues. The graphtool is also forced to light mode, rather than heftily reworking it, and because JSXGraph doesn't really support dark mode. The same is true of JSXGraph images for the `plots.pl` macro. "Knowl" dialogs are forced into light mode, because help files are not updated to work well in dark mode. Images always have a white background so that if the image has a transparent background it will not have contrast issues. MathQuill needs a couple of small changes so that it works well in dark mode. The cursor color needs to use the `currentcolor` and the background color of empty blocks needs to be color scheme responsive. That is in a pull request to the https://github.com/openwebwork/mathquill repository. Note that it is not published, so you will need to use the `npm link` approach to test with webwork2. There may be further modifications needed, but all problems I have tested are working fine. Note that one thing this cannot account for is colors that problem authors use. One thing that authors can do is use the CSS `light-dark` function instead of a single color. That will work for recent versions of all browsers.
1 parent e6aa246 commit e4257ff

13 files changed

Lines changed: 106 additions & 32 deletions

File tree

htdocs/js/DragNDrop/dragndrop.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@
410410
htmlBucket(label, removable, indices = []) {
411411
const bucketElement = document.createElement('div');
412412
bucketElement.classList.add('dd-bucket');
413+
bucketElement.dataset.bsTheme = 'light';
413414

414415
const bucketLabel = document.createElement('div');
415416
bucketLabel.classList.add('dd-bucket-label');

htdocs/js/DropDown/dropdown.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
--bs-btn-active-border-color: #ccc;
1717
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
1818

19+
[data-bs-theme='dark'] & {
20+
--bs-btn-color: #bbb;
21+
--bs-btn-bg: black;
22+
--bs-btn-hover-color: #fff;
23+
--bs-btn-hover-bg: #2c2b2a;
24+
--bs-btn-active-color: #fff;
25+
--bs-btn-active-bg: #2c2b2a;
26+
}
27+
1928
&.show {
2029
border-color: rgba(112, 154, 192, 0.8);
2130
outline: 0;
@@ -37,5 +46,11 @@
3746
--bs-dropdown-link-active-color: black;
3847
--bs-dropdown-link-active-bg: lightgray;
3948
--bs-dropdown-link-hover-bg: #d3d3d387;
49+
50+
[data-bs-theme='dark'] & {
51+
--bs-dropdown-link-active-color: white;
52+
--bs-dropdown-link-active-bg: #737373;
53+
--bs-dropdown-link-hover-bg: #77777777;
54+
}
4055
}
4156
}

htdocs/js/GraphTool/graphtool.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ window.graphTool = (containerId, options) => {
126126
descriptionSpan.textContent = options.ariaDescription ?? 'Interactively graph objects';
127127
gt.board.containerObj.after(descriptionSpan);
128128
gt.board.containerObj.setAttribute('aria-describedby', descriptionSpan.id);
129+
gt.board.containerObj.dataset.bsTheme = 'light';
129130

130131
gt.board.suspendUpdate();
131132

htdocs/js/GraphTool/graphtool.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
border-radius: 10px;
77
box-shadow: inset 0 0 5px 5px rgba(0, 0, 0, 0.15);
88

9+
[data-bs-theme='dark'] & {
10+
box-shadow: inset 0 0 5px 5px rgba(255, 255, 255, 0.15);
11+
}
12+
913
@media only screen and (max-width: 600px) {
1014
width: 342px;
1115
}
@@ -276,6 +280,7 @@
276280
flex-direction: column;
277281
width: calc(100% - 40px);
278282
background-color: #fff;
283+
color: #000;
279284
opacity: 0;
280285
transition: all 0.2s ease-in-out;
281286

@@ -339,11 +344,15 @@
339344
}
340345

341346
.gt-fullscreenwrap:fullscreen {
342-
background-color: #ccc;
347+
background-color: #f5f5f5;
343348
padding: 0;
344349
width: 100%;
345350
height: 100%;
346351

352+
[data-bs-theme='dark'] & {
353+
background-color: #252525;
354+
}
355+
347356
.graphtool-container {
348357
margin: 0 auto;
349358

htdocs/js/ImageView/imageview.js

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@
3131
modal.setAttribute('aria-label', 'image view dialog');
3232
modal.tabIndex = -1;
3333

34-
// Force the dialog into light mode. This is needed for a webwork2 page in dark mode since the dialog is outside
35-
// of the problem content. At least until PG is updated to honor dark mode. Further discussion on this will
36-
// also be needed at that time since many images have transparent backgrounds that will not work with a dark
37-
// background.
38-
modal.dataset.bsTheme = 'light';
39-
4034
const dialog = document.createElement('div');
4135
dialog.classList.add('modal-dialog');
4236

@@ -118,6 +112,7 @@
118112

119113
const body = document.createElement('div');
120114
body.classList.add('modal-body');
115+
body.dataset.bsTheme = 'light';
121116

122117
let graphDiv = null;
123118
if (imgType == 'div') {
@@ -335,26 +330,44 @@
335330
}
336331
};
337332

338-
// Set up images that are already in the page.
339-
document.querySelectorAll('.image-view-elt').forEach((elt) => {
340-
elt.addEventListener('click', imageViewDialog);
341-
elt.addEventListener('keydown', keyHandler);
342-
});
333+
const handleBrokenImage = (img) => {
334+
img.classList.add('broken');
335+
img.removeAttribute('role');
336+
};
337+
338+
const attachListeners = (img) => {
339+
img.removeEventListener('click', imageViewDialog);
340+
img.removeEventListener('keydown', keyHandler);
341+
img.addEventListener('click', imageViewDialog);
342+
img.addEventListener('keydown', keyHandler);
343+
};
343344

344-
const attachListeners = (node) => {
345-
node.removeEventListener('click', imageViewDialog);
346-
node.removeEventListener('keydown', keyHandler);
347-
node.addEventListener('click', imageViewDialog);
348-
node.addEventListener('keydown', keyHandler);
345+
const initializeImgViewElt = (img) => {
346+
if (img instanceof HTMLImageElement) {
347+
if (img.complete) {
348+
if (img.naturalWidth === 0) handleBrokenImage(img);
349+
else attachListeners(img);
350+
} else {
351+
img.addEventListener('error', () => handleBrokenImage(img));
352+
img.addEventListener('load', () => attachListeners(img));
353+
}
354+
} else {
355+
attachListeners(img);
356+
}
349357
};
350358

359+
// Set up images that are already in the page.
360+
for (const elt of document.querySelectorAll('.image-view-elt')) {
361+
initializeImgViewElt(elt);
362+
}
363+
351364
// Deal with images that are added to the page later.
352365
const observer = new MutationObserver((mutationsList) => {
353366
mutationsList.forEach((mutation) => {
354367
mutation.addedNodes.forEach((node) => {
355368
if (node instanceof Element) {
356-
if (node.classList.contains('image-view-elt')) attachListeners(node);
357-
else node.querySelectorAll('.image-view-elt').forEach(attachListeners);
369+
if (node.classList.contains('image-view-elt')) initializeImgViewElt(node);
370+
else node.querySelectorAll('.image-view-elt').forEach(initializeImgViewElt);
358371
}
359372
});
360373
});

htdocs/js/ImageView/imageview.scss

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
.image-view-elt {
22
max-width: 100%;
33

4-
&:hover {
5-
cursor: pointer;
4+
&:not(.broken) {
5+
background-color: #f5f5f5;
6+
7+
&:hover {
8+
cursor: pointer;
9+
}
610
}
711

812
&.top {
@@ -32,6 +36,7 @@
3236
padding: 8px;
3337
text-align: center;
3438
box-sizing: content-box !important;
39+
background-color: white;
3540

3641
img {
3742
max-width: 100%;
@@ -54,6 +59,7 @@
5459
}
5560

5661
.btn {
62+
--bs-btn-box-shadow: none;
5763
padding: 0 0.2rem;
5864
margin: 0 0.25rem 0 0;
5965
border: none;

htdocs/js/Knowls/knowl.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
knowl.knowlModal.setAttribute('aria-labelledby', `${knowl.knowlModal.id}-title`);
2525
knowl.knowlModal.setAttribute('aria-hidden', 'true');
2626

27-
// Force the dialog into light mode. This is needed for a webwork2 page in dark mode since the dialog is
28-
// outside of the problem content. At least until PG and the help files are updated to honor dark mode.
27+
// Force the dialog into light mode. This is needed at least until
28+
// the knowl css and help files are updated to honor dark mode.
2929
knowl.knowlModal.dataset.bsTheme = 'light';
3030

3131
const knowlDialog = document.createElement('div');

htdocs/js/MathQuill/mqeditor.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,17 @@
170170
title.textContent = 'Equation Editor';
171171

172172
const closeButton = document.createElement('button');
173-
// When bootstrap is upgraded to version 5.3 this will need to be changed.
174-
// btn-close-white will be deprecated and data-bs-theme="dark" is used instead.
175-
closeButton.classList.add('btn-close', 'btn-close-white');
173+
closeButton.classList.add('btn-close');
176174
closeButton.type = 'button';
177175
closeButton.setAttribute('aria-label', 'Close');
176+
closeButton.dataset.bsTheme = 'dark';
178177
closeButton.dataset.bsToggle = 'collapse';
179178
closeButton.dataset.bsTarget = `#${answerLabel}-equation-editor`;
180179

181180
cardHeader.append(title, closeButton);
182181

183182
const cardBody = document.createElement('div');
184-
cardBody.classList.add('card-body', 'p-2', 'd-flex', 'align-items-center');
183+
cardBody.classList.add('card-body', 'p-2', 'd-flex', 'align-items-center', 'bg-light-subtle');
185184
cardBody.append(answerQuill);
186185

187186
// Insert text at a the current cursor position in a text input replacing the current selection if any.
@@ -213,7 +212,7 @@
213212
'pb-2',
214213
'px-2',
215214
'gap-2',
216-
'bg-white',
215+
'bg-light-subtle',
217216
'border-top-0'
218217
);
219218

htdocs/js/MathQuill/mqeditor.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ span[id^='mq-answer'] {
88
background-color: white;
99
margin-right: 0;
1010
margin-left: 0;
11+
12+
[data-bs-theme='dark'] & {
13+
background-color: black;
14+
}
1115
}
1216

1317
input[type='text'].codeshard.mq-edit {

htdocs/js/Problem/problem.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
border-radius: 4px;
1414
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
1515

16+
[data-bs-theme='dark'] & {
17+
background-color: #252525;
18+
border-color: #e3e3e3;
19+
}
20+
1621
p {
1722
margin-top: 1rem;
1823
margin-bottom: 1rem;
@@ -39,6 +44,11 @@
3944
border: 1px solid #ccc;
4045
border-radius: 4px;
4146
background-color: white;
47+
48+
[data-bs-theme='dark'] & {
49+
color: #bbb;
50+
background-color: black;
51+
}
4252
}
4353

4454
textarea,
@@ -357,6 +367,9 @@
357367
.popover-header {
358368
--bs-popover-header-bg: #ffc107;
359369
--bs-popover-header-color: black;
370+
.btn-close {
371+
--bs-btn-close-filter: invert(0) grayscale(100%) brightness(200%);
372+
}
360373
}
361374
}
362375

@@ -385,6 +398,10 @@
385398
.card {
386399
--bs-card-cap-bg: #ddd;
387400

401+
[data-bs-theme='dark'] & {
402+
--bs-card-cap-bg: #333;
403+
}
404+
388405
.card-header {
389406
border-radius: 0;
390407

@@ -400,6 +417,10 @@
400417

401418
.parsehilight {
402419
background-color: yellow;
420+
421+
[data-bs-theme='dark'] & {
422+
background-color: #550;
423+
}
403424
}
404425

405426
.ArrayLayout {
@@ -414,6 +435,7 @@
414435
&.feedback-message {
415436
direction: ltr;
416437
background-color: #ede275;
438+
color: #212529;
417439
&:not(:last-child) {
418440
border-bottom: 1px solid black;
419441
}

0 commit comments

Comments
 (0)