| @@ -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 |
| @@ -135,8 +174,70 @@ | ||
| 135 | 174 | } |
| 136 | 175 | |
| 137 | 176 | \wp_safe_redirect(\admin_url('admin.php?page=' . self::PAGE_SLUG . '#/help')); |
| 138 | 177 | exit; |
| 178 | + } | |
| 179 | + | |
| 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>'; | |
| 139 | 240 | } |
| 140 | 241 | |
| 141 | 242 | /** |
| 142 | 243 | * The page body: just the node React mounts on. Every menu entry renders this |