PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / themes-tabs.php

themes-tabs.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/themes-tabs.php

261 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — 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 `openstation_dock_item`
16 * filter.
17 *
18 * Resulting tab order: Appearance | Add Theme | Editor | Fonts | …
19 *
20 * @package OpenStation
21 */
22
23 defined( 'ABSPATH' ) || exit;
24
25 /**
26 * Prepends an "Add Theme" entry to the Appearance dock item's submenu.
27 *
28 * Called once per dock item via {@see openstation_build_dock_items()}.
29 * Skipped for every menu other than `themes.php` and for users who
30 * lack the `install_themes` capability — matching what classic
31 * admin's "Add Theme" page-title-action enforces.
32 *
33 * @param array $dock_item The dock item data (id, title, icon, url,
34 * badge, submenu, multi, placement, isCore).
35 * @param string $menu_slug The menu slug — e.g. `themes.php`.
36 * @return array Filtered dock item.
37 */
38 function openstation_inject_appearance_tabs( $dock_item, $menu_slug ) {
39 if ( 'themes.php' !== $menu_slug ) {
40 return $dock_item;
41 }
42
43 if ( ! current_user_can( 'install_themes' ) ) {
44 return $dock_item;
45 }
46
47 if ( ! isset( $dock_item['submenu'] ) || ! is_array( $dock_item['submenu'] ) ) {
48 $dock_item['submenu'] = array();
49 }
50
51 // `?browse=popular` lands on the Popular tab (the JS installer
52 // router would default-redirect to this anyway when no sort is
53 // specified — passing it explicitly makes the intent visible
54 // AND avoids the brief flash where WP's Backbone router rewrites
55 // the URL after first paint, which inside the iframe re-arms the
56 // chromeless body class flicker). Other valid values: `new`
57 // (Latest), `block-themes`, `favorites`.
58 $add_theme = array(
59 'title' => __( 'Add Theme', 'desktop-mode' ),
60 'url' => admin_url( 'theme-install.php?browse=popular' ),
61 );
62
63 // Idempotent: skip if a previous filter pass (or another plugin)
64 // already added an entry pointing at theme-install.php.
65 foreach ( $dock_item['submenu'] as $existing ) {
66 if ( ! empty( $existing['url'] ) && false !== strpos( $existing['url'], 'theme-install.php' ) ) {
67 return $dock_item;
68 }
69 }
70
71 array_unshift( $dock_item['submenu'], $add_theme );
72
73 return $dock_item;
74 }
75 add_filter( 'openstation_dock_item', 'openstation_inject_appearance_tabs', 10, 2 );
76
77 /**
78 * Renders the orientation header for the chromeless Themes workspace.
79 *
80 * Core's page heading is intentionally hidden inside OpenStation because the
81 * window chrome already names the screen. The Themes page still needs a little
82 * more context than a generic list screen, though: a theme changes the public
83 * face of the site, and Core's single-theme state otherwise opens directly on
84 * an unexplained details panel.
85 *
86 * The markup is emitted through `admin_notices`, which places it immediately
87 * before the page's `.wrap`. Core continues to own the theme cards, details
88 * overlay, actions, AJAX updates, and keyboard behavior.
89 *
90 * The screen is identified by `get_current_screen()`, not `$pagenow`. Every
91 * page registered with `add_theme_page()` also reports `themes.php` as its
92 * `$pagenow`, but its body class comes from the hook suffix
93 * (`appearance_page_<slug>`) and so matches none of the workspace CSS, which
94 * is scoped to `.themes-php`. Gating on `$pagenow` would emit this header
95 * unstyled on top of unrelated Appearance screens. The screen ID also keeps
96 * network admin (`themes-network`, a different list table entirely) out.
97 */
98 function openstation_render_themes_workspace_intro() {
99 if ( ! openstation_is_chromeless_request() ) {
100 return;
101 }
102
103 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
104 if ( ! $screen || 'themes' !== $screen->id ) {
105 return;
106 }
107
108 /*
109 * Mirrors how `wp_prepare_themes_for_js()` builds its own list, without
110 * paying for the preparation itself. Core already called that function
111 * on this request before `admin-header.php` ran, and calling it again
112 * would re-resolve every screenshot, tag and author URL and fire
113 * `pre_prepare_themes_for_js` / `wp_prepare_themes_for_js` a second
114 * time — so any third-party callback hanging off them would run twice
115 * per page load. `wp_get_themes()` is cached, so this is a lookup.
116 */
117 if ( current_user_can( 'switch_themes' ) ) {
118 $themes = wp_get_themes( array( 'allowed' => true ) );
119 $stylesheet = get_stylesheet();
120 if ( ! isset( $themes[ $stylesheet ] ) ) {
121 $themes[ $stylesheet ] = wp_get_theme();
122 }
123 $theme_count = count( $themes );
124 } else {
125 $theme_count = 1;
126 }
127 ?>
128 <section class="openstation-themes-intro" aria-labelledby="openstation-themes-intro-title">
129 <div class="openstation-themes-intro__copy">
130 <p class="openstation-themes-intro__eyebrow"><?php esc_html_e( 'Site appearance', 'desktop-mode' ); ?></p>
131 <h1 id="openstation-themes-intro-title"><?php esc_html_e( 'Choose how your site greets the world.', 'desktop-mode' ); ?></h1>
132 <p><?php esc_html_e( 'Themes shape your site’s look. Switching keeps your posts and pages in place.', 'desktop-mode' ); ?></p>
133 </div>
134 <?php
135 /*
136 * Left readable rather than labelled: `aria-label` is ignored on a
137 * `<p>` (the paragraph role prohibits an author-supplied name), so
138 * pairing it with `aria-hidden` children hid the count from
139 * assistive tech entirely — and Core's own `.title-count` inside
140 * the page `<h1>` is display:none in chromeless mode, making this
141 * the only count on the screen. Read in DOM order the two spans
142 * announce as "3 Themes installed" on their own.
143 */
144 ?>
145 <p class="openstation-themes-intro__count">
146 <strong><?php echo esc_html( number_format_i18n( $theme_count ) ); ?></strong>
147 <span><?php echo esc_html( _n( 'Theme installed', 'Themes installed', $theme_count, 'desktop-mode' ) ); ?></span>
148 </p>
149 </section>
150 <?php
151 }
152 add_action( 'admin_notices', 'openstation_render_themes_workspace_intro', 0 );
153
154 /**
155 * Fixes the visible "active tab" state inside chromeless
156 * theme-install.php iframes.
157 *
158 * Background: WP's installer JS uses a Backbone router whose pattern
159 * `theme-install.php?browse=:sort` greedy-captures everything past
160 * `browse=` ([^/?]+ matches `&` too). Inside chromeless the URL also
161 * carries `openstation_chromeless=1`, so `:sort` ends up as
162 * `popular&openstation_chromeless=1`. WP's `view.sort()` then runs
163 * with that polluted value and the `[data-sort]` selector finds
164 * nothing — leaving every tab in the unselected state even though
165 * the popular themes ARE the rendered listing.
166 *
167 * Reordering query params or re-encoding doesn't help: any URL that
168 * exposes `browse=` to the router matches the route, and any URL
169 * that hides it falls through to a redirect that strips the
170 * chromeless flag (which would re-render the iframe with full admin
171 * chrome on next paint).
172 *
173 * The minimum-viable shim: inline JS that reads the real `browse`
174 * value dynamically via URLSearchParams on every check, finds the
175 * matching `[data-sort]` tab, and sets `current` + `aria-current` on it.
176 * A MutationObserver on `.filter-links` keeps the tab synced when WP's
177 * router fires or state changes (e.g. switching tabs to Latest or
178 * returning via pushState).
179 */
180 function openstation_theme_install_active_tab_script() {
181 if ( ! openstation_is_chromeless_request() ) {
182 return;
183 }
184 if ( ! isset( $GLOBALS['pagenow'] ) || 'theme-install.php' !== $GLOBALS['pagenow'] ) {
185 return;
186 }
187
188 $js = <<<'JS'
189 ( function () {
190 function getBrowseParam() {
191 return new URLSearchParams( window.location.search ).get( 'browse' );
192 }
193 if ( ! getBrowseParam() ) {
194 return;
195 }
196 function applyActiveTab() {
197 var browseParam = getBrowseParam();
198 if ( ! browseParam ) {
199 return true;
200 }
201 var match = document.querySelector(
202 '.filter-links li > a[data-sort="' + browseParam + '"]'
203 );
204 if ( ! match ) {
205 return false;
206 }
207 var tabs = document.querySelectorAll( '.filter-links li > a[data-sort]' );
208 var alreadyCorrect =
209 match.classList.contains( 'current' ) &&
210 Array.prototype.every.call( tabs, function ( a ) {
211 return a === match || ! a.classList.contains( 'current' );
212 } );
213 if ( alreadyCorrect ) {
214 return true;
215 }
216 Array.prototype.forEach.call( tabs, function ( a ) {
217 a.classList.remove( 'current' );
218 a.removeAttribute( 'aria-current' );
219 } );
220 match.classList.add( 'current' );
221 match.setAttribute( 'aria-current', 'page' );
222 return true;
223 }
224 function init() {
225 if ( ! applyActiveTab() ) {
226 window.requestAnimationFrame( init );
227 return;
228 }
229 var container = document.querySelector( '.filter-links' );
230 if ( ! container ) {
231 return;
232 }
233 var pending = false;
234 var observer = new MutationObserver( function () {
235 if ( pending ) {
236 return;
237 }
238 pending = true;
239 window.requestAnimationFrame( function () {
240 pending = false;
241 applyActiveTab();
242 } );
243 } );
244 observer.observe( container, {
245 attributes: true,
246 subtree: true,
247 attributeFilter: [ 'class', 'aria-current' ],
248 } );
249 }
250 if ( document.readyState === 'loading' ) {
251 document.addEventListener( 'DOMContentLoaded', init );
252 } else {
253 init();
254 }
255 } )();
256 JS;
257
258 wp_print_inline_script_tag( $js );
259 }
260 add_action( 'admin_footer', 'openstation_theme_install_active_tab_script', 100 );
261