-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdarktheme.js
More file actions
186 lines (161 loc) · 6.89 KB
/
Copy pathdarktheme.js
File metadata and controls
186 lines (161 loc) · 6.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Dark theme module
const autoDarkClass = "auto-dark";
const darkClass = "dark";
const darkThemeToggleClass = "ff-dark-theme-toggle";
const darkThemeToggleEventClass = ".ff-dark-theme-toggle";
const noInitialTransitionsClass = "no-initial-transitions";
const html = document.documentElement;
const darkSchemeMql = window.matchMedia("(prefers-color-scheme: dark)");
// The pending wait for the theme colours to settle, cancelled when another change comes in.
let settleHandle;
// Gets or sets the dark theme selection: "auto", "light" or "dark".
// Setting a value is the same as setting the CSS classes of the document element directly.
Object.defineProperty(F, "darkTheme", {
get: getSelection,
set: value => {
switch (value) {
case "auto":
html.classList.add(autoDarkClass);
html.classList.toggle(darkClass, darkSchemeMql.matches);
break;
case "light":
html.classList.remove(autoDarkClass);
html.classList.remove(darkClass);
break;
case "dark":
html.classList.remove(autoDarkClass);
html.classList.add(darkClass);
break;
default:
throw new TypeError('The dark theme selection must be "auto", "light" or "dark": ' + value);
}
}
});
// Returns the dark theme selection that the document element's CSS classes describe.
function getSelection() {
if (html.classList.contains(autoDarkClass))
return "auto";
return html.F.dark ? "dark" : "light";
}
// Heal a dark class that does not match the system preference, in case the document was delivered
// or cached with a stale one
if (html.classList.contains(autoDarkClass))
html.classList.toggle(darkClass, darkSchemeMql.matches);
// The state that the last notification was sent for. Used to tell an actual theme change from any
// other change of the document element's class attribute.
let lastDark = html.F.dark;
let lastSelection = getSelection();
// Watching the class attribute makes this the only place that sends the notifications, no matter
// who changed the theme. Several changes within one task are delivered together, so they are
// announced once, and a change that is undone again is not announced at all.
new MutationObserver(onClassChanged)
.observe(html, { attributes: true, attributeFilter: ["class"] });
// Follow the system preference as long as no explicit theme was selected
darkSchemeMql.addEventListener("change", event => {
if (html.classList.contains(autoDarkClass))
html.classList.toggle(darkClass, event.matches);
});
// Allow transitions again now that the page is set up
F.onReady(() => requestAnimationFrame(() => html.classList.remove(noInitialTransitionsClass)));
// Sends the theme notifications after the document element's CSS classes have changed.
function onClassChanged() {
let selection = getSelection();
if (selection !== lastSelection) {
lastSelection = selection;
html.F.trigger("darkthemeselect", { bubbles: true }, { darkTheme: selection });
}
let dark = html.F.dark;
if (dark !== lastDark) {
lastDark = dark;
html.F.trigger("darkthemechanging", { bubbles: true }, { dark: dark });
// Another change replaces a pending one so that the settled notification is only sent for
// the final state
settleHandle?.cancel();
settleHandle = html.F.afterTransition("background-color", () => {
settleHandle = undefined;
html.F.trigger("darkthemechange", { bubbles: true }, { dark: dark });
});
}
}
// ==================== Dark theme toggle plugin ====================
// Defines default options for the dark theme toggle plugin.
let darkThemeToggleDefaults = {
// Indicates whether selecting the theme that the system already prefers selects "auto" instead.
// Ignored for three-state checkboxes because they can select "auto" explicitly.
implicitAuto: true
};
// Makes each selected checkbox select the dark theme.
function darkThemeToggle(options) {
return this.forEach(input => {
if (input.classList.contains(darkThemeToggleClass)) return; // Already done
if (!input.matches("input[type=checkbox]")) return; // Wrong element
input.classList.add(darkThemeToggleClass);
let opt = F.initOptions("darkThemeToggle", input, {}, options);
let updating = false;
input.F.on("change" + darkThemeToggleEventClass, () => {
if (updating) return; // Not a user interaction
let selection;
if (isThreeState()) {
selection = input.indeterminate ? "auto" : input.checked ? "dark" : "light";
}
else if (opt.implicitAuto && input.checked === darkSchemeMql.matches) {
// Selecting what the system prefers anyway means following the system again
selection = "auto";
}
else {
selection = input.checked ? "dark" : "light";
}
F.darkTheme = selection;
});
// Follow changes made anywhere else, including by another toggle on the page.
// The events are forwarded to the input instead of listening on the document element,
// because a handler there would close over the input, and the document element is never
// garbage-collected. The input could then not be collected either after it was removed from
// the document, and its handler would keep running for a detached element. Forwarding holds
// only a weak reference to the input and keeps the handler on the input itself.
html.F.forwardEvent("darkthemeselect", input);
html.F.forwardEvent("darkthemechanging", input);
input.F.on("darkthemeselect" + darkThemeToggleEventClass +
" darkthemechanging" + darkThemeToggleEventClass, updateInput);
updateInput();
// Determines whether the checkbox can select the "auto" theme with its indeterminate state.
function isThreeState() {
return input.matches(".three-state, .ff-threestate");
}
// Sets the checkbox to the current theme selection.
function updateInput() {
let indeterminate = isThreeState() && getSelection() === "auto";
let checked = !indeterminate && html.F.dark;
// Already set
if (input.checked === checked && input.indeterminate === indeterminate) return;
updating = true;
input.checked = checked;
input.indeterminate = indeterminate;
// Let the three-state plugin update its backup of the indeterminate state
if (isThreeState())
input.F.threeState();
// The change event lets a toggle switch or other replacement element update itself
input.F.trigger("change", { bubbles: true });
updating = false;
}
});
}
// Deinitializes the plugin. The checked state of the checkbox is left as it is because it is part
// of the document, not something the plugin has added.
function deinit() {
return this.forEach(input => {
if (!input.classList.contains(darkThemeToggleClass)) return;
input.classList.remove(darkThemeToggleClass);
input.F.off(darkThemeToggleEventClass);
html.F.unforwardEvent("darkthemeselect", input);
html.F.unforwardEvent("darkthemechanging", input);
F.deleteOptions("darkThemeToggle", input);
});
}
F.registerPlugin("darkThemeToggle", darkThemeToggle, {
defaultOptions: darkThemeToggleDefaults,
methods: {
deinit: deinit
},
selectors: ["input[type=checkbox].dark-theme-toggle"]
});