PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 2.1.3
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v2.1.3
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
← All changes | src/Admin/Admin.php +187 -6 2.0.42.1.3 View file →
@@ -42,8 +42,17 @@
42 42 private $version;
43 43 public $unique_id;
44 44
45 45 /**
46 + * Whether enqueue_scripts() actually put the admin engine in the queue on
47 + * this request. Read by darkify_restore_no_conflict_scripts(), which must
48 + * only re-add handles Darkify itself asked for.
49 + *
50 + * @var bool
51 + */
52 + private $engine_enqueued = false;
53 +
54 + /**
46 55 * The class constructor.
47 56 *
48 57 * @param string $plugin_name The slug of the plugin.
49 58 * @param string $version Current version of the plugin.
@@ -98,8 +107,30 @@
98 107 // src/Admin/Views/*.php, so the text domain has to be registered first —
99 108 // Darkify::load_textdomain() runs on this same hook at priority 1.
100 109 add_action('after_setup_theme', array($this, 'init_components'));
101 110 add_filter('autoptimize_filter_js_exclude', array($this, 'darkify_exclude_js_from_cache_plugins'));
111 +
112 + /*
113 + * "No-conflict mode" compatibility.
114 + *
115 + * Fluent Forms, FluentCRM, Fluent Booking, FluentSMTP, WP Social Ninja
116 + * and friends dequeue *every* script served from wp-content/plugins on
117 + * their own admin screens (Fluent Forms: app/Hooks/actions.php, on
118 + * `wp_print_scripts` priority 1). That runs after `admin_enqueue_scripts`,
119 + * so it strips Darkify's engine while the admin-bar node — printed by
120 + * `admin_bar_menu` and untouched by the dequeue — keeps its inline
121 + * onclick, producing "darkify_switch_trigger is not defined" and a dead
122 + * toggle on e.g. admin.php?page=fluent_forms.
123 + *
124 + * Two layers: the published allow-list filters where a plugin offers one,
125 + * and a generic re-enqueue after their sweep for the ones that don't.
126 + */
127 + add_filter('fluentform/exclude_js_slugs_from_dequeue', array($this, 'darkify_no_conflict_allow_slugs'));
128 + add_filter('fluent_crm_asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
129 + add_filter('fluent_booking/asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
130 + add_action('wp_print_scripts', array($this, 'darkify_restore_no_conflict_scripts'), 100);
131 + add_action('wp_print_styles', array($this, 'darkify_restore_no_conflict_styles'), 100);
132 +
102 133 add_action('admin_init', [$this, 'load_classic_editor_scripts']);
103 134 add_filter('admin_footer_text', array($this, 'darkify_admin_footer'), 1, 2);
104 135 $active_plugins = get_option('active_plugins');
105 136 foreach ($active_plugins as $active_plugin) {
@@ -167,9 +198,14 @@
167 198 }
168 199
169 200 public function darkify_exclude_js_from_cache_plugins($excludes)
170 201 {
171 - $excludes .= ',client_main.js,client_main.min.js';
202 + // Dark Reader proxies document.styleSheets and injects its own <style>
203 + // elements; an optimizer that concatenates or defers it changes when it
204 + // sees the page's sheets, which shows up as a half-darkened admin.
205 + $excludes .= ',client_main.js,client_main.min.js'
206 + . ',darkreader.js,darkreader.min.js'
207 + . ',admin_darkreader.js,admin_darkreader.min.js';
172 208 return $excludes;
173 209 }
174 210
175 211 public function enqueue_styles()
@@ -218,17 +254,128 @@
218 254 return;
219 255 }
220 256 }
221 257
258 + /*
259 + * wp-admin runs the Dark Reader engine, NOT the per-element classifier in
260 + * client_main.js. That engine walks every element and reads
261 + * getComputedStyle on each, then re-walks on DOM mutation; wp-admin (the
262 + * block editor especially) mutates continuously, so it never settles and
263 + * the editor locks up. Dark Reader works per stylesheet instead, so the
264 + * cost does not scale with element churn at all.
265 + *
266 + * The frontend is untouched and still runs client_main.js.
267 + * See src/assets/js/admin_darkreader.js for the full rationale.
268 + */
269 + $darkify_lib_rel = 'src/assets/js/vendor/darkreader.min.js';
270 + $darkify_engine_rel = 'src/assets/js/admin_darkreader' . $this->min . '.js';
271 + $darkify_lib_url = DARKIFY_DIR_URL . $darkify_lib_rel;
272 +
273 + /*
274 + * Both in <head> (in_footer = false), deliberately. Dark Reader does not
275 + * need a parsed body to start, and enabling it before first paint is what
276 + * keeps the admin from flashing light and then darkening.
277 + */
222 278 wp_enqueue_script(
223 - 'darkify-admin-client-main',
224 - DARKIFY_DIR_URL . 'src/assets/js/client_main' . $this->min . '.js',
279 + 'darkify-darkreader',
280 + $darkify_lib_url,
225 281 array(),
226 - $this->darkify_asset_version('src/assets/js/client_main' . $this->min . '.js')
282 + $this->darkify_asset_version($darkify_lib_rel),
283 + false
227 284 );
285 +
286 + /*
287 + * The block editor canvas and the classic editor's TinyMCE body are
288 + * separate documents, and Dark Reader binds the realm it was loaded into.
289 + * The engine injects its own copy into each same-origin frame, so it
290 + * needs a URL it can point a <script> at from inside that frame.
291 + *
292 + * Darkify's own React settings screens are excluded from Dark Reader:
293 + * they ship a full dark theme of their own (`.dark` in
294 + * darkify-react/src/index.css) which also covers the surrounding wp-admin
295 + * chrome, so darkening them again produces a double-dark UI — and because
296 + * Dark Reader 4.9 cannot parse the `oklch()` values those tokens use, the
297 + * unresolved properties settle on `--destructive` and paint card borders
298 + * and toggles red. The engine still loads there: it owns the toggle the
299 + * admin-bar icon calls, and the SPA mirrors the class onto `.dark` itself.
300 + */
301 + wp_add_inline_script(
302 + 'darkify-darkreader',
303 + 'window.darkifyDarkReaderSrc = ' . wp_json_encode($darkify_lib_url) . ';'
304 + . 'window.darkifyAdminSelfThemed = ' . ($is_spa ? 'true' : 'false') . ';',
305 + 'before'
306 + );
307 +
308 + wp_enqueue_script(
309 + 'darkify-admin-engine',
310 + DARKIFY_DIR_URL . $darkify_engine_rel,
311 + array('darkify-darkreader'),
312 + $this->darkify_asset_version($darkify_engine_rel),
313 + false
314 + );
315 +
316 + $this->engine_enqueued = true;
228 317 }
229 318
230 319 /**
320 + * Add Darkify to a no-conflict allow-list.
321 + *
322 + * Both filter shapes this is attached to pass an array of substrings matched
323 + * against the script `src`, so the plugin folder name is the right token.
324 + *
325 + * @param array $slugs Allowed slugs.
326 + *
327 + * @return array
328 + */
329 + public function darkify_no_conflict_allow_slugs($slugs)
330 + {
331 + if (! is_array($slugs)) {
332 + return $slugs;
333 + }
334 +
335 + $slugs[] = 'darkify';
336 +
337 + return array_unique($slugs);
338 + }
339 +
340 + /**
341 + * Put the admin engine back after a no-conflict sweep dropped it.
342 + *
343 + * Runs on `wp_print_scripts` at 100 — after the sweeps, which all hook it at
344 + * 1 — and before `do_items()`, so a re-enqueue here still prints. Gated on
345 + * `$engine_enqueued`, so it never resurrects a script Darkify itself chose
346 + * not to load (or dequeued in admin_dequeue_for_specefic_pages).
347 + */
348 + public function darkify_restore_no_conflict_scripts()
349 + {
350 + if (! $this->engine_enqueued || ! is_admin()) {
351 + return;
352 + }
353 +
354 + foreach (array('darkify-darkreader', 'darkify-admin-engine') as $handle) {
355 + if (wp_script_is($handle, 'registered') && ! wp_script_is($handle, 'enqueued')) {
356 + wp_enqueue_script($handle);
357 + }
358 + }
359 + }
360 +
361 + /**
362 + * Same sweep, styles side: the engine adds `.darkify_dark_mode_enabled` and
363 + * the switch classes, and client_main.css is what they mean. Losing only the
364 + * stylesheet leaves a half-styled admin bar icon.
365 + */
366 + public function darkify_restore_no_conflict_styles()
367 + {
368 + if (! $this->engine_enqueued || ! is_admin()) {
369 + return;
370 + }
371 +
372 + if (wp_style_is('darkify-admin-switch', 'registered') && ! wp_style_is('darkify-admin-switch', 'enqueued')) {
373 + wp_enqueue_style('darkify-admin-switch');
374 + }
375 + }
376 +
377 + /**
231 378 * Cache-busting version for a bundled asset: the plugin version plus the
232 379 * file's mtime, so rebuilding client_main.js/.css (which does NOT bump the
233 380 * plugin version) still changes the URL and browsers fetch the new file
234 381 * instead of serving a stale one. Mirrors Admin\Assets::asset_version().
@@ -294,8 +441,12 @@
294 441
295 442 if (in_array($page, $dequeue_pages, true)) {
296 443 wp_deregister_style('darkify-admin-switch');
297 444 wp_dequeue_script('darkify-admin-client-main');
445 + wp_dequeue_script('darkify-admin-engine');
446 + wp_dequeue_script('darkify-darkreader');
447 + // Keep darkify_restore_no_conflict_scripts() from undoing this.
448 + $this->engine_enqueued = false;
298 449 }
299 450 }
300 451
301 452 /**
@@ -365,8 +516,28 @@
365 516 if (! $this->darkify_is_dark_mode_allowed()) {
366 517 return;
367 518 }
368 519
520 + /*
521 + * The block editor already carries its own toggle in the top toolbar
522 + * (see Frontend/Templates/footer_script.php), firing the same
523 + * darkify_switch_trigger() this node would — two controls for one state,
524 + * side by side.
525 + *
526 + * The toolbar one is the keeper, not this one: Gutenberg hides the WP
527 + * admin bar in fullscreen mode, which is the default, so this node is
528 + * simply absent for most people editing a post. Dropping the toolbar
529 + * button instead would leave them no toggle at all.
530 + *
531 + * The classic editor is deliberately not included here. Its TinyMCE moon
532 + * toggles only the content frame, while this switch toggles the whole
533 + * admin — two different controls, so both belong on screen.
534 + */
535 + $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
536 + if ($screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
537 + return;
538 + }
539 +
369 540 $options = get_option('darkify');
370 541 $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
371 542
372 543 // Everywhere but Darkify's own screens the node is only registered when
@@ -383,9 +554,16 @@
383 554 'parent' => 'top-secondary',
384 555 'id' => 'darkify_admin_bar_switch_container',
385 556 'meta' => array(
386 557 'class' => $classes,
387 - 'onclick' => 'darkify_switch_trigger()',
558 + // Guarded call: a plugin's "no-conflict mode" can still strip the
559 + // engine on its own screens (see the compatibility hooks in the
560 + // constructor). A missing function should be an inert click, not
561 + // an uncaught ReferenceError in every visitor's console.
562 + // No quotes in the expression: the admin bar escapes `meta`
563 + // attributes, and the escaped quotes come back as literal
564 + // entities inside the handler ("Invalid or unexpected token").
565 + 'onclick' => 'window.darkify_switch_trigger&&window.darkify_switch_trigger()',
388 566 ),
389 567 ));
390 568 }
391 569
@@ -441,9 +619,12 @@
441 619 return;
442 620 }
443 621
444 622 add_filter('mce_external_plugins', function ($plugins) {
445 - $plugins['darkify_button'] = plugins_url('src/assets/js/admin-classic-editor.js', DARKIFY_FILE);
623 + // Honour the same debug switch as every other bundled asset; gulp
624 + // builds the .min beside it and this was the one enqueue still
625 + // pointing at the readable copy in production.
626 + $plugins['darkify_button'] = plugins_url('src/assets/js/admin-classic-editor' . $this->min . '.js', DARKIFY_FILE);
446 627 return $plugins;
447 628 });
448 629
449 630 add_filter('mce_buttons', function ($buttons) {