| 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 |
|