PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 2.1.0
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v2.1.0
2.1.3 2.1.2 2.1.1 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 trunk 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 All 57 releases
darkify / src / assets / js / admin_darkreader.js

admin_darkreader.js in Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) 2.1.0, at src/assets/js/admin_darkreader.js

821 lines 29.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Darkify — admin dark mode, Dark Reader engine.
3 *
4 * This replaces the per-element classifier (client_main.js) for wp-admin only.
5 * The frontend is untouched and still runs that engine.
6 *
7 * Why the swap: the classifier walks every element, reads getComputedStyle on
8 * each (plus ::before/::after), and re-runs on DOM mutations. wp-admin — the
9 * block editor above all — mutates continuously, and React rewrites classes on
10 * nodes the engine has stamped, so the walk never settles. Past a few hundred
11 * elements the editor stops responding.
12 *
13 * Dark Reader works at the stylesheet level instead: it parses the sheets once,
14 * emits an inverted shadow stylesheet, and watches for *new stylesheets* rather
15 * than element churn. Typing in the editor costs it nothing, so the failure
16 * mode is structurally absent rather than tuned away.
17 *
18 * What this file owns:
19 * - the on/off state for the admin (localStorage + the plugin's option)
20 * - the palette -> Dark Reader theme mapping
21 * - the same-origin editor iframes (block editor canvas, classic TinyMCE),
22 * which Dark Reader's page-level API does not reach on its own
23 *
24 * Integration contract: `darkify_switch_trigger()` and `darkify_theme_select()`
25 * must stay on `window` under exactly those names. The admin-bar node is
26 * registered with an inline `onclick="darkify_switch_trigger()"` (see
27 * Admin::darkify_admin_bar_switch), and the React settings screen calls
28 * `darkify_theme_select`. Renaming either breaks the toggle silently.
29 */
30 (function () {
31 "use strict";
32
33 if (typeof DarkReader === "undefined") {
34 return;
35 }
36
37 var HTML = document.documentElement;
38 var DARK_CLASS = "darkify_dark_mode_enabled";
39 var STATE_KEY = "darkify_admin_panel_last_state";
40 /*
41 * The editor's palette keeps the existing key, so the toolbar dropdown a user
42 * has already set carries over. The rest of wp-admin gets its own, because the
43 * two are independent choices.
44 */
45 var THEME_KEY = "darkify_selected_theme";
46 var ADMIN_THEME_KEY = "darkify_admin_palette";
47
48 /** Where to fetch the library from inside an iframe realm. Set by PHP. */
49 var LIB_SRC =
50 typeof window.darkifyDarkReaderSrc === "string"
51 ? window.darkifyDarkReaderSrc
52 : "";
53
54 /* ---------------------------------------------------------------------- */
55 /* Palettes */
56 /* ---------------------------------------------------------------------- */
57
58 /*
59 * Copied from client_main.js's darkify_apply_palette() so the admin shows the
60 * same named presets the frontend does. Only the fields Dark Reader can act
61 * on are carried across: it derives every other colour itself, which is the
62 * whole reason it handles unknown markup the classifier could not.
63 *
64 * The button/placeholder entries the frontend palette also carries have no
65 * Dark Reader equivalent and are deliberately dropped rather than faked.
66 */
67 var PALETTES = {
68 set1: { bg: "#0F0F0F", secondary_bg: "#171717", text_color: "#BEBEBE", link_color: "#E7E7E7", link_hover_color: "#BEBEBE", border_color: "#4A4A4A", input_bg: "#2D2D2D", input_text_color: "#BEBEBE" },
69 set3: { bg: "#211e3c", secondary_bg: "#302C57", text_color: "#B1BBD8", link_color: "#8071fb", link_hover_color: "#B1BBD8", border_color: "#4E478D", input_bg: "#2A264D", input_text_color: "#B1BBD8" },
70 set6: { bg: "#082032", secondary_bg: "#061825", text_color: "#B5D9F3", link_color: "#61bbff", link_hover_color: "#B5D9F3", border_color: "#144E78", input_bg: "#0E3755", input_text_color: "#B5D9F3" },
71 set9: { bg: "#04261d", secondary_bg: "#021e16", text_color: "#C1D2BB", link_color: "#00d29a", link_hover_color: "#C1D2BB", border_color: "#095541", input_bg: "#073d2f", input_text_color: "#C1D2BB" },
72 set10: { bg: "#171004", secondary_bg: "#211706", text_color: "#E0D2BD", link_color: "#e09525", link_hover_color: "#E0D2BD", border_color: "#5D4010", input_bg: "#372911", input_text_color: "#E0D2BD" },
73 };
74
75 /**
76 * Which palette applies here, or "auto" for Dark Reader's own derivation.
77 *
78 * "auto" is the default, and it is the better default. Forcing
79 * darkSchemeBackgroundColor/darkSchemeTextColor overrides the per-colour
80 * inversion that is the entire reason Dark Reader handles markup this plugin
81 * has never seen. Pinning two colours flattens that into "everything is this
82 * grey", and any theme whose surfaces carry meaning through colour loses it.
83 * A palette is now something a user opts into, not something they get.
84 *
85 * The block editor keeps its own choice, independent of the rest of wp-admin:
86 * the editor is where a writer is looking at their content and may well want a
87 * different surface than the one they want on the plugins list.
88 */
89 function isEditorContext() {
90 return !!(
91 document.body &&
92 (document.body.classList.contains("block-editor-page") ||
93 document.querySelector('iframe[name="editor-canvas"]'))
94 );
95 }
96
97 function paletteKey() {
98 return isEditorContext() ? THEME_KEY : ADMIN_THEME_KEY;
99 }
100
101 /**
102 * The site-wide default for this context, from the plugin's settings.
103 *
104 * Absent or unrecognised means "auto", which keeps installs that predate these
105 * two fields on Dark Reader's derivation rather than silently pinning them to
106 * a preset they never chose.
107 */
108 function paletteDefault() {
109 var value = isEditorContext()
110 ? window.darkify_editor_palette_default
111 : window.darkify_admin_palette_default;
112 return PALETTES[value] ? value : "auto";
113 }
114
115 /**
116 * The palette in force: a per-user choice if one exists, otherwise the site
117 * default. The editor's toolbar dropdown writes the per-user value, and it
118 * winning over the setting is the relationship that control has always had.
119 */
120 /*
121 * Unsaved values from the settings screen, so the two palette pickers show
122 * their effect while you are choosing rather than only after a save and a
123 * reload. Outranks both the per-user choice and the saved default for as long
124 * as the screen holds them; cleared when it stops previewing.
125 */
126 var previewOverrides = null;
127
128 function isPaletteValue(value) {
129 return value === "auto" || !!PALETTES[value];
130 }
131
132 function currentPaletteName() {
133 var context = isEditorContext() ? "editor" : "admin";
134
135 if (previewOverrides && isPaletteValue(previewOverrides[context])) {
136 return previewOverrides[context];
137 }
138
139 var stored;
140 try {
141 stored = localStorage.getItem(paletteKey());
142 } catch (e) {
143 stored = null;
144 }
145
146 if (isPaletteValue(stored)) {
147 return stored;
148 }
149
150 return paletteDefault();
151 }
152
153 /** The palette record, or null when running on Dark Reader's own colours. */
154 function currentPalette() {
155 var name = currentPaletteName();
156 return name === "auto" ? null : PALETTES[name];
157 }
158
159 /* ---------------------------------------------------------------------- */
160 /* Theme + fixes */
161 /* ---------------------------------------------------------------------- */
162
163 function buildTheme() {
164 var theme = {
165 mode: 1,
166 brightness: 100,
167 contrast: 100,
168 grayscale: 0,
169 sepia: 0,
170 selectionColor: "auto",
171 // wp-admin's form controls are styled by WordPress, not by the UA. Letting
172 // Dark Reader restyle system controls on top of that double-darkens them.
173 styleSystemControls: false,
174 };
175
176 var p = currentPalette();
177 if (p) {
178 theme.darkSchemeBackgroundColor = p.bg;
179 theme.darkSchemeTextColor = p.text_color;
180 theme.scrollbarColor = p.secondary_bg;
181 }
182
183 return theme;
184 }
185
186 function buildFixes() {
187 var p = currentPalette();
188
189 /*
190 * The two-tone page.
191 *
192 * wp-admin paints `body { background: #f0f0f1 }` and leaves <html> unpainted.
193 * Dark Reader derives body's dark colour *from* #f0f0f1, but paints <html>
194 * with darkSchemeBackgroundColor — a different value. Whenever the content is
195 * shorter than the viewport, <html> shows below <body> and the join is
196 * visible as a horizontal seam, usually right around 100vh. (The same seam
197 * appears in the Dark Reader extension and in other plugins built on it; it
198 * is inherent to theming those two elements independently, not to this
199 * integration.)
200 *
201 * `${...}` hands the colour to Dark Reader's own processing, so <html> is
202 * asked for the same derivation <body> gets. This alone does NOT settle it —
203 * Dark Reader's own root rule is `!important` as well and outranks this one —
204 * so syncRootBackground() pins the real value inline after the fact. This
205 * rule stays as the pre-paint approximation, narrowing the seam before that
206 * measurement can happen; do not remove it, and do not assume it is
207 * sufficient on its own.
208 *
209 * Matching the colours is the fix either way; stretching #wpwrap to full
210 * height only moves the seam somewhere less obvious.
211 */
212 var css =
213 "html { background-color: ${#f0f0f1} !important; }\n" +
214 // The login and about screens paint a different base colour.
215 "body.login, body.about-php { background-color: ${#f0f0f1} !important; }\n";
216
217 /*
218 * Only a chosen palette asserts a link colour. On "auto", Dark Reader's
219 * derived link colour is the correct one — it is computed from the link's
220 * own original colour, so a theme that colours its links deliberately keeps
221 * that distinction instead of having it overwritten.
222 */
223 if (p) {
224 css +=
225 "a, a:visited { color: " + p.link_color + " !important; }\n" +
226 "a:hover, a:focus { color: " + p.link_hover_color + " !important; }\n";
227 }
228
229 return {
230 invert: [],
231
232 css: css,
233
234 /*
235 * Inline styles that carry meaning rather than design. A colour picker's
236 * swatch *is* the value it represents — inverting it makes the control
237 * lie about what it will apply. Same for the block editor's palette
238 * circles and the theme/pattern previews, which are showing the user the
239 * light-mode design on purpose.
240 */
241 ignoreInlineStyle: [
242 ".darkify_switch",
243 ".darkify_ignore",
244 ".wp-picker-container",
245 ".wp-color-result",
246 ".color-option",
247 ".components-color-picker",
248 ".components-circular-option-picker__option",
249 ".components-palette-edit__colors",
250 ".block-editor-color-gradient-control",
251 ".block-editor-block-preview__container",
252 ".block-editor-block-preview__content",
253 ".block-editor-patterns__list",
254 ".editor-styles-wrapper",
255 ],
256
257 /*
258 * Image analysis fetches and samples every image to decide whether to
259 * invert it. In wp-admin that means the whole media library, and a
260 * screenshot or a logo inverted "helpfully" is simply wrong. Off wholesale.
261 */
262 ignoreImageAnalysis: ["*"],
263
264 disableStyleSheetsProxy: false,
265 ignoreCSSUrl: [],
266 };
267 }
268
269 /* ---------------------------------------------------------------------- */
270 /* State */
271 /* ---------------------------------------------------------------------- */
272
273 /**
274 * Whether Admin Panel Dark Mode is switched on in the plugin's settings.
275 *
276 * Darkify's own settings screens load this engine even while the option is
277 * off, so the admin-bar icon can appear the moment it is switched on without
278 * a reload. A stale remembered state must not darken those screens. An absent
279 * flag counts as enabled, which keeps installs that have not re-saved since
280 * the flag was introduced behaving as they did.
281 */
282 /**
283 * Darkify's own React settings screens ship a complete dark theme of their own
284 * (`.dark { --background: oklch(...) }` in darkify-react/src/index.css),
285 * mirrored onto <html> by adminDarkMode.js and set pre-paint by PHP. It styles
286 * the surrounding wp-admin chrome too (`.dark #wpcontent`).
287 *
288 * Running Dark Reader over that darkens an already-dark app twice, and Dark
289 * Reader 4.9 does not parse `oklch()`, so the unresolved custom properties
290 * settle on neighbouring token values — which is why card borders and toggle
291 * backgrounds came out red (`--destructive`). The app themes itself; the
292 * engine's only job on these screens is to keep the toggle working.
293 */
294 function isSelfThemedScreen() {
295 return window.darkifyAdminSelfThemed === true;
296 }
297
298 function optionEnabled() {
299 return (
300 typeof window.darkify_admin_panel_dark_enabled === "undefined" ||
301 window.darkify_admin_panel_dark_enabled === "1"
302 );
303 }
304
305 function readState() {
306 try {
307 return localStorage.getItem(STATE_KEY) === "1";
308 } catch (e) {
309 return false;
310 }
311 }
312
313 function writeState(on) {
314 try {
315 localStorage.setItem(STATE_KEY, on ? "1" : "0");
316 } catch (e) {
317 // Private mode / blocked storage: the toggle still works for this page
318 // load, it just will not be remembered.
319 }
320 }
321
322 function isDark() {
323 return HTML.classList.contains(DARK_CLASS);
324 }
325
326 /* ---------------------------------------------------------------------- */
327 /* Iframes */
328 /* ---------------------------------------------------------------------- */
329
330 /*
331 * The block editor canvas (WP 6.3+) and the classic editor's TinyMCE body are
332 * separate documents. Dark Reader's page-level API binds the realm it was
333 * loaded into, so each same-origin frame needs its own copy loaded inside it
334 * and enabled there. Cross-origin frames are unreachable and left alone.
335 */
336 var IFRAME_SELECTOR =
337 'iframe[name="editor-canvas"], iframe#content_ifr, .mce-container-body iframe';
338
339 /** The classic editor's own toggle state, written by admin-classic-editor.js. */
340 var CLASSIC_MODE_KEY = "darkify_classic_editor_mode";
341
342 function isClassicFrame(iframe) {
343 return (
344 iframe.id === "content_ifr" ||
345 !!(iframe.closest && iframe.closest(".mce-container-body"))
346 );
347 }
348
349 /**
350 * Whether the classic editor's content area should be dark.
351 *
352 * The TinyMCE toolbar carries its own moon/sun button with its own remembered
353 * state, so unlike the block editor canvas this frame is not simply "whatever
354 * the admin is". Honouring only the page state made that button do nothing:
355 * it removed the legacy stylesheet it manages while Dark Reader carried on
356 * painting the frame dark underneath.
357 *
358 * With no remembered choice the frame follows the admin, which is what someone
359 * who has never touched the button expects.
360 */
361 function classicWantsDark(pageDark) {
362 var stored;
363 try {
364 stored = localStorage.getItem(CLASSIC_MODE_KEY);
365 } catch (e) {
366 stored = null;
367 }
368
369 if (stored === "1") return true;
370 if (stored === "0") return false;
371 return pageDark;
372 }
373
374 /**
375 * Put the Dark Reader bundle into a same-origin frame without enabling it.
376 *
377 * Idempotent: the script element's id is the guard, so repeated calls (every
378 * toggle, every canvas re-scan) load it once. Enabling is deliberately not
379 * done here — this only makes `win.DarkReader` exist so a later enable() is
380 * the sole cost.
381 */
382 function ensureLibraryInFrame(doc) {
383 if (!LIB_SRC || !doc || doc.getElementById("darkify-darkreader-lib")) {
384 return;
385 }
386 var win = doc.defaultView;
387 if (!win || win.DarkReader) {
388 return;
389 }
390
391 var script = doc.createElement("script");
392 script.id = "darkify-darkreader-lib";
393 script.src = LIB_SRC;
394 (doc.head || doc.documentElement).appendChild(script);
395 }
396
397 function applyToIframe(iframe, enabled) {
398 if (isClassicFrame(iframe)) {
399 /*
400 * Replaces the page state rather than narrowing it. The classic editor's
401 * moon button darkens the content frame on its own terms — a light admin
402 * with a dark writing area is a combination people deliberately choose,
403 * and the legacy stylesheet this replaced supported it. Writing this as
404 * `enabled && classicWantsDark(...)` made page-dark a precondition, so the
405 * button did nothing whenever the admin was light.
406 */
407 enabled = classicWantsDark(enabled);
408 }
409
410 var doc;
411 var win;
412 try {
413 doc = iframe.contentDocument;
414 win = iframe.contentWindow;
415 } catch (e) {
416 return; // cross-origin
417 }
418 if (!doc || !doc.documentElement || !win) {
419 return;
420 }
421
422 if (!enabled) {
423 try {
424 if (win.DarkReader && win.DarkReader.isEnabled()) {
425 win.DarkReader.disable();
426 }
427 } catch (e) {
428 // Frame navigated out from under us.
429 }
430
431 /*
432 * Load the library into the frame anyway, while nothing is waiting on it.
433 *
434 * Turning dark on is inherently slower than turning it off — enable()
435 * analyses every stylesheet and generates an inverted one, disable() only
436 * tears down what already exists. That asymmetry belongs to Dark Reader
437 * and cannot be removed here. What can be removed is the rest of the first
438 * toggle's bill: without this, the first light->dark in the editor also
439 * pays to fetch 106 KB into the canvas frame and parse it, before the
440 * analysis has even started. Paying that during idle time after load means
441 * the click only costs the part that is genuinely unavoidable.
442 */
443 ensureLibraryInFrame(doc);
444 return;
445 }
446
447 if (win.DarkReader) {
448 try {
449 win.DarkReader.enable(buildTheme(), buildFixes());
450 } catch (e) {
451 // ignore
452 }
453 return;
454 }
455
456 if (!LIB_SRC || doc.getElementById("darkify-darkreader-lib")) {
457 return; // no URL to load, or a load is already in flight
458 }
459
460 var script = doc.createElement("script");
461 script.id = "darkify-darkreader-lib";
462 script.src = LIB_SRC;
463 script.onload = function () {
464 try {
465 win.DarkReader.setFetchMethod(win.fetch.bind(win));
466 win.DarkReader.enable(buildTheme(), buildFixes());
467 } catch (e) {
468 // ignore
469 }
470 };
471 (doc.head || doc.documentElement).appendChild(script);
472 }
473
474 function applyToAllIframes(enabled) {
475 var frames = document.querySelectorAll(IFRAME_SELECTOR);
476 for (var i = 0; i < frames.length; i++) {
477 var frame = frames[i];
478 // Re-apply after every navigation of the frame, bound once per element.
479 if (!frame.dataset.darkifyDrBound) {
480 frame.dataset.darkifyDrBound = "1";
481 frame.addEventListener("load", function () {
482 applyToIframe(this, isDark());
483 });
484 }
485 applyToIframe(frame, enabled);
486 }
487 }
488
489 /*
490 * The canvas iframe is mounted asynchronously, well after this script runs,
491 * and is replaced when the editor switches between visual and code view. One
492 * cheap querySelector per frame is enough to notice; the rAF gate keeps the
493 * editor's constant DOM churn from turning that into per-mutation work.
494 */
495 var scanScheduled = false;
496
497 function scheduleIframeScan() {
498 if (scanScheduled) {
499 return;
500 }
501 scanScheduled = true;
502 requestAnimationFrame(function () {
503 scanScheduled = false;
504 if (document.querySelector(IFRAME_SELECTOR)) {
505 applyToAllIframes(isDark());
506 }
507 });
508 }
509
510 var frameWatcher = null;
511
512 function watchForIframes() {
513 // Reachable from both boot paths and from every toggle; a second observer
514 // on the same body would double every scan for no benefit.
515 if (frameWatcher || !document.body) {
516 return;
517 }
518 frameWatcher = new MutationObserver(scheduleIframeScan);
519 frameWatcher.observe(document.body, {
520 childList: true,
521 subtree: true,
522 });
523 }
524
525 /* ---------------------------------------------------------------------- */
526 /* Apply */
527 /* ---------------------------------------------------------------------- */
528
529 /*
530 * Darkify's own screens are themed by their own design tokens, not by Dark
531 * Reader, so a chosen palette has to reach them a different way: by writing
532 * the palette's colours into those tokens directly.
533 *
534 * The map is deliberately partial. Four groups are left alone:
535 *
536 * --primary / --primary-foreground a contrast PAIR (near-white fill,
537 * near-black text). Overwriting half of
538 * it makes buttons unreadable.
539 * --destructive a delete button that stops being red
540 * stops communicating danger.
541 * --chart-* categorical colours; they have to stay
542 * distinguishable from each other.
543 * --sidebar-primary same pairing problem as --primary.
544 */
545 var SELF_THEMED_TOKENS = {
546 "--background": "bg",
547 "--card": "secondary_bg",
548 "--popover": "secondary_bg",
549 "--sidebar": "secondary_bg",
550 "--secondary": "secondary_bg",
551 "--muted": "secondary_bg",
552 "--accent": "secondary_bg",
553 "--sidebar-accent": "secondary_bg",
554 "--foreground": "text_color",
555 "--card-foreground": "text_color",
556 "--popover-foreground": "text_color",
557 "--secondary-foreground": "text_color",
558 "--accent-foreground": "text_color",
559 "--sidebar-foreground": "text_color",
560 "--sidebar-accent-foreground": "text_color",
561 "--muted-foreground": "input_text_color",
562 "--border": "border_color",
563 "--input": "border_color",
564 "--sidebar-border": "border_color",
565 "--ring": "link_color",
566 };
567
568 /**
569 * Push the active palette into the self-themed app's tokens, or clear them.
570 *
571 * Cleared on "auto" and in light mode alike, which hands the app back to the
572 * tokens its own stylesheet defines rather than leaving a half-applied
573 * palette behind.
574 */
575 function applySelfThemedTokens() {
576 var p = isDark() ? currentPalette() : null;
577
578 for (var token in SELF_THEMED_TOKENS) {
579 if (!Object.prototype.hasOwnProperty.call(SELF_THEMED_TOKENS, token)) {
580 continue;
581 }
582 if (p) {
583 HTML.style.setProperty(token, p[SELF_THEMED_TOKENS[token]]);
584 } else {
585 HTML.style.removeProperty(token);
586 }
587 }
588 }
589
590 /*
591 * Erase the horizontal seam near the bottom of short admin pages.
592 *
593 * wp-admin paints `body { background: #f0f0f1 }` and leaves <html> unpainted.
594 * Dark Reader derives body's colour from #f0f0f1 but paints <html> with
595 * darkSchemeBackgroundColor — a different value. When the content is shorter
596 * than the viewport, <html> shows below <body> and the join is visible. The
597 * admin menu column ends at the same line, which is what makes the seam run
598 * the full width of the page.
599 *
600 * Declaring `html { background-color: ${#f0f0f1} }` in fixes.css does not win:
601 * Dark Reader's own generated rule for the root element is `!important` too,
602 * and it is the one that applies. So rather than predicting the colour, read
603 * back the one it actually produced for <body> and pin <html> to it inline —
604 * an inline `!important` outranks any stylesheet, including its own.
605 *
606 * Runs twice on purpose. Dark Reader processes stylesheets as it finds them,
607 * and a sheet that arrives late (an admin page loading its own CSS) can change
608 * what body resolves to after the first read.
609 */
610 function syncRootBackground() {
611 if (!document.body) {
612 return;
613 }
614
615 var color = window.getComputedStyle(document.body).backgroundColor;
616
617 // Transparent means body is not painting anything of its own, so there is no
618 // second colour to reconcile and nothing to correct.
619 if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
620 HTML.style.removeProperty("background-color");
621 return;
622 }
623
624 HTML.style.setProperty("background-color", color, "important");
625 }
626
627 function scheduleRootBackgroundSync() {
628 if (typeof requestAnimationFrame === "function") {
629 requestAnimationFrame(syncRootBackground);
630 } else {
631 setTimeout(syncRootBackground, 0);
632 }
633 setTimeout(syncRootBackground, 300);
634 }
635
636 function apply() {
637 var enabled = isDark();
638
639 if (isSelfThemedScreen()) {
640 // The page's own CSS does the painting here; the palette reaches it
641 // through its tokens. Disable defensively in case a previous navigation
642 // on this document had Dark Reader on.
643 DarkReader.disable();
644 applySelfThemedTokens();
645 return;
646 }
647
648 if (enabled) {
649 DarkReader.enable(buildTheme(), buildFixes());
650 } else {
651 DarkReader.disable();
652 }
653
654 if (enabled) {
655 scheduleRootBackgroundSync();
656 } else {
657 HTML.style.removeProperty("background-color");
658 }
659
660 applyToAllIframes(enabled);
661 watchForIframes();
662 }
663
664 /**
665 * Keep the palette class on <html> in step with the frontend engine's
666 * convention, so CSS that keys off it (the switch, the admin bar icon) still
667 * matches. "auto" gets no class — there is no palette to name.
668 */
669 function applyPaletteClass() {
670 var classes = Array.prototype.slice.call(HTML.classList);
671 for (var i = 0; i < classes.length; i++) {
672 if (classes[i].indexOf("darkify-set") === 0) {
673 HTML.classList.remove(classes[i]);
674 }
675 }
676
677 var name = currentPaletteName();
678 if (name !== "auto") {
679 HTML.classList.add("darkify-" + name);
680 }
681 }
682
683 /* ---------------------------------------------------------------------- */
684 /* Public API (names are load-bearing — see the file header) */
685 /* ---------------------------------------------------------------------- */
686
687 window.darkify_switch_trigger = function () {
688 if (!optionEnabled()) {
689 return;
690 }
691 HTML.classList.toggle(DARK_CLASS);
692 writeState(isDark());
693 apply();
694 };
695
696 window.darkify_theme_select = function (theme) {
697 // "auto" is a valid choice, not a missing one: it hands the colours back to
698 // Dark Reader's derivation.
699 if (isPaletteValue(theme)) {
700 try {
701 localStorage.setItem(paletteKey(), theme);
702 } catch (e) {
703 // not remembered, still applied below
704 }
705 }
706
707 applyPaletteClass();
708 apply();
709 };
710
711 /** Exposed for support: what the engine thinks it is doing right now. */
712 window.darkifyAdminEngine = {
713 engine: "darkreader",
714 version: typeof DarkReader.getVersion === "function" ? DarkReader.getVersion() : null,
715 isDark: isDark,
716 palette: currentPaletteName,
717 paletteKey: paletteKey,
718 selfThemed: isSelfThemedScreen,
719 palettes: function () {
720 return ["auto"].concat(Object.keys(PALETTES));
721 },
722 theme: buildTheme,
723 fixes: buildFixes,
724 reapply: apply,
725
726 /**
727 * Preview unsaved palette choices.
728 *
729 * @param {{admin?: string, editor?: string}|null} overrides
730 * Palette ids to show, or null to drop back to saved values.
731 */
732 preview: function (overrides) {
733 previewOverrides = overrides || null;
734 applyPaletteClass();
735 apply();
736 },
737 };
738
739 /* ---------------------------------------------------------------------- */
740 /* Boot */
741 /* ---------------------------------------------------------------------- */
742
743 function init() {
744 // Cross-origin admin stylesheets (a CDN-hosted plugin sheet) are otherwise
745 // skipped, leaving patches of the admin un-darkened.
746 try {
747 DarkReader.setFetchMethod(window.fetch.bind(window));
748 } catch (e) {
749 // ignore
750 }
751
752 var on = optionEnabled() && readState();
753
754 if (on) {
755 HTML.classList.add(DARK_CLASS);
756 applyPaletteClass();
757 } else {
758 HTML.classList.remove(DARK_CLASS);
759 }
760
761 if (on && !isSelfThemedScreen()) {
762 DarkReader.enable(buildTheme(), buildFixes());
763 } else {
764 DarkReader.disable();
765 }
766
767 if (isSelfThemedScreen()) {
768 applySelfThemedTokens();
769 } else if (on) {
770 // init() runs in <head>, where there is no <body> to measure yet.
771 if (document.readyState === "loading") {
772 document.addEventListener("DOMContentLoaded", scheduleRootBackgroundSync);
773 } else {
774 scheduleRootBackgroundSync();
775 }
776 }
777 }
778
779 /*
780 * Two-stage boot, and the split is the whole point.
781 *
782 * The paint has to be claimed before the browser makes one: this script is
783 * printed in <head>, and Dark Reader is built to run there — enable() does not
784 * need a parsed body. Waiting for DOMContentLoaded to call it would let the
785 * light admin paint first and then swap, which is exactly the flash the
786 * pre-paint snippet in header_script.php exists to prevent.
787 *
788 * The iframe work genuinely does need a body to query and observe, so only
789 * that half waits.
790 */
791 init();
792
793 if (document.readyState === "loading") {
794 document.addEventListener("DOMContentLoaded", initFrames);
795 } else {
796 initFrames();
797 }
798
799 function initFrames() {
800 if (isSelfThemedScreen()) {
801 return;
802 }
803
804 /*
805 * init() ran in <head>, where there is no <body> to test — so
806 * isEditorContext() was necessarily false and the page was themed with the
807 * admin palette. On the block editor that is the wrong one, and it would
808 * have left the editor's chrome on the admin palette while only the canvas
809 * picked up the editor's. Now that the body exists the context is knowable,
810 * so re-theme the page before touching the frames.
811 */
812 if (isEditorContext()) {
813 apply();
814 return; // apply() reaches the frames itself
815 }
816
817 applyToAllIframes(isDark());
818 watchForIframes();
819 }
820 })();
821