PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.2
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.2
1.1.10 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 All 34 releases
desktop-mode / includes / render / chromeless-title-actions.php

chromeless-title-actions.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.2, at includes/render/chromeless-title-actions.php

208 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — In-page "Add New" button de-duplication.
4 *
5 * Most list screens render a `.page-title-action` button next to the
6 * `<h1>` ("Add New Post", "Add New User", …). Inside a window the
7 * same destination is usually already a tab in the submenu strip
8 * just above it, so the button is a duplicate.
9 *
10 * We emit one inline CSS rule per submenu URL of the current parent
11 * menu, matching `.page-title-action` on its `href`. A button is
12 * hidden only when its destination is a tab in the same window.
13 *
14 * Matching is on the exact href, never a partial path compare. A
15 * missed match leaves a redundant button; a loose match takes away
16 * the only route to a page. So these all stay visible:
17 *
18 * - "Upload Plugin" (`plugin-install.php?tab=upload`), which is a
19 * different URL from the "Add Plugin" tab (`plugin-install.php`).
20 * - WooCommerce's "Add order" (`…&action=new`) next to the Orders
21 * tab (no `action`).
22 * - Anything on a screen with no submenu strip, which contributes
23 * no URLs at all.
24 * - In-page toggles, see
25 * {@see openstation_chromeless_title_action_toggle_classes()}.
26 *
27 * `themes.php` is the one screen this can't cover; see the per-page
28 * rule in `assets/css/chromeless.css`.
29 *
30 * @package OpenStation
31 */
32
33 defined( 'ABSPATH' ) || exit;
34
35 /**
36 * Collects the tab URLs the current window's submenu strip exposes.
37 *
38 * Repeats the submenu filtering {@see openstation_build_dock_items()}
39 * does (capability, empty title, `openstation_menu_item_url()`) for
40 * one parent menu only. A full dock rebuild would run `get_plugins()`
41 * and walk every other menu, which is a lot of work for an iframe
42 * load that needs one menu's children.
43 *
44 * WordPress auto-prepends a self-link to every parent menu and we
45 * keep it, because the shell shows it as the "back to parent" tab.
46 *
47 * `$parent_file` is set by each `wp-admin/*.php` page before it
48 * includes `admin-header.php`. Plugin screens (`admin.php?page=…`)
49 * don't have one this early, so they return no URLs and nothing gets
50 * hidden. That's the outcome we want there anyway.
51 *
52 * Two cases this still can't see, both of which end with a button
53 * hidden and no tab replacing it. Start here if someone reports that:
54 *
55 * - `openstation_dock_item` can add or remove submenu entries, and
56 * `includes/themes-tabs.php` uses it in-tree. Firing it would
57 * mean inventing a dock item to pass through it, so we don't.
58 * - A window whose URL matches no dock entry gets no tab strip at
59 * all (`findDockEntry` feeds `config.submenu` in
60 * `src/window/dom.ts`), while `$parent_file` still resolves here.
61 *
62 * @return string[] Absolute admin URLs, empty when the current screen
63 * has no resolvable submenu strip.
64 */
65 function openstation_chromeless_submenu_tab_urls() {
66 global $submenu, $parent_file;
67
68 $parent = is_string( $parent_file ) ? $parent_file : '';
69
70 if ( '' === $parent || empty( $submenu[ $parent ] ) || ! is_array( $submenu[ $parent ] ) ) {
71 return array();
72 }
73
74 $urls = array();
75
76 foreach ( $submenu[ $parent ] as $sub_item ) {
77 if ( empty( $sub_item[2] ) ) {
78 continue;
79 }
80 if ( ! empty( $sub_item[1] ) && ! current_user_can( $sub_item[1] ) ) {
81 continue;
82 }
83 // A row with no usable label never becomes a tab, so it can't
84 // be duplicating one either.
85 if ( '' === openstation_menu_item_title( $sub_item[0] ?? '' ) ) {
86 continue;
87 }
88
89 $url = openstation_menu_item_url( (string) $sub_item[2] );
90 if ( '' !== $url ) {
91 $urls[] = $url;
92 }
93 }
94
95 return array_values( array_unique( $urls ) );
96 }
97
98 /**
99 * Anchors that are really in-page toggles, so their href is just the
100 * no-JS fallback and doesn't say where the button goes.
101 *
102 * - `aria-button-if-js` is core's own marker. On `upload.php` in
103 * grid mode, media-grid.js opens a drop zone above the grid
104 * instead of leaving for `media-new.php`. The list-mode copy of
105 * that button has no marker, so it does get de-duplicated.
106 * - `upload-view-toggle` is `plugin-install.php`'s "Upload Plugin",
107 * which plugin-install.js flips to "Browse Plugins" and back. Its
108 * href always points at the state it is NOT in, so on
109 * `?tab=upload` it reads `plugin-install.php`, identical to the
110 * "Add Plugin" tab.
111 *
112 * The chromeless bridge skips clicks on both for the same reason.
113 *
114 * @return string[] CSS class names.
115 */
116 function openstation_chromeless_title_action_toggle_classes() {
117 return array( 'aria-button-if-js', 'upload-view-toggle' );
118 }
119
120 /**
121 * Escapes a URL for use inside a double-quoted CSS attribute value.
122 *
123 * Backslashes and quotes are escaped. Angle brackets and newlines
124 * are stripped instead, so a plugin-authored menu slug can't close
125 * the `<style>` tag; the selector just stops matching.
126 *
127 * @param string $url URL to escape.
128 * @return string Escaped value, without the surrounding quotes.
129 */
130 function openstation_chromeless_css_attr_value( $url ) {
131 $url = preg_replace( '/[\r\n<>]/', '', (string) $url );
132
133 return str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), $url );
134 }
135
136 /**
137 * Builds the de-duplication CSS for the current chromeless screen.
138 *
139 * Two selectors per URL: a CSS attribute selector compares the
140 * attribute's parsed value, which is entity-decoded but NOT resolved
141 * against the document URL. So we need both the absolute form core
142 * renders and the admin-relative form some plugins hand-write
143 * (`href="post-new.php"`).
144 *
145 * The `:not()` guards come from
146 * {@see openstation_chromeless_title_action_toggle_classes()}.
147 *
148 * @param string[] $tab_urls Absolute admin URLs.
149 * @return string CSS, or an empty string when there's nothing to hide.
150 */
151 function openstation_chromeless_title_action_css( $tab_urls ) {
152 if ( empty( $tab_urls ) ) {
153 return '';
154 }
155
156 $admin_base = admin_url();
157 $selectors = array();
158
159 $exclusions = '';
160 foreach ( openstation_chromeless_title_action_toggle_classes() as $class ) {
161 $exclusions .= ':not( .' . $class . ' )';
162 }
163
164 foreach ( $tab_urls as $url ) {
165 $hrefs = array( $url );
166
167 if ( str_starts_with( $url, $admin_base ) ) {
168 $relative = substr( $url, strlen( $admin_base ) );
169 if ( '' !== $relative ) {
170 $hrefs[] = $relative;
171 }
172 }
173
174 foreach ( $hrefs as $href ) {
175 $selectors[] = '.os-chromeless .wrap > .page-title-action[href="'
176 . openstation_chromeless_css_attr_value( $href ) . '"]' . $exclusions;
177 }
178 }
179
180 $selectors = array_values( array_unique( $selectors ) );
181
182 return implode( ",\n", $selectors ) . " {\n\tdisplay: none;\n}\n";
183 }
184
185 /**
186 * Attaches the de-duplication rules to the chromeless stylesheet.
187 *
188 * CSS rather than removing the node: the rule lands in `<head>`
189 * before first paint, so the button never flashes, and it stays in
190 * the DOM for the core scripts that toggle `.page-title-action`.
191 */
192 function openstation_chromeless_title_action_styles() {
193 if ( ! openstation_is_chromeless_request() ) {
194 return;
195 }
196
197 $css = openstation_chromeless_title_action_css(
198 openstation_chromeless_submenu_tab_urls()
199 );
200
201 if ( '' === $css ) {
202 return;
203 }
204
205 wp_add_inline_style( 'os-chromeless', $css );
206 }
207 add_action( 'openstation_chromeless_styles', 'openstation_chromeless_title_action_styles' );
208