PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.6
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.6, at includes/render/chromeless-title-actions.php

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