initially, and then core's * `svg-painter` inlines/recolors them, causing a visible "black → scheme color" flash. * This helper pre-empts that by forcing WordPress to use the `.wp-menu-image:before` * pseudo-element (via a Dashicon placeholder) and then applying the SVG as a CSS mask. * * @since 7.6.0 */ namespace ASENHA\Classes; class Admin_Menu_Svg_Icon_Mask { /** * Registered SVG data URIs keyed by post type key. * * @since 7.6.0 * @var array */ private static $post_type_svgs = array(); /** * Registered SVG data URIs keyed by top-level menu slug (from add_menu_page()). * * @since 7.6.0 * @var array */ private static $toplevel_menu_slug_svgs = array(); /** * Registered SVG data URIs keyed by a substring to match against `href`. * * This is useful for menu slugs that are full URLs/queries (e.g. `post.php?post=123&action=edit`). * * @since 7.6.0 * @var array */ private static $href_contains_svgs = array(); /** * Check whether a string is an SVG data URI that WordPress would render as an . * * @since 7.6.0 * * @param mixed $value Potential menu icon. * @return bool */ public static function is_svg_data_uri( $value ) { return is_string( $value ) && ( 0 === strpos( $value, 'data:image/svg+xml' ) ); } /** * Register an SVG icon for a post type's admin menu entry. * * @since 7.6.0 * * @param string $post_type Post type key. * @param string $svg_data_uri SVG data URI. * @return void */ public static function register_post_type_svg_icon( $post_type, $svg_data_uri ) { $post_type = sanitize_key( (string) $post_type ); $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $post_type || '' === $svg_data_uri ) { return; } if ( ! self::is_svg_data_uri( $svg_data_uri ) ) { return; } self::$post_type_svgs[ $post_type ] = $svg_data_uri; } /** * Register an SVG icon for a top-level menu entry created by add_menu_page(). * * @since 7.6.0 * * @param string $menu_slug Menu slug passed to add_menu_page(). * @param string $svg_data_uri SVG data URI. * @return void */ public static function register_toplevel_menu_slug_svg_icon( $menu_slug, $svg_data_uri ) { $menu_slug = (string) $menu_slug; $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $menu_slug || '' === $svg_data_uri ) { return; } if ( ! self::is_svg_data_uri( $svg_data_uri ) ) { return; } self::$toplevel_menu_slug_svgs[ $menu_slug ] = $svg_data_uri; } /** * Register an SVG icon for a menu entry by matching a substring within the anchor href. * * @since 7.6.0 * * @param string $needle Substring expected in href. * @param string $svg_data_uri SVG data URI. * @return void */ public static function register_menu_href_contains_svg_icon( $needle, $svg_data_uri ) { $needle = trim( (string) $needle ); $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $needle || '' === $svg_data_uri ) { return; } if ( ! self::is_svg_data_uri( $svg_data_uri ) ) { return; } self::$href_contains_svgs[ $needle ] = $svg_data_uri; } /** * Enqueue inline CSS masks for all registered menu SVG icons. * * @since 7.6.0 * * @return void */ public static function enqueue_mask_css() { if ( empty( self::$post_type_svgs ) && empty( self::$toplevel_menu_slug_svgs ) && empty( self::$href_contains_svgs ) ) { return; } $css = ''; foreach ( self::$post_type_svgs as $post_type => $svg_data_uri ) { $post_type = sanitize_key( (string) $post_type ); $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $post_type || '' === $svg_data_uri || ! self::is_svg_data_uri( $svg_data_uri ) ) { continue; } $selector = '#adminmenu .menu-icon-' . $post_type . ' .wp-menu-image:before'; $css .= self::build_mask_rule( $selector, $svg_data_uri ); } foreach ( self::$toplevel_menu_slug_svgs as $menu_slug => $svg_data_uri ) { $menu_slug = (string) $menu_slug; $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $menu_slug || '' === $svg_data_uri || ! self::is_svg_data_uri( $svg_data_uri ) ) { continue; } $selectors = array( '#toplevel_page_' . sanitize_title( $menu_slug ) . ' .wp-menu-image:before', '#adminmenu a[href*="' . self::escape_css_attr_value( $menu_slug ) . '"] .wp-menu-image:before', ); $css .= self::build_mask_rule( implode( ',' . PHP_EOL, $selectors ), $svg_data_uri ); } foreach ( self::$href_contains_svgs as $needle => $svg_data_uri ) { $needle = trim( (string) $needle ); $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $needle || '' === $svg_data_uri || ! self::is_svg_data_uri( $svg_data_uri ) ) { continue; } $selector = '#adminmenu a[href*="' . self::escape_css_attr_value( $needle ) . '"] .wp-menu-image:before'; $css .= self::build_mask_rule( $selector, $svg_data_uri ); } if ( '' === $css ) { return; } wp_add_inline_style( 'asenha-wp-admin', $css ); } /** * Build a CSS rule that masks a WP admin menu icon pseudo-element. * * @since 7.6.0 * * @param string $selector CSS selector(s). * @param string $svg_data_uri SVG data URI. * @return string */ private static function build_mask_rule( $selector, $svg_data_uri ) { $selector = trim( (string) $selector ); $svg_data_uri = trim( (string) $svg_data_uri ); if ( '' === $selector || '' === $svg_data_uri ) { return ''; } return $selector . '{' . PHP_EOL . "\tcontent:\"\";" . PHP_EOL . "\tbackground-color:currentColor;" . PHP_EOL . "\t-webkit-mask-image:url(\"" . $svg_data_uri . "\");" . PHP_EOL . "\tmask-image:url(\"" . $svg_data_uri . "\");" . PHP_EOL . "\t-webkit-mask-repeat:no-repeat;" . PHP_EOL . "\tmask-repeat:no-repeat;" . PHP_EOL . "\t-webkit-mask-position:center;" . PHP_EOL . "\tmask-position:center;" . PHP_EOL . "\t-webkit-mask-size:20px 20px;" . PHP_EOL . "\tmask-size:20px 20px;" . PHP_EOL . '}' . PHP_EOL; } /** * Escape a value used inside a double-quoted CSS attribute selector. * * @since 7.6.0 * * @param string $value Raw value. * @return string */ private static function escape_css_attr_value( $value ) { return str_replace( array( '\\', '"' ), array( '\\\\', '\"' ), (string) $value ); } }