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/Menu.php +111 -1 2.0.12.1.3 View file →
@@ -48,8 +48,10 @@
48 48
49 49 public function __construct()
50 50 {
51 51 \add_action('admin_menu', [$this, 'register']);
52 + \add_action('admin_head', [$this, 'menu_icon_style']);
53 + \add_action('admin_footer', [$this, 'upgrade_link_new_tab']);
52 54 // Old bookmarks / links to the retired "Get Help" screen are forwarded to
53 55 // the SPA's own Help route (see redirect_legacy_help_page()).
54 56 \add_action('admin_page_access_denied', [$this, 'redirect_legacy_help_page']);
55 57 }
@@ -111,8 +113,45 @@
111 113 );
112 114 }
113 115
114 116 /**
117 + * Open the "Upgrade To Pro!" submenu link in a new tab.
118 + *
119 + * That submenu is registered with an external URL as its slug, so WordPress
120 + * renders it as an ordinary anchor into wp-admin's menu. Leaving wp-admin
121 + * entirely is not what a menu click should do — someone half-way through
122 + * configuring the plugin loses the screen they were on, and the back button
123 + * is their only way back.
124 + *
125 + * There is no filter for the attributes of that anchor: wp-admin builds the
126 + * markup itself in wp-admin/menu-header.php, and the only thing under our
127 + * control is the title string, which is escaped into the link text. So the
128 + * attributes are set client-side instead, keyed off the class the title
129 + * already carries.
130 + *
131 + * `rel` is not optional here. A `target="_blank"` link hands the opened page
132 + * a `window.opener` reference to this one; `noopener` severs it, and
133 + * `noreferrer` keeps the admin URL (which can carry screen and query
134 + * context) out of the referrer header sent to the marketing site.
135 + */
136 + public function upgrade_link_new_tab(): void
137 + {
138 + ?>
139 + <script>
140 + (function () {
141 + var label = document.querySelector('#adminmenu .darkify-get-pro-text');
142 + var link = label && label.closest('a');
143 + if (!link) {
144 + return;
145 + }
146 + link.target = '_blank';
147 + link.rel = 'noopener noreferrer';
148 + })();
149 + </script>
150 + <?php
151 + }
152 +
153 + /**
115 154 * Forward the retired `?page=darkify-help` screen to the SPA's Help route.
116 155 *
117 156 * "Get Help" used to be its own menu page; it is now a route inside the one
118 157 * SPA page. Without this, an existing bookmark (or any third-party link) to
@@ -138,8 +177,70 @@
138 177 exit;
139 178 }
140 179
141 180 /**
181 + * Make the menu icon take its colour from the menu, like every other icon.
182 + *
183 + * WordPress renders a `data:` menu icon as a CSS `background-image` on
184 + * `div.wp-menu-image.svg`, and ours has `fill="white"` baked into the SVG. A
185 + * background-image cannot inherit `color`, so while WordPress's own icons are
186 + * dashicons — font glyphs that follow the menu's text colour and dim to 0.6
187 + * opacity — ours painted flat white and read as brighter than everything
188 + * around it. Dark mode made it obvious, but it was never matching; the dark
189 + * sidebar just hid it.
190 + *
191 + * Switching from background-image to a `mask` inverts the relationship: the
192 + * SVG becomes a stencil and `background-color: currentColor` supplies the
193 + * paint, so the icon follows the menu's colour exactly as a dashicon does —
194 + * under any admin colour scheme, under any Darkify palette, with no colour
195 + * hardcoded here.
196 + *
197 + * Printed from the same schema value `register()` passes to add_menu_page(),
198 + * so there is only ever one copy of the SVG to keep correct.
199 + */
200 + public function menu_icon_style(): void
201 + {
202 + $args = SchemaRegistry::options(self::OPTION_KEY);
203 + $icon = $args['menu_icon'] ?? '';
204 +
205 + /*
206 + * Dashicon slugs and image files are already coloured (or already correct)
207 + * by WordPress; only a base64 inline SVG needs this treatment.
208 + *
209 + * Matched against a strict pattern rather than escaped with esc_url():
210 + * `data:` is not in wp_allowed_protocols(), so esc_url() strips the whole
211 + * value and the icon disappears. The pattern is what makes it safe to
212 + * print raw — base64 cannot contain the `<` that would be needed to close
213 + * the surrounding <style> element, and nothing outside the alphabet gets
214 + * through.
215 + */
216 + if (! \is_string($icon) || ! \preg_match('#^data:image/svg\+xml;base64,[A-Za-z0-9+/]+={0,2}$#', $icon)) {
217 + return;
218 + }
219 +
220 + $selector = '#adminmenu .toplevel_page_' . self::PAGE_SLUG . ' div.wp-menu-image.svg';
221 + $url = "url('" . $icon . "')";
222 +
223 + $css = $selector . '{'
224 + . 'background-image:none!important;'
225 + . '-webkit-mask:' . $url . ' no-repeat center;'
226 + . 'mask:' . $url . ' no-repeat center;'
227 + . '-webkit-mask-size:20px auto;'
228 + . 'mask-size:20px auto;'
229 + . 'background-color:currentColor!important;'
230 + // Match the 0.6 dashicons rest at, and the full opacity they take on
231 + // hover and while current.
232 + . 'opacity:.6;'
233 + . '}'
234 + . '#adminmenu .toplevel_page_' . self::PAGE_SLUG . ':hover div.wp-menu-image.svg,'
235 + . '#adminmenu .toplevel_page_' . self::PAGE_SLUG . '.current div.wp-menu-image.svg,'
236 + . '#adminmenu .toplevel_page_' . self::PAGE_SLUG . '.wp-has-current-submenu div.wp-menu-image.svg'
237 + . '{opacity:1;}';
238 +
239 + echo '<style id="darkify-menu-icon">' . $css . '</style>';
240 + }
241 +
242 + /**
142 243 * The page body: just the node React mounts on. Every menu entry renders this
143 244 * same root — the SPA reads the `?page=` slug to decide which route to open.
144 245 *
145 246 * The `darkify_ignore` class opts the whole React admin out of Darkify's own
@@ -151,10 +252,19 @@
151 252 * inline background was overwritten so every preview looked empty. The React
152 253 * admin already themes itself via the `.dark` class, so the engine must leave
153 254 * its subtree alone; `.darkify_ignore` / `.darkify_ignore *` is that engine's
154 255 * built-in exclusion hook.
256 + *
257 + * `translate="no"` / `.notranslate` opts the subtree out of browser page
258 + * translation (Chrome, Edge). Those translators replace each text node with
259 + * a `<font>` wrapper; React later tries to remove the original node, hits
260 + * "Failed to execute 'removeChild' on 'Node'", and the whole app unmounts.
261 + * Because saving re-renders (spinner, then toast), the crash landed exactly
262 + * on Save and looked like settings failing to save. The panel ships real
263 + * translations instead — see languages/ — so nothing is lost by declining
264 + * the browser's.
155 265 */
156 266 public function render(): void
157 267 {
158 - echo '<div id="darkify_react" class="darkify_ignore"></div>';
268 + echo '<div id="darkify_react" class="darkify_ignore notranslate" translate="no"></div>';
159 269 }
160 270 }