| 1 |
<?php |
| 2 |
/** |
| 3 |
* Desktop Mode — Appearance window tab adaptations. |
| 4 |
* |
| 5 |
* The Appearance dock item opens an iframe of `themes.php` and uses |
| 6 |
* the in-window submenu strip (built from the WP `$submenu` global) |
| 7 |
* as its tab bar. WP Core does not register `theme-install.php` as a |
| 8 |
* submenu of `themes.php`; classic admin instead surfaces it through |
| 9 |
* the in-page "Add Theme" `.page-title-action` button. |
| 10 |
* |
| 11 |
* Inside chromeless we hide that button (it scrolled out of the |
| 12 |
* iframe's visible area on first paint, leaving no entry point to |
| 13 |
* the install flow — see assets/css/chromeless.css). This file |
| 14 |
* compensates by injecting an "Add Theme" tab at the front of the |
| 15 |
* Appearance window's submenu strip via the `desktop_mode_dock_item` |
| 16 |
* filter. |
| 17 |
* |
| 18 |
* Resulting tab order: Appearance | Add Theme | Editor | Fonts | … |
| 19 |
* |
| 20 |
* @package Desktop_Mode |
| 21 |
* @since 0.8.2 |
| 22 |
*/ |
| 23 |
|
| 24 |
defined( 'ABSPATH' ) || exit; |
| 25 |
|
| 26 |
/** |
| 27 |
* Prepends an "Add Theme" entry to the Appearance dock item's submenu. |
| 28 |
* |
| 29 |
* Called once per dock item via {@see desktop_mode_build_dock_items()}. |
| 30 |
* Skipped for every menu other than `themes.php` and for users who |
| 31 |
* lack the `install_themes` capability — matching what classic |
| 32 |
* admin's "Add Theme" page-title-action enforces. |
| 33 |
* |
| 34 |
* @since 0.8.2 |
| 35 |
* |
| 36 |
* @param array $dock_item The dock item data (id, title, icon, url, |
| 37 |
* badge, submenu, multi, placement, isCore). |
| 38 |
* @param string $menu_slug The menu slug — e.g. `themes.php`. |
| 39 |
* @return array Filtered dock item. |
| 40 |
*/ |
| 41 |
function desktop_mode_inject_appearance_tabs( $dock_item, $menu_slug ) { |
| 42 |
if ( 'themes.php' !== $menu_slug ) { |
| 43 |
return $dock_item; |
| 44 |
} |
| 45 |
|
| 46 |
if ( ! current_user_can( 'install_themes' ) ) { |
| 47 |
return $dock_item; |
| 48 |
} |
| 49 |
|
| 50 |
if ( ! isset( $dock_item['submenu'] ) || ! is_array( $dock_item['submenu'] ) ) { |
| 51 |
$dock_item['submenu'] = array(); |
| 52 |
} |
| 53 |
|
| 54 |
// `?browse=popular` lands on the Popular tab (the JS installer |
| 55 |
// router would default-redirect to this anyway when no sort is |
| 56 |
// specified — passing it explicitly makes the intent visible |
| 57 |
// AND avoids the brief flash where WP's Backbone router rewrites |
| 58 |
// the URL after first paint, which inside the iframe re-arms the |
| 59 |
// chromeless body class flicker). Other valid values: `new` |
| 60 |
// (Latest), `block-themes`, `favorites`. |
| 61 |
$add_theme = array( |
| 62 |
'title' => __( 'Add Theme', 'desktop-mode' ), |
| 63 |
'url' => admin_url( 'theme-install.php?browse=popular' ), |
| 64 |
); |
| 65 |
|
| 66 |
// Idempotent: skip if a previous filter pass (or another plugin) |
| 67 |
// already added an entry pointing at theme-install.php. |
| 68 |
foreach ( $dock_item['submenu'] as $existing ) { |
| 69 |
if ( ! empty( $existing['url'] ) && false !== strpos( $existing['url'], 'theme-install.php' ) ) { |
| 70 |
return $dock_item; |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
array_unshift( $dock_item['submenu'], $add_theme ); |
| 75 |
|
| 76 |
return $dock_item; |
| 77 |
} |
| 78 |
add_filter( 'desktop_mode_dock_item', 'desktop_mode_inject_appearance_tabs', 10, 2 ); |
| 79 |
|
| 80 |
/** |
| 81 |
* Fixes the visible "active tab" state inside chromeless |
| 82 |
* theme-install.php iframes. |
| 83 |
* |
| 84 |
* Background: WP's installer JS uses a Backbone router whose pattern |
| 85 |
* `theme-install.php?browse=:sort` greedy-captures everything past |
| 86 |
* `browse=` ([^/?]+ matches `&` too). Inside chromeless the URL also |
| 87 |
* carries `desktop_mode_chromeless=1`, so `:sort` ends up as |
| 88 |
* `popular&desktop_mode_chromeless=1`. WP's `view.sort()` then runs |
| 89 |
* with that polluted value and the `[data-sort]` selector finds |
| 90 |
* nothing — leaving every tab in the unselected state even though |
| 91 |
* the popular themes ARE the rendered listing. |
| 92 |
* |
| 93 |
* Reordering query params or re-encoding doesn't help: any URL that |
| 94 |
* exposes `browse=` to the router matches the route, and any URL |
| 95 |
* that hides it falls through to a redirect that strips the |
| 96 |
* chromeless flag (which would re-render the iframe with full admin |
| 97 |
* chrome on next paint). |
| 98 |
* |
| 99 |
* The minimum-viable shim: inline JS that reads the real `browse` |
| 100 |
* value via URLSearchParams, finds the matching `[data-sort]` tab, |
| 101 |
* and sets `current` + `aria-current` on it. A MutationObserver on |
| 102 |
* `.filter-links` keeps the tab synced if WP's router fires again |
| 103 |
* (e.g. the user picks Latest, then comes back to Popular via the |
| 104 |
* route's pushState). |
| 105 |
* |
| 106 |
* @since 0.8.2 |
| 107 |
*/ |
| 108 |
function desktop_mode_theme_install_active_tab_script() { |
| 109 |
if ( ! desktop_mode_is_chromeless_request() ) { |
| 110 |
return; |
| 111 |
} |
| 112 |
if ( ! isset( $GLOBALS['pagenow'] ) || 'theme-install.php' !== $GLOBALS['pagenow'] ) { |
| 113 |
return; |
| 114 |
} |
| 115 |
|
| 116 |
$js = <<<'JS' |
| 117 |
( function () { |
| 118 |
var browseParam = new URLSearchParams( window.location.search ).get( 'browse' ); |
| 119 |
if ( ! browseParam ) { |
| 120 |
return; |
| 121 |
} |
| 122 |
function applyActiveTab() { |
| 123 |
var match = document.querySelector( |
| 124 |
'.filter-links li > a[data-sort="' + browseParam + '"]' |
| 125 |
); |
| 126 |
if ( ! match ) { |
| 127 |
return false; |
| 128 |
} |
| 129 |
var tabs = document.querySelectorAll( '.filter-links li > a[data-sort]' ); |
| 130 |
var alreadyCorrect = |
| 131 |
match.classList.contains( 'current' ) && |
| 132 |
Array.prototype.every.call( tabs, function ( a ) { |
| 133 |
return a === match || ! a.classList.contains( 'current' ); |
| 134 |
} ); |
| 135 |
if ( alreadyCorrect ) { |
| 136 |
return true; |
| 137 |
} |
| 138 |
Array.prototype.forEach.call( tabs, function ( a ) { |
| 139 |
a.classList.remove( 'current' ); |
| 140 |
a.removeAttribute( 'aria-current' ); |
| 141 |
} ); |
| 142 |
match.classList.add( 'current' ); |
| 143 |
match.setAttribute( 'aria-current', 'page' ); |
| 144 |
return true; |
| 145 |
} |
| 146 |
function init() { |
| 147 |
if ( ! applyActiveTab() ) { |
| 148 |
window.requestAnimationFrame( init ); |
| 149 |
return; |
| 150 |
} |
| 151 |
var container = document.querySelector( '.filter-links' ); |
| 152 |
if ( ! container ) { |
| 153 |
return; |
| 154 |
} |
| 155 |
var pending = false; |
| 156 |
var observer = new MutationObserver( function () { |
| 157 |
if ( pending ) { |
| 158 |
return; |
| 159 |
} |
| 160 |
pending = true; |
| 161 |
window.requestAnimationFrame( function () { |
| 162 |
pending = false; |
| 163 |
applyActiveTab(); |
| 164 |
} ); |
| 165 |
} ); |
| 166 |
observer.observe( container, { |
| 167 |
attributes: true, |
| 168 |
subtree: true, |
| 169 |
attributeFilter: [ 'class', 'aria-current' ], |
| 170 |
} ); |
| 171 |
} |
| 172 |
if ( document.readyState === 'loading' ) { |
| 173 |
document.addEventListener( 'DOMContentLoaded', init ); |
| 174 |
} else { |
| 175 |
init(); |
| 176 |
} |
| 177 |
} )(); |
| 178 |
JS; |
| 179 |
|
| 180 |
wp_print_inline_script_tag( $js ); |
| 181 |
} |
| 182 |
add_action( 'admin_footer', 'desktop_mode_theme_install_active_tab_script', 100 ); |
| 183 |
|