| @@ -1,20 +1,20 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — Native windows registry. | |
| 3 | + * OpenStation — Native windows registry. | |
| 4 | 4 | * |
| 5 | 5 | * The largest of the five components.php registries — owns: |
| 6 | 6 | * |
| 7 | - * - `desktop_mode_register_window()` — plugin-author API | |
| 8 | - * - `desktop_mode_native_window_registry()` — internal store | |
| 9 | - * - `desktop_mode_native_window_allowed_html()` — wp_kses | |
| 7 | + * - `openstation_register_window()` — plugin-author API | |
| 8 | + * - `openstation_native_window_registry()` — internal store | |
| 9 | + * - `openstation_native_window_allowed_html()` — wp_kses | |
| 10 | 10 | * allowlist for `<template>` payloads |
| 11 | - * - `desktop_mode_build_native_window_template_html()` — | |
| 11 | + * - `openstation_build_native_window_template_html()` — | |
| 12 | 12 | * wraps the registered template callback in tabs markup |
| 13 | 13 | * when the window has multiple registered tabs |
| 14 | - * - `desktop_mode_enqueue_native_window_scripts()` — enqueue | |
| 14 | + * - `openstation_enqueue_native_window_scripts()` — enqueue | |
| 15 | 15 | * hook that ships every registered window's script handle |
| 16 | - * - `desktop_mode_render_native_window_templates()` — renders | |
| 16 | + * - `openstation_render_native_window_templates()` — renders | |
| 17 | 17 | * the `<template>` elements the shell clones |
| 18 | 18 | * |
| 19 | 19 | * Extracted from `components.php` during the architecture-0.8.1 |
| 20 | 20 | * PHP slicing (phase 6). The window-tabs registry that builds on |
| @@ -19,10 +19,9 @@ | ||
| 19 | 19 | * Extracted from `components.php` during the architecture-0.8.1 |
| 20 | 20 | * PHP slicing (phase 6). The window-tabs registry that builds on |
| 21 | 21 | * top of this lives in `includes/registries/window-tabs.php`. |
| 22 | 22 | * |
| 23 | - * @package Desktop_Mode | |
| 24 | - * @since 0.8.1 | |
| 23 | + * @package OpenStation | |
| 25 | 24 | */ |
| 26 | 25 | |
| 27 | 26 | defined( 'ABSPATH' ) || exit; |
| 28 | 27 | |
| @@ -32,17 +31,17 @@ | ||
| 32 | 31 | * Under the hood this: |
| 33 | 32 | * |
| 34 | 33 | * 1. Captures the $args and stores them on a module-level |
| 35 | 34 | * registry so the relevant admin_footer + enqueue hooks fire |
| 36 | - * only for the current user's desktop-mode shell. | |
| 35 | + * only for the current user's openstation shell. | |
| 37 | 36 | * 2. On `admin_footer` (shell-side only), emits |
| 38 | - * `<template id="desktop-mode-native-window-<id>">` wrapping the | |
| 37 | + * `<template id="os-native-window-<id>">` wrapping the | |
| 39 | 38 | * output of the `template` callback. Each registered window |
| 40 | 39 | * gets its own template element. |
| 41 | 40 | * 3. On `admin_enqueue_scripts` (shell-side), enqueues the |
| 42 | 41 | * caller's `script` handle if one was provided. The script |
| 43 | 42 | * registers a render callback at |
| 44 | - * `window.desktopModeNativeWindows[<id>]`. On every window open | |
| 43 | + * `window.openStationNativeWindows[<id>]`. On every window open | |
| 45 | 44 | * the shell clones the registered template into the body and |
| 46 | 45 | * then invokes the callback — render is enhancement: query |
| 47 | 46 | * the body for mount points your template declared, light |
| 48 | 47 | * them up. Without a `script` the cloned template IS the |
| @@ -47,12 +46,12 @@ | ||
| 47 | 46 | * the body for mount points your template declared, light |
| 48 | 47 | * them up. Without a `script` the cloned template IS the |
| 49 | 48 | * window; declarative-only plugins need zero JS. |
| 50 | 49 | * 4. Passes a localized config blob to the script |
| 51 | - * (`desktopModeNativeWindow_<id>`) carrying the window's | |
| 50 | + * (`openStationNativeWindow_<id>`) carrying the window's | |
| 52 | 51 | * `id`, `title`, `icon`, dimensions, and `placement`. The |
| 53 | - * script then calls `wp.desktop.registerSystemTile()` + | |
| 54 | - * `wp.desktop.registerWindow()` to wire up the dock tile | |
| 52 | + * script then calls `wp.os.registerSystemTile()` + | |
| 53 | + * `wp.os.registerWindow()` to wire up the dock tile | |
| 55 | 54 | * and the open-on-click behaviour. |
| 56 | 55 | * |
| 57 | 56 | * Plugins write the template callback + the render callback on |
| 58 | 57 | * the JS side; everything else is shell plumbing. Capability gate |
| @@ -61,17 +60,11 @@ | ||
| 61 | 60 | * |
| 62 | 61 | * Note on scope: the shell doesn't auto-open windows server-side |
| 63 | 62 | * — `registerWindow` declares availability, not presence. Users |
| 64 | 63 | * click the registered tile (or your plugin calls |
| 65 | - * `wp.desktop.windowManager.open()` programmatically) to surface | |
| 64 | + * `wp.os.windowManager.open()` programmatically) to surface | |
| 66 | 65 | * the window. |
| 67 | 66 | * |
| 68 | - * @since 0.10.0 | |
| 69 | - * @since 0.11.0 Returns `WP_Error` on validation failure instead of | |
| 70 | - * silent `false`. Legacy `if ( $result )` callers remain | |
| 71 | - * correct because `WP_Error` is truthy; new code should | |
| 72 | - * prefer `is_wp_error( $result )` for diagnostics. | |
| 73 | - * | |
| 74 | 67 | * @param string $id Doubles as window id + dock-tile id. Must |
| 75 | 68 | * be a kebab-case-ish slug. |
| 76 | 69 | * @param array $args { |
| 77 | 70 | * Window registration options. |
| @@ -79,9 +72,9 @@ | ||
| 79 | 72 | * @type string $title Window + tooltip title. Required. |
| 80 | 73 | * @type string $icon Dashicons class or URL. Required. |
| 81 | 74 | * @type callable $template Echoes the window body markup. |
| 82 | 75 | * Wrapped on `admin_footer` in a |
| 83 | - * `<template id="desktop-mode-native-window- | |
| 76 | + * `<template id="os-native-window- | |
| 84 | 77 | * <id>">`; cloned into the window |
| 85 | 78 | * body on every open. The render |
| 86 | 79 | * callback runs against the cloned |
| 87 | 80 | * body, so mount points declared in |
| @@ -91,8 +84,61 @@ | ||
| 91 | 84 | * owns the JS render callback. |
| 92 | 85 | * Optional — omit for a purely |
| 93 | 86 | * declarative window whose body is |
| 94 | 87 | * exactly the cloned template. |
| 88 | + * Loaded the first time the window | |
| 89 | + * opens, not at boot — see | |
| 90 | + * `$preload_script`. | |
| 91 | + * @type string[] $scripts Companion script handles loaded | |
| 92 | + * immediately before `$script`, in | |
| 93 | + * the order given. For a bundle that | |
| 94 | + * extends the window from outside it | |
| 95 | + * — subscribing to the window's own | |
| 96 | + * actions, contributing a section — | |
| 97 | + * and therefore has to be in the tab | |
| 98 | + * before the window's render callback | |
| 99 | + * paints. Declaring it here is what | |
| 100 | + * keeps it off the boot critical | |
| 101 | + * path: it travels with the window | |
| 102 | + * it extends. Default empty. | |
| 103 | + * @type string[] $styles Companion style handles injected on | |
| 104 | + * the window's first open, after the | |
| 105 | + * window's own `$style`, in the order | |
| 106 | + * given — so at equal specificity a | |
| 107 | + * companion's overrides win, the same | |
| 108 | + * source-order contract an enqueue | |
| 109 | + * dependency gives. The styles-side | |
| 110 | + * mirror of `$scripts`: a stylesheet | |
| 111 | + * that only paints surfaces inside | |
| 112 | + * this window is dead weight on every | |
| 113 | + * document that never shows it — | |
| 114 | + * declared here it costs nothing at | |
| 115 | + * boot and never reaches chromeless | |
| 116 | + * iframes at all. Unlike `$style` | |
| 117 | + * (injected when the window registers, | |
| 118 | + * so mid-session activations paint), | |
| 119 | + * companions wait for the first open; | |
| 120 | + * the deferral is the point. Default | |
| 121 | + * empty. | |
| 122 | + * @type bool $preload_script Load `$script` (and `$scripts`) at | |
| 123 | + * shell boot instead of on first | |
| 124 | + * open. Default false — a window's | |
| 125 | + * bundle is dead weight until the | |
| 126 | + * window opens, and the documented | |
| 127 | + * contract for it is "publish a | |
| 128 | + * render callback on | |
| 129 | + * `window.openStationNativeWindows[ | |
| 130 | + * <id> ]`", which the shell reads at | |
| 131 | + * open time. Opt in only when the | |
| 132 | + * bundle ALSO has a boot-time job | |
| 133 | + * that must run whether or not the | |
| 134 | + * user ever opens the window — a | |
| 135 | + * dock badge poller, a public API it | |
| 136 | + * installs on `wp.os`. Prefer | |
| 137 | + * splitting that job into an | |
| 138 | + * always-loaded bundle over paying | |
| 139 | + * the whole window's weight on every | |
| 140 | + * admin page. | |
| 95 | 141 | * @type int $width Initial width (px). Default 520. |
| 96 | 142 | * @type int $height Initial height (px). Default 400. |
| 97 | 143 | * @type int $min_width Minimum width (px). Default 280. |
| 98 | 144 | * @type int $min_height Minimum height (px). Default 220. |
| @@ -98,11 +144,51 @@ | ||
| 98 | 144 | * @type int $min_height Minimum height (px). Default 220. |
| 99 | 145 | * @type string $placement 'dock' | 'none'. Default 'dock'. |
| 100 | 146 | * 'none' skips the tile (plugin |
| 101 | 147 | * opens the window programmatically). |
| 148 | + * A PROPOSED default only: the user's | |
| 149 | + * OpenStation Preferences → Navigation | |
| 150 | + * pick wins, and so does a right-click | |
| 151 | + * "Keep in dock". | |
| 152 | + * @type string $admin 'site' | 'network' | 'any'. Default | |
| 153 | + * 'site': offered on every site's | |
| 154 | + * shell and never on the network | |
| 155 | + * admin's, which is right for a window | |
| 156 | + * that reads the current site's REST | |
| 157 | + * API. 'network' is the network | |
| 158 | + * admin's shell only; 'any' is both. | |
| 159 | + * @type string $nav_kind 'app' | 'control'. Default 'app'. | |
| 160 | + * What the window IS, which decides | |
| 161 | + * where its launcher defaults to (apps | |
| 162 | + * to the desktop, controls to the | |
| 163 | + * dock) and which dock zone it sits | |
| 164 | + * in. Plugins want 'app'; 'control' | |
| 165 | + * is for OpenStation's own | |
| 166 | + * affordances. | |
| 167 | + * @type int $dock_order Sort key among system tiles, | |
| 168 | + * ascending; ties keep registration | |
| 169 | + * order. Default 0, which places the | |
| 170 | + * tile ahead of the shell's own | |
| 171 | + * trailing cluster (Mio 10, Overview | |
| 172 | + * 20, System 30, Exit 35, Trash 40). | |
| 173 | + * Needed because registration order | |
| 174 | + * is not something a plugin controls: | |
| 175 | + * tiles land when their lazy script | |
| 176 | + * resolves. | |
| 177 | + * @type bool $placeable Whether the dock tile gets a row in | |
| 178 | + * OpenStation Preferences → Apps & | |
| 179 | + * Plugins, so the user can move it to | |
| 180 | + * the wallpaper or hide it. Defaults | |
| 181 | + * to the dock either way. Default | |
| 182 | + * false, because most tiles are | |
| 183 | + * load-bearing. Opt in for a window | |
| 184 | + * the user can reasonably do without. | |
| 185 | + * Only offer this on a window that | |
| 186 | + * registers no desktop icon: the icon | |
| 187 | + * already owns a row of its own. | |
| 102 | 188 | * @type string[] $capabilities User capabilities that gate the |
| 103 | 189 | * registration. ANY miss returns |
| 104 | - * `WP_Error desktop_mode_capability_denied`. | |
| 190 | + * `WP_Error openstation_capability_denied`. | |
| 105 | 191 | * @type bool|string $autofocus Passed verbatim to |
| 106 | 192 | * `NativeWindowDef.autofocus`. |
| 107 | 193 | * @type string $main_tab_label Label for the "main" tab that |
| 108 | 194 | * displays the window's own |
| @@ -108,9 +194,9 @@ | ||
| 108 | 194 | * displays the window's own |
| 109 | 195 | * `template` output. Only rendered |
| 110 | 196 | * when at least one additional |
| 111 | 197 | * tab is registered via |
| 112 | - * {@see desktop_mode_register_window_tab()}. | |
| 198 | + * {@see openstation_register_window_tab()}. | |
| 113 | 199 | * Defaults to the window's `title`. |
| 114 | 200 | * @type int $main_tab_padding Padding (in px) applied to the |
| 115 | 201 | * auto-generated tab-wrap around |
| 116 | 202 | * the window body. Only applies |
| @@ -117,15 +203,15 @@ | ||
| 117 | 203 | * when additional tabs are |
| 118 | 204 | * registered. Default 16. Pass 0 |
| 119 | 205 | * for edge-to-edge content. |
| 120 | 206 | * Filterable at runtime via |
| 121 | - * `desktop_mode_native_window_tab_wrap_padding`. | |
| 207 | + * `openstation_native_window_tab_wrap_padding`. | |
| 122 | 208 | * @type array $config Arbitrary serializable data to ship |
| 123 | 209 | * to the bundle alongside the script |
| 124 | 210 | * tag. Read in JS via |
| 125 | - * `wp.desktop.getWindowConfig( $id )` | |
| 211 | + * `wp.os.getWindowConfig( $id )` | |
| 126 | 212 | * (or directly at |
| 127 | - * `window.desktopModeWindowConfig[ $id ]`). | |
| 213 | + * `window.openStationWindowConfig[ $id ]`). | |
| 128 | 214 | * Recommended over `wp_localize_script` |
| 129 | 215 | * for native-window scripts because |
| 130 | 216 | * the lazy-load path bypasses |
| 131 | 217 | * `wp_print_scripts` — passing config |
| @@ -139,13 +225,13 @@ | ||
| 139 | 225 | * @return true|WP_Error `true` on success; `WP_Error` when any |
| 140 | 226 | * required arg is missing/invalid or a |
| 141 | 227 | * declared capability is unmet. |
| 142 | 228 | */ |
| 143 | -function desktop_mode_register_window( $id, $args = array() ) { | |
| 229 | +function openstation_register_window( $id, $args = array() ) { | |
| 144 | 230 | $id = sanitize_key( (string) $id ); |
| 145 | 231 | if ( '' === $id ) { |
| 146 | - return desktop_mode_registration_error( | |
| 147 | - 'desktop_mode_missing_id', | |
| 232 | + return openstation_registration_error( | |
| 233 | + 'openstation_missing_id', | |
| 148 | 234 | __( 'Native window id is required and must be a valid slug.', 'desktop-mode' ) |
| 149 | 235 | ); |
| 150 | 236 | } |
| 151 | 237 | |
| @@ -153,14 +239,17 @@ | ||
| 153 | 239 | 'title' => '', |
| 154 | 240 | 'icon' => 'dashicons-admin-generic', |
| 155 | 241 | 'template' => null, |
| 156 | 242 | 'script' => '', |
| 243 | + 'scripts' => array(), | |
| 244 | + 'styles' => array(), | |
| 245 | + 'preload_script' => false, | |
| 157 | 246 | // Optional WP style handle (registered with `wp_register_style()`). |
| 158 | 247 | // Resolved at payload-build time so the shell can lazy-inject a |
| 159 | 248 | // `<link rel="stylesheet">` when a peer plugin is activated |
| 160 | 249 | // mid-session — without this, the parent shell page already |
| 161 | 250 | // finished `wp_print_styles` and the plugin's CSS is missing |
| 162 | - // until F5. @since 0.18.1 | |
| 251 | + // until F5. | |
| 163 | 252 | 'style' => '', |
| 164 | 253 | 'width' => 520, |
| 165 | 254 | 'height' => 400, |
| 166 | 255 | 'min_width' => 280, |
| @@ -165,8 +254,12 @@ | ||
| 165 | 254 | 'height' => 400, |
| 166 | 255 | 'min_width' => 280, |
| 167 | 256 | 'min_height' => 220, |
| 168 | 257 | 'placement' => 'dock', |
| 258 | + 'admin' => 'site', | |
| 259 | + 'nav_kind' => 'app', | |
| 260 | + 'dock_order' => 0, | |
| 261 | + 'placeable' => false, | |
| 169 | 262 | 'capabilities' => array(), |
| 170 | 263 | 'autofocus' => false, |
| 171 | 264 | 'main_tab_label' => '', |
| 172 | 265 | 'main_tab_padding' => '', |
| @@ -171,21 +264,27 @@ | ||
| 171 | 264 | 'main_tab_label' => '', |
| 172 | 265 | 'main_tab_padding' => '', |
| 173 | 266 | 'config' => array(), |
| 174 | 267 | ); |
| 175 | - $args = wp_parse_args( $args, $defaults ); | |
| 268 | + $args = wp_parse_args( $args, $defaults ); | |
| 269 | + if ( ! in_array( $args['admin'], array( 'site', 'network', 'any' ), true ) ) { | |
| 270 | + $args['admin'] = 'site'; | |
| 271 | + } | |
| 176 | 272 | |
| 177 | 273 | // Capability gate — ALL listed caps must match. Fail closed. |
| 178 | 274 | foreach ( (array) $args['capabilities'] as $cap ) { |
| 179 | 275 | if ( ! current_user_can( (string) $cap ) ) { |
| 180 | - return desktop_mode_registration_error( | |
| 181 | - 'desktop_mode_capability_denied', | |
| 276 | + return openstation_registration_error( | |
| 277 | + 'openstation_capability_denied', | |
| 182 | 278 | sprintf( |
| 183 | 279 | /* translators: %s: capability slug. */ |
| 184 | 280 | __( 'Current user lacks the %s capability required to register this native window.', 'desktop-mode' ), |
| 185 | 281 | (string) $cap |
| 186 | 282 | ), |
| 187 | - array( 'capability' => (string) $cap, 'id' => $id ) | |
| 283 | + array( | |
| 284 | + 'capability' => (string) $cap, | |
| 285 | + 'id' => $id, | |
| 286 | + ) | |
| 188 | 287 | ); |
| 189 | 288 | } |
| 190 | 289 | } |
| 191 | 290 | |
| @@ -190,17 +289,17 @@ | ||
| 190 | 289 | } |
| 191 | 290 | |
| 192 | 291 | // Required fields. |
| 193 | 292 | if ( '' === (string) $args['title'] ) { |
| 194 | - return desktop_mode_registration_error( | |
| 195 | - 'desktop_mode_missing_title', | |
| 293 | + return openstation_registration_error( | |
| 294 | + 'openstation_missing_title', | |
| 196 | 295 | __( 'Native window registration requires a non-empty `title`.', 'desktop-mode' ), |
| 197 | 296 | array( 'id' => $id ) |
| 198 | 297 | ); |
| 199 | 298 | } |
| 200 | 299 | if ( ! is_callable( $args['template'] ) ) { |
| 201 | - return desktop_mode_registration_error( | |
| 202 | - 'desktop_mode_invalid_template', | |
| 300 | + return openstation_registration_error( | |
| 301 | + 'openstation_invalid_template', | |
| 203 | 302 | __( 'Native window registration requires a callable `template` that echoes the template body.', 'desktop-mode' ), |
| 204 | 303 | array( 'id' => $id ) |
| 205 | 304 | ); |
| 206 | 305 | } |
| @@ -208,8 +307,17 @@ | ||
| 208 | 307 | $placement = in_array( $args['placement'], array( 'dock', 'none' ), true ) |
| 209 | 308 | ? $args['placement'] |
| 210 | 309 | : 'dock'; |
| 211 | 310 | |
| 311 | + // What the window IS, which is what decides where its launcher | |
| 312 | + // goes by default and which dock zone it sits in. `'app'` for an | |
| 313 | + // installed app (the default, and what every plugin wants); | |
| 314 | + // `'control'` for an OpenStation affordance — the Trash is the | |
| 315 | + // only shipped one. | |
| 316 | + $nav_kind = in_array( $args['nav_kind'], array( 'app', 'control' ), true ) | |
| 317 | + ? $args['nav_kind'] | |
| 318 | + : 'app'; | |
| 319 | + | |
| 212 | 320 | $entry = array( |
| 213 | 321 | 'id' => $id, |
| 214 | 322 | 'title' => (string) $args['title'], |
| 215 | 323 | 'icon' => (string) $args['icon'], |
| @@ -214,8 +322,32 @@ | ||
| 214 | 322 | 'title' => (string) $args['title'], |
| 215 | 323 | 'icon' => (string) $args['icon'], |
| 216 | 324 | 'template' => $args['template'], |
| 217 | 325 | 'script' => (string) $args['script'], |
| 326 | + // Companion handles, deduped and stripped of empties so the | |
| 327 | + // payload builder can resolve the list without re-checking. | |
| 328 | + 'scripts' => array_values( | |
| 329 | + array_unique( | |
| 330 | + array_filter( | |
| 331 | + array_map( 'strval', (array) $args['scripts'] ), | |
| 332 | + static function ( $handle ) { | |
| 333 | + return '' !== $handle; | |
| 334 | + } | |
| 335 | + ) | |
| 336 | + ) | |
| 337 | + ), | |
| 338 | + // Companion style handles, same dedupe/strip as `scripts`. | |
| 339 | + 'styles' => array_values( | |
| 340 | + array_unique( | |
| 341 | + array_filter( | |
| 342 | + array_map( 'strval', (array) $args['styles'] ), | |
| 343 | + static function ( $handle ) { | |
| 344 | + return '' !== $handle; | |
| 345 | + } | |
| 346 | + ) | |
| 347 | + ) | |
| 348 | + ), | |
| 349 | + 'preload_script' => (bool) $args['preload_script'], | |
| 218 | 350 | 'style' => (string) $args['style'], |
| 219 | 351 | 'width' => (int) $args['width'], |
| 220 | 352 | 'height' => (int) $args['height'], |
| 221 | 353 | 'min_width' => (int) $args['min_width'], |
| @@ -220,20 +352,29 @@ | ||
| 220 | 352 | 'height' => (int) $args['height'], |
| 221 | 353 | 'min_width' => (int) $args['min_width'], |
| 222 | 354 | 'min_height' => (int) $args['min_height'], |
| 223 | 355 | 'placement' => $placement, |
| 356 | + 'nav_kind' => $nav_kind, | |
| 357 | + // Which admin's shell offers it; see the `admin` arg. | |
| 358 | + 'admin' => $args['admin'], | |
| 359 | + // Sort key among system tiles, ascending. `0` (the default) | |
| 360 | + // puts a plugin's tile ahead of the shell's own trailing | |
| 361 | + // cluster — Mio 10, Overview 20, System 30 — which is where a | |
| 362 | + // launcher belongs. Trash uses 40 to sit at the very end. | |
| 363 | + 'dock_order' => (int) $args['dock_order'], | |
| 364 | + 'placeable' => (bool) $args['placeable'], | |
| 224 | 365 | 'autofocus' => $args['autofocus'], |
| 225 | 366 | 'main_tab_label' => (string) $args['main_tab_label'], |
| 226 | - // Stored as-is (string or int). `desktop_mode_build_native_window_template_html` | |
| 367 | + // Stored as-is (string or int). `openstation_build_native_window_template_html` | |
| 227 | 368 | // coerces to int and falls back to 16 when absent. |
| 228 | 369 | 'main_tab_padding' => $args['main_tab_padding'], |
| 229 | 370 | // Bundle-bound config delivered through the same path as |
| 230 | 371 | // `wp_localize_script` `extra['data']` — see the `config` doc |
| 231 | - // in this function's `$args` block and `desktop_mode_resolve_script_payload()` | |
| 372 | + // in this function's `$args` block and `openstation_resolve_script_payload()` | |
| 232 | 373 | // for how it lands on the wire. |
| 233 | 374 | 'config' => is_array( $args['config'] ) ? $args['config'] : array(), |
| 234 | 375 | ); |
| 235 | - desktop_mode_native_window_registry( $id, $entry ); | |
| 376 | + openstation_native_window_registry( $id, $entry ); | |
| 236 | 377 | |
| 237 | 378 | /** |
| 238 | 379 | * Fires after a native desktop window is successfully registered. |
| 239 | 380 | * |
| @@ -239,19 +380,17 @@ | ||
| 239 | 380 | * |
| 240 | 381 | * Lets plugins react to registrations made by other plugins — |
| 241 | 382 | * e.g. a widget that auto-opens when a given window registers, |
| 242 | 383 | * or analytics tracking of which windows the current install |
| 243 | - * exposes. Does NOT fire when `desktop_mode_register_window()` | |
| 384 | + * exposes. Does NOT fire when `openstation_register_window()` | |
| 244 | 385 | * returns a `WP_Error`. |
| 245 | 386 | * |
| 246 | - * @since 0.11.0 | |
| 247 | - * | |
| 248 | 387 | * @param string $id The window id. |
| 249 | 388 | * @param array $entry The stored registry entry (id, title, |
| 250 | 389 | * icon, template callback, script handle, |
| 251 | 390 | * size defaults, placement, autofocus). |
| 252 | 391 | */ |
| 253 | - do_action( 'desktop_mode_native_window_registered', $id, $entry ); | |
| 392 | + do_action( 'openstation_native_window_registered', $id, $entry ); | |
| 254 | 393 | |
| 255 | 394 | return true; |
| 256 | 395 | } |
| 257 | 396 | |
| @@ -256,14 +395,13 @@ | ||
| 256 | 395 | } |
| 257 | 396 | |
| 258 | 397 | /** |
| 259 | 398 | * Internal module-level registry for native windows registered |
| 260 | - * via {@see desktop_mode_register_window()}. Passing a second | |
| 399 | + * via {@see openstation_register_window()}. Passing a second | |
| 261 | 400 | * argument stores the entry; passing only the id returns the |
| 262 | 401 | * stored value (or null). Kept small and side-effect-free so |
| 263 | 402 | * tests can introspect. |
| 264 | 403 | * |
| 265 | - * @since 0.10.0 | |
| 266 | 404 | * @internal |
| 267 | 405 | * |
| 268 | 406 | * @param string $id Window id. |
| 269 | 407 | * @param array|null $entry Entry to store, or null to just read. |
| @@ -269,9 +407,9 @@ | ||
| 269 | 407 | * @param array|null $entry Entry to store, or null to just read. |
| 270 | 408 | * @return array|null Either the stored entry or the full registry |
| 271 | 409 | * (when id is empty). |
| 272 | 410 | */ |
| 273 | -function desktop_mode_native_window_registry( $id = '', $entry = null ) { | |
| 411 | +function openstation_native_window_registry( $id = '', $entry = null ) { | |
| 274 | 412 | static $store = array(); |
| 275 | 413 | |
| 276 | 414 | if ( '' === (string) $id ) { |
| 277 | 415 | return $store; |
| @@ -283,26 +421,8 @@ | ||
| 283 | 421 | } |
| 284 | 422 | |
| 285 | 423 | |
| 286 | 424 | /** |
| 287 | - * Render a native window's template HTML to a string, wrapping | |
| 288 | - * with tabs when the window has at least one additional tab | |
| 289 | - * registered. Shared by `desktop_mode_render_native_window_templates()` | |
| 290 | - * (which emits the live `<template>` element) and | |
| 291 | - * `desktop_mode_build_native_windows_payload()` (which captures the same | |
| 292 | - * string for the shell config so mid-session activation can inject | |
| 293 | - * the template without a reload). | |
| 294 | - * | |
| 295 | - * Single-tab windows (no additional tabs registered) render the | |
| 296 | - * same flat body they always did — backwards-compatible with | |
| 297 | - * every existing caller. | |
| 298 | - * | |
| 299 | - * @since 0.11.0 | |
| 300 | - * | |
| 301 | - * @param array $entry Window registry entry. | |
| 302 | - * @return string Template body HTML (no outer `<template>` tag). | |
| 303 | - */ | |
| 304 | -/** | |
| 305 | 425 | * Returns the `wp_kses`-shaped allowlist used to escape native-window |
| 306 | 426 | * `<template>` payloads (and the recycle-bin template) before they're |
| 307 | 427 | * emitted into the page. |
| 308 | 428 | * |
| @@ -308,18 +428,16 @@ | ||
| 308 | 428 | * |
| 309 | 429 | * Templates are inert until JS clones them out of the `<template>` |
| 310 | 430 | * tag — but Plugin Check still requires escape-on-output. The list |
| 311 | 431 | * extends `wp_kses_allowed_html( 'post' )` with form controls, |
| 312 | - * `<wpd-*>` web components, and dashicon spans, plus permissive | |
| 313 | - * `data-*`, `aria-*`, and component-specific attributes. Plugins | |
| 432 | + * `<os-*>` web components, and dashicon spans, plus permissive | |
| 433 | + * `data-*`, common ARIA, and component-specific attributes. Plugins | |
| 314 | 434 | * registering their own native windows can extend the list via the |
| 315 | - * `desktop_mode_native_window_allowed_html` filter below. | |
| 435 | + * `openstation_native_window_allowed_html` filter below. | |
| 316 | 436 | * |
| 317 | - * @since 0.6.2 | |
| 318 | - * | |
| 319 | 437 | * @return array<string,array<string,bool>> |
| 320 | 438 | */ |
| 321 | -function desktop_mode_native_window_allowed_html() { | |
| 439 | +function openstation_native_window_allowed_html() { | |
| 322 | 440 | $base = wp_kses_allowed_html( 'post' ); |
| 323 | 441 | |
| 324 | 442 | $global_attrs = array( |
| 325 | 443 | 'id' => true, |
| @@ -335,11 +453,17 @@ | ||
| 335 | 453 | 'dir' => true, |
| 336 | 454 | 'draggable' => true, |
| 337 | 455 | 'contenteditable' => true, |
| 338 | 456 | 'data-*' => true, |
| 339 | - 'aria-*' => true, | |
| 457 | + // `wp_kses` only treats the `data-*` wildcard specially. ARIA | |
| 458 | + // attributes must be admitted by their exact names or they are | |
| 459 | + // silently stripped from native-window templates. | |
| 460 | + 'aria-label' => true, | |
| 461 | + 'aria-labelledby' => true, | |
| 462 | + 'aria-current' => true, | |
| 463 | + 'aria-hidden' => true, | |
| 340 | 464 | // `full-width` is a layout-level flag honoured by |
| 341 | - // `<wpd-form>` (and any future wpd-* container that opts in | |
| 465 | + // `<os-form>` (and any future os-* container that opts in | |
| 342 | 466 | // to row-spanning slotted children). Lives in the global |
| 343 | 467 | // allowlist so a plain `<div full-width>` wrapper isn't |
| 344 | 468 | // stripped by kses on its way through the template. |
| 345 | 469 | 'full-width' => true, |
| @@ -349,38 +473,38 @@ | ||
| 349 | 473 | $global_attrs, |
| 350 | 474 | array( |
| 351 | 475 | 'name' => true, |
| 352 | 476 | 'value' => true, |
| 353 | - 'placeholder' => true, | |
| 354 | - 'required' => true, | |
| 355 | - 'disabled' => true, | |
| 356 | - 'readonly' => true, | |
| 357 | - 'checked' => true, | |
| 358 | - 'selected' => true, | |
| 359 | - 'min' => true, | |
| 360 | - 'max' => true, | |
| 361 | - 'step' => true, | |
| 362 | - 'minlength' => true, | |
| 363 | - 'maxlength' => true, | |
| 364 | - 'pattern' => true, | |
| 477 | + 'placeholder' => true, | |
| 478 | + 'required' => true, | |
| 479 | + 'disabled' => true, | |
| 480 | + 'readonly' => true, | |
| 481 | + 'checked' => true, | |
| 482 | + 'selected' => true, | |
| 483 | + 'min' => true, | |
| 484 | + 'max' => true, | |
| 485 | + 'step' => true, | |
| 486 | + 'minlength' => true, | |
| 487 | + 'maxlength' => true, | |
| 488 | + 'pattern' => true, | |
| 365 | 489 | 'autocomplete' => true, |
| 366 | - 'autofocus' => true, | |
| 367 | - 'multiple' => true, | |
| 368 | - 'rows' => true, | |
| 369 | - 'cols' => true, | |
| 370 | - 'wrap' => true, | |
| 371 | - 'size' => true, | |
| 372 | - 'for' => true, | |
| 373 | - 'form' => true, | |
| 374 | - 'type' => true, | |
| 375 | - 'accept' => true, | |
| 376 | - 'list' => true, | |
| 377 | - 'src' => true, | |
| 378 | - 'href' => true, | |
| 379 | - 'target' => true, | |
| 380 | - 'rel' => true, | |
| 381 | - 'open' => true, | |
| 382 | - 'variant' => true, | |
| 490 | + 'autofocus' => true, | |
| 491 | + 'multiple' => true, | |
| 492 | + 'rows' => true, | |
| 493 | + 'cols' => true, | |
| 494 | + 'wrap' => true, | |
| 495 | + 'size' => true, | |
| 496 | + 'for' => true, | |
| 497 | + 'form' => true, | |
| 498 | + 'type' => true, | |
| 499 | + 'accept' => true, | |
| 500 | + 'list' => true, | |
| 501 | + 'src' => true, | |
| 502 | + 'href' => true, | |
| 503 | + 'target' => true, | |
| 504 | + 'rel' => true, | |
| 505 | + 'open' => true, | |
| 506 | + 'variant' => true, | |
| 383 | 507 | ) |
| 384 | 508 | ); |
| 385 | 509 | |
| 386 | 510 | $wpd_attrs = array_merge( |
| @@ -385,115 +509,218 @@ | ||
| 385 | 509 | |
| 386 | 510 | $wpd_attrs = array_merge( |
| 387 | 511 | $form_attrs, |
| 388 | 512 | array( |
| 389 | - 'gap' => true, | |
| 390 | - 'padding' => true, | |
| 391 | - 'align' => true, | |
| 392 | - 'justify' => true, | |
| 393 | - 'direction' => true, | |
| 394 | - 'wrap' => true, | |
| 395 | - 'inset' => true, | |
| 396 | - 'icon' => true, | |
| 397 | - 'tone' => true, | |
| 398 | - 'size' => true, | |
| 399 | - 'shape' => true, | |
| 400 | - 'badge' => true, | |
| 401 | - 'selectable' => true, | |
| 402 | - 'sticky-header' => true, | |
| 513 | + 'gap' => true, | |
| 514 | + 'padding' => true, | |
| 515 | + 'align' => true, | |
| 516 | + 'justify' => true, | |
| 517 | + 'direction' => true, | |
| 518 | + 'wrap' => true, | |
| 519 | + 'inset' => true, | |
| 520 | + 'icon' => true, | |
| 521 | + 'tone' => true, | |
| 522 | + 'size' => true, | |
| 523 | + 'shape' => true, | |
| 524 | + 'badge' => true, | |
| 525 | + 'selectable' => true, | |
| 526 | + 'sticky-header' => true, | |
| 403 | 527 | 'sticky-columns' => true, |
| 404 | - 'hover' => true, | |
| 405 | - 'striped' => true, | |
| 406 | - 'bordered' => true, | |
| 407 | - 'compact' => true, | |
| 408 | - 'loading' => true, | |
| 409 | - 'loading-rows' => true, | |
| 410 | - 'columns' => true, | |
| 411 | - 'rows' => true, | |
| 412 | - 'sortable' => true, | |
| 413 | - 'expandable' => true, | |
| 414 | - 'preset' => true, | |
| 415 | - 'label' => true, | |
| 416 | - 'description' => true, | |
| 417 | - 'orientation' => true, | |
| 418 | - 'level' => true, | |
| 419 | - 'collapsed' => true, | |
| 420 | - // `<wpd-form>` props + the `full-width` row span flag | |
| 528 | + 'hover' => true, | |
| 529 | + 'striped' => true, | |
| 530 | + 'bordered' => true, | |
| 531 | + 'compact' => true, | |
| 532 | + 'loading' => true, | |
| 533 | + 'loading-rows' => true, | |
| 534 | + 'empty' => true, | |
| 535 | + 'columns' => true, | |
| 536 | + 'rows' => true, | |
| 537 | + 'sortable' => true, | |
| 538 | + 'expandable' => true, | |
| 539 | + 'preset' => true, | |
| 540 | + 'label' => true, | |
| 541 | + 'heading' => true, | |
| 542 | + 'description' => true, | |
| 543 | + 'orientation' => true, | |
| 544 | + 'level' => true, | |
| 545 | + 'collapsed' => true, | |
| 546 | + // `<os-form>` props + the `full-width` row span flag | |
| 421 | 547 | // honoured by the form's slotted-child layout rule. |
| 422 | - 'submit-label' => true, | |
| 423 | - 'reset-label' => true, | |
| 424 | - 'busy' => true, | |
| 425 | - 'error' => true, | |
| 426 | - 'min-column' => true, | |
| 427 | - 'show-reset' => true, | |
| 428 | - 'reveal' => true, | |
| 429 | - 'full-width' => true, | |
| 548 | + 'submit-label' => true, | |
| 549 | + 'reset-label' => true, | |
| 550 | + 'busy' => true, | |
| 551 | + 'error' => true, | |
| 552 | + 'min-column' => true, | |
| 553 | + 'show-reset' => true, | |
| 554 | + 'reveal' => true, | |
| 555 | + 'full-width' => true, | |
| 430 | 556 | ) |
| 431 | 557 | ); |
| 432 | 558 | |
| 433 | 559 | // Built-in HTML elements the templates rely on. |
| 434 | 560 | $extra = array( |
| 435 | - 'form' => $form_attrs, | |
| 436 | - 'fieldset' => $form_attrs, | |
| 437 | - 'legend' => $global_attrs, | |
| 438 | - 'label' => $form_attrs, | |
| 439 | - 'input' => $form_attrs, | |
| 440 | - 'select' => $form_attrs, | |
| 441 | - 'option' => $form_attrs, | |
| 442 | - 'optgroup' => $form_attrs, | |
| 443 | - 'textarea' => $form_attrs, | |
| 444 | - 'button' => $form_attrs, | |
| 445 | - 'output' => $form_attrs, | |
| 446 | - 'datalist' => $global_attrs, | |
| 447 | - 'progress' => $form_attrs, | |
| 448 | - 'meter' => $form_attrs, | |
| 449 | - 'details' => $global_attrs, | |
| 450 | - 'summary' => $global_attrs, | |
| 451 | - 'dialog' => $global_attrs, | |
| 452 | - 'header' => $global_attrs, | |
| 453 | - 'footer' => $global_attrs, | |
| 454 | - 'main' => $global_attrs, | |
| 455 | - 'nav' => $global_attrs, | |
| 456 | - 'section' => $global_attrs, | |
| 457 | - 'article' => $global_attrs, | |
| 458 | - 'aside' => $global_attrs, | |
| 459 | - 'figure' => $global_attrs, | |
| 561 | + 'form' => $form_attrs, | |
| 562 | + 'fieldset' => $form_attrs, | |
| 563 | + 'legend' => $global_attrs, | |
| 564 | + 'label' => $form_attrs, | |
| 565 | + 'input' => $form_attrs, | |
| 566 | + 'select' => $form_attrs, | |
| 567 | + 'option' => $form_attrs, | |
| 568 | + 'optgroup' => $form_attrs, | |
| 569 | + 'textarea' => $form_attrs, | |
| 570 | + 'button' => $form_attrs, | |
| 571 | + 'output' => $form_attrs, | |
| 572 | + 'datalist' => $global_attrs, | |
| 573 | + 'progress' => $form_attrs, | |
| 574 | + 'meter' => $form_attrs, | |
| 575 | + 'details' => $global_attrs, | |
| 576 | + 'summary' => $global_attrs, | |
| 577 | + 'dialog' => $global_attrs, | |
| 578 | + 'header' => $global_attrs, | |
| 579 | + 'footer' => $global_attrs, | |
| 580 | + 'main' => $global_attrs, | |
| 581 | + 'nav' => $global_attrs, | |
| 582 | + 'section' => $global_attrs, | |
| 583 | + 'article' => $global_attrs, | |
| 584 | + 'aside' => $global_attrs, | |
| 585 | + 'figure' => $global_attrs, | |
| 460 | 586 | 'figcaption' => $global_attrs, |
| 461 | - 'time' => array_merge( $global_attrs, array( 'datetime' => true ) ), | |
| 462 | - 'mark' => $global_attrs, | |
| 463 | - 'small' => $global_attrs, | |
| 464 | - 'svg' => array_merge( $global_attrs, array( 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'xmlns' => true ) ), | |
| 465 | - 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'class' => true ), | |
| 466 | - 'g' => array( 'class' => true, 'transform' => true, 'fill' => true ), | |
| 467 | - 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'class' => true ), | |
| 468 | - 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'class' => true ), | |
| 469 | - 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true ), | |
| 470 | - 'polyline' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'class' => true ), | |
| 471 | - 'polygon' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'class' => true ), | |
| 472 | - 'use' => array( 'href' => true, 'class' => true ), | |
| 587 | + 'time' => array_merge( $global_attrs, array( 'datetime' => true ) ), | |
| 588 | + 'mark' => $global_attrs, | |
| 589 | + 'small' => $global_attrs, | |
| 590 | + 'svg' => array_merge( | |
| 591 | + $global_attrs, | |
| 592 | + array( | |
| 593 | + 'viewbox' => true, | |
| 594 | + 'width' => true, | |
| 595 | + 'height' => true, | |
| 596 | + 'fill' => true, | |
| 597 | + 'stroke' => true, | |
| 598 | + 'xmlns' => true, | |
| 599 | + ) | |
| 600 | + ), | |
| 601 | + 'path' => array( | |
| 602 | + 'd' => true, | |
| 603 | + 'fill' => true, | |
| 604 | + 'stroke' => true, | |
| 605 | + 'stroke-width' => true, | |
| 606 | + 'stroke-linecap' => true, | |
| 607 | + 'stroke-linejoin' => true, | |
| 608 | + 'class' => true, | |
| 609 | + ), | |
| 610 | + 'g' => array( | |
| 611 | + 'class' => true, | |
| 612 | + 'transform' => true, | |
| 613 | + 'fill' => true, | |
| 614 | + ), | |
| 615 | + 'circle' => array( | |
| 616 | + 'cx' => true, | |
| 617 | + 'cy' => true, | |
| 618 | + 'r' => true, | |
| 619 | + 'fill' => true, | |
| 620 | + 'stroke' => true, | |
| 621 | + 'class' => true, | |
| 622 | + ), | |
| 623 | + 'rect' => array( | |
| 624 | + 'x' => true, | |
| 625 | + 'y' => true, | |
| 626 | + 'width' => true, | |
| 627 | + 'height' => true, | |
| 628 | + 'rx' => true, | |
| 629 | + 'ry' => true, | |
| 630 | + 'fill' => true, | |
| 631 | + 'stroke' => true, | |
| 632 | + 'class' => true, | |
| 633 | + ), | |
| 634 | + 'line' => array( | |
| 635 | + 'x1' => true, | |
| 636 | + 'y1' => true, | |
| 637 | + 'x2' => true, | |
| 638 | + 'y2' => true, | |
| 639 | + 'stroke' => true, | |
| 640 | + 'stroke-width' => true, | |
| 641 | + 'class' => true, | |
| 642 | + ), | |
| 643 | + 'polyline' => array( | |
| 644 | + 'points' => true, | |
| 645 | + 'fill' => true, | |
| 646 | + 'stroke' => true, | |
| 647 | + 'class' => true, | |
| 648 | + ), | |
| 649 | + 'polygon' => array( | |
| 650 | + 'points' => true, | |
| 651 | + 'fill' => true, | |
| 652 | + 'stroke' => true, | |
| 653 | + 'class' => true, | |
| 654 | + ), | |
| 655 | + 'use' => array( | |
| 656 | + 'href' => true, | |
| 657 | + 'class' => true, | |
| 658 | + ), | |
| 473 | 659 | ); |
| 474 | 660 | |
| 475 | - // `<wpd-*>` web components — every shipped tag plus a permissive | |
| 661 | + // `<os-*>` web components — every shipped tag plus a permissive | |
| 476 | 662 | // open door for new ones added by plugin templates. |
| 477 | 663 | $wpd_tags = array( |
| 478 | - 'wpd-stack', 'wpd-cluster', 'wpd-grid', 'wpd-spacer', 'wpd-divider', | |
| 479 | - 'wpd-tabs', 'wpd-tab', 'wpd-tabpanel', | |
| 480 | - 'wpd-segmented', 'wpd-segment', | |
| 481 | - 'wpd-button', 'wpd-icon-button', 'wpd-button-group', | |
| 482 | - 'wpd-text-field', 'wpd-textarea', 'wpd-search-field', | |
| 483 | - 'wpd-select', 'wpd-option', 'wpd-checkbox', 'wpd-checkbox-label', | |
| 484 | - 'wpd-radio', 'wpd-radio-group', 'wpd-form', | |
| 485 | - 'wpd-switch', 'wpd-slider', | |
| 486 | - 'wpd-table', 'wpd-table-column', 'wpd-table-row', 'wpd-table-cell', | |
| 487 | - 'wpd-card', 'wpd-list', 'wpd-list-item', | |
| 488 | - 'wpd-badge', 'wpd-pill', 'wpd-tag', 'wpd-chip', | |
| 489 | - 'wpd-spinner', 'wpd-skeleton', 'wpd-empty-state', | |
| 490 | - 'wpd-tooltip', 'wpd-popover', 'wpd-menu', 'wpd-menu-item', | |
| 491 | - 'wpd-modal', 'wpd-drawer', 'wpd-toast', | |
| 492 | - 'wpd-icon', 'wpd-avatar', 'wpd-heading', 'wpd-text', 'wpd-link', | |
| 493 | - 'wpd-banner', 'wpd-alert', 'wpd-callout', | |
| 494 | - 'wpd-form-row', 'wpd-form-section', 'wpd-help-text', | |
| 495 | - 'wpd-toolbar', 'wpd-toolbar-group', | |
| 664 | + 'os-stack', | |
| 665 | + 'os-cluster', | |
| 666 | + 'os-grid', | |
| 667 | + 'os-spacer', | |
| 668 | + 'os-divider', | |
| 669 | + 'os-tabs', | |
| 670 | + 'os-tab', | |
| 671 | + 'os-tabpanel', | |
| 672 | + 'os-segmented', | |
| 673 | + 'os-segment', | |
| 674 | + 'os-button', | |
| 675 | + 'os-icon-button', | |
| 676 | + 'os-button-group', | |
| 677 | + 'os-text-field', | |
| 678 | + 'os-textarea', | |
| 679 | + 'os-search-field', | |
| 680 | + 'os-select', | |
| 681 | + 'os-option', | |
| 682 | + 'os-checkbox', | |
| 683 | + 'os-checkbox-label', | |
| 684 | + 'os-radio', | |
| 685 | + 'os-radio-group', | |
| 686 | + 'os-form', | |
| 687 | + 'os-switch', | |
| 688 | + 'os-slider', | |
| 689 | + 'os-table', | |
| 690 | + 'os-table-column', | |
| 691 | + 'os-table-row', | |
| 692 | + 'os-table-cell', | |
| 693 | + 'os-card', | |
| 694 | + 'os-list', | |
| 695 | + 'os-list-item', | |
| 696 | + 'os-badge', | |
| 697 | + 'os-pill', | |
| 698 | + 'os-tag', | |
| 699 | + 'os-chip', | |
| 700 | + 'os-spinner', | |
| 701 | + 'os-skeleton', | |
| 702 | + 'os-empty-state', | |
| 703 | + 'os-tooltip', | |
| 704 | + 'os-popover', | |
| 705 | + 'os-menu', | |
| 706 | + 'os-menu-item', | |
| 707 | + 'os-modal', | |
| 708 | + 'os-drawer', | |
| 709 | + 'os-toast', | |
| 710 | + 'os-icon', | |
| 711 | + 'os-avatar', | |
| 712 | + 'os-heading', | |
| 713 | + 'os-text', | |
| 714 | + 'os-link', | |
| 715 | + 'os-banner', | |
| 716 | + 'os-alert', | |
| 717 | + 'os-callout', | |
| 718 | + 'os-form-row', | |
| 719 | + 'os-form-section', | |
| 720 | + 'os-help-text', | |
| 721 | + 'os-toolbar', | |
| 722 | + 'os-toolbar-group', | |
| 496 | 723 | ); |
| 497 | 724 | foreach ( $wpd_tags as $tag ) { |
| 498 | 725 | $extra[ $tag ] = $wpd_attrs; |
| 499 | 726 | } |
| @@ -500,9 +727,9 @@ | ||
| 500 | 727 | |
| 501 | 728 | $allowed = array_merge( $base, $extra ); |
| 502 | 729 | |
| 503 | 730 | // Promote the framework's global attrs (`slot`, `part`, |
| 504 | - // `full-width`, `data-*`, `aria-*`, …) to EVERY allowed tag — | |
| 731 | + // `full-width`, `data-*`, common ARIA, …) to EVERY allowed tag — | |
| 505 | 732 | // otherwise plain wrappers like `<div slot="header">` lose |
| 506 | 733 | // their `slot` attribute on the way through kses and get |
| 507 | 734 | // projected into the default slot instead of the named one. |
| 508 | 735 | // Caught by inspection when the Add User form's header |
| @@ -524,21 +751,19 @@ | ||
| 524 | 751 | * Plugins registering their own native windows can extend the |
| 525 | 752 | * list with custom tags or attributes if their templates need |
| 526 | 753 | * markup not covered here. |
| 527 | 754 | * |
| 528 | - * @since 0.6.2 | |
| 529 | - * | |
| 530 | 755 | * @param array $allowed wp_kses-shaped allowlist. |
| 531 | 756 | */ |
| 532 | - return (array) apply_filters( 'desktop_mode_native_window_allowed_html', $allowed ); | |
| 757 | + return (array) apply_filters( 'openstation_native_window_allowed_html', $allowed ); | |
| 533 | 758 | } |
| 534 | 759 | |
| 535 | 760 | /** |
| 536 | 761 | * Run `wp_kses` on a native-window template body with the framework |
| 537 | - * allowlist, **auto-extending the allowlist with every `<wpd-*>` tag | |
| 762 | + * allowlist, **auto-extending the allowlist with every `<os-*>` tag | |
| 538 | 763 | * the template actually uses.** |
| 539 | 764 | * |
| 540 | - * The pain this fixes: each shipped `<wpd-*>` component had to be | |
| 765 | + * The pain this fixes: each shipped `<os-*>` component had to be | |
| 541 | 766 | * manually added to the `$wpd_tags` list above, and the failure mode |
| 542 | 767 | * of forgetting it was silent — kses would strip the tag, the |
| 543 | 768 | * template would render as a sea of unparented children, and you'd |
| 544 | 769 | * spend an afternoon working out why "the form has no buttons." |
| @@ -543,29 +768,27 @@ | ||
| 543 | 768 | * template would render as a sea of unparented children, and you'd |
| 544 | 769 | * spend an afternoon working out why "the form has no buttons." |
| 545 | 770 | * |
| 546 | 771 | * Plugin authors registering a new component now only need to |
| 547 | - * `defineComponent('wpd-foo', WpdFoo)` on the JS side and use | |
| 548 | - * `<wpd-foo>` in their template — this helper finds the tag at | |
| 772 | + * `defineComponent('os-foo', OsFoo)` on the JS side and use | |
| 773 | + * `<os-foo>` in their template — this helper finds the tag at | |
| 549 | 774 | * render time, tags it onto the allowlist with the standard |
| 550 | 775 | * permissive attrs, and runs kses with the extended list. |
| 551 | 776 | * |
| 552 | 777 | * Every callsite in the framework that previously did the |
| 553 | - * `wp_kses( $html, desktop_mode_native_window_allowed_html() )` | |
| 778 | + * `wp_kses( $html, openstation_native_window_allowed_html() )` | |
| 554 | 779 | * dance can call this instead and get tag-discovery for free. |
| 555 | 780 | * |
| 556 | - * @since 0.18.0 | |
| 557 | - * | |
| 558 | 781 | * @param string $html Template HTML to sanitize. |
| 559 | 782 | * @return string Sanitized HTML. |
| 560 | 783 | */ |
| 561 | -function desktop_mode_kses_native_window_template( $html ) { | |
| 562 | - $allowed = desktop_mode_native_window_allowed_html(); | |
| 784 | +function openstation_kses_native_window_template( $html ) { | |
| 785 | + $allowed = openstation_native_window_allowed_html(); | |
| 563 | 786 | |
| 564 | - if ( preg_match_all( '/<(wpd-[a-z][a-z0-9-]*)\b/i', (string) $html, $matches ) ) { | |
| 565 | - $unique = array_unique( array_map( 'strtolower', $matches[1] ) ); | |
| 566 | - $wpd_attrs = isset( $allowed['wpd-button'] ) | |
| 567 | - ? $allowed['wpd-button'] | |
| 787 | + if ( preg_match_all( '/<(os-[a-z][a-z0-9-]*)\b/i', (string) $html, $matches ) ) { | |
| 788 | + $unique = array_unique( array_map( 'strtolower', $matches[1] ) ); | |
| 789 | + $wpd_attrs = isset( $allowed['os-button'] ) | |
| 790 | + ? $allowed['os-button'] | |
| 568 | 791 | : array(); |
| 569 | 792 | foreach ( $unique as $tag ) { |
| 570 | 793 | if ( ! isset( $allowed[ $tag ] ) ) { |
| 571 | 794 | $allowed[ $tag ] = $wpd_attrs; |
| @@ -575,14 +798,30 @@ | ||
| 575 | 798 | |
| 576 | 799 | return wp_kses( (string) $html, $allowed ); |
| 577 | 800 | } |
| 578 | 801 | |
| 579 | -function desktop_mode_build_native_window_template_html( $entry ) { | |
| 802 | +/** | |
| 803 | + * Render a native window's template HTML to a string, wrapping | |
| 804 | + * with tabs when the window has at least one additional tab | |
| 805 | + * registered. Shared by `openstation_render_native_window_templates()` | |
| 806 | + * (which emits the live `<template>` element) and | |
| 807 | + * `openstation_build_native_windows_payload()` (which captures the same | |
| 808 | + * string for the shell config so mid-session activation can inject | |
| 809 | + * the template without a reload). | |
| 810 | + * | |
| 811 | + * Single-tab windows (no additional tabs registered) render the | |
| 812 | + * same flat body they always did — backwards-compatible with | |
| 813 | + * every existing caller. | |
| 814 | + * | |
| 815 | + * @param array $entry Window registry entry. | |
| 816 | + * @return string Template body HTML (no outer `<template>` tag). | |
| 817 | + */ | |
| 818 | +function openstation_build_native_window_template_html( $entry ) { | |
| 580 | 819 | if ( ! is_array( $entry ) || ! is_callable( $entry['template'] ) ) { |
| 581 | 820 | return ''; |
| 582 | 821 | } |
| 583 | 822 | |
| 584 | - $tabs = desktop_mode_get_native_window_tabs( $entry['id'] ); | |
| 823 | + $tabs = openstation_get_native_window_tabs( $entry['id'] ); | |
| 585 | 824 | $has_extras = count( $tabs ) > 1; |
| 586 | 825 | |
| 587 | 826 | // Fast path — single-pane window, no wrapping. |
| 588 | 827 | if ( ! $has_extras ) { |
| @@ -590,21 +829,30 @@ | ||
| 590 | 829 | call_user_func( $entry['template'] ); |
| 591 | 830 | return (string) ob_get_clean(); |
| 592 | 831 | } |
| 593 | 832 | |
| 594 | - // Multi-tab window — wrap in <wpd-stack> + <wpd-tabs> + one | |
| 595 | - // <wpd-tabpanel> per tab. The default active tab is the main | |
| 596 | - // one (the window's own template). Plugin authors still get to | |
| 597 | - // declare their own tab-change side effects via the | |
| 598 | - // `wpd-tab-change` event bubbled by <wpd-tabs>. | |
| 833 | + // Multi-tab window — wrap in <os-stack> + one <os-tabpanel> per | |
| 834 | + // tab. The default active tab is the main one (the window's own | |
| 835 | + // template). | |
| 599 | 836 | // |
| 837 | + // The tab STRIP is deliberately absent from this markup. It is | |
| 838 | + // built by the shell in the window chrome, under the title bar, | |
| 839 | + // from the same tab metadata this function walks (the payload | |
| 840 | + // carries it as `tabs`). One tab strip per window, in one place, | |
| 841 | + // whether the window is an admin page in an iframe or a native | |
| 842 | + // window like this one. | |
| 843 | + // | |
| 844 | + // Plugin authors declare tab-change side effects by listening for | |
| 845 | + // `os-window-tab-change` on the window element; see | |
| 846 | + // docs/migration-window-tabs.md. | |
| 847 | + // | |
| 600 | 848 | // The wrap's padding is plugin-controllable two ways: |
| 601 | - // 1. `main_tab_padding` arg on `desktop_mode_register_window` — | |
| 602 | - // a per-window override. `0` opts into edge-to-edge | |
| 603 | - // content. | |
| 604 | - // 2. `desktop_mode_native_window_tab_wrap_padding` filter for | |
| 605 | - // late-bound overrides (e.g. a theme that wants every | |
| 606 | - // tabbed window to adopt a narrower inset). | |
| 849 | + // 1. `main_tab_padding` arg on `openstation_register_window` — | |
| 850 | + // a per-window override. `0` opts into edge-to-edge | |
| 851 | + // content. | |
| 852 | + // 2. `openstation_native_window_tab_wrap_padding` filter for | |
| 853 | + // late-bound overrides (e.g. a theme that wants every | |
| 854 | + // tabbed window to adopt a narrower inset). | |
| 607 | 855 | // Default stays 16px so existing plugins don't shift. |
| 608 | 856 | $default_padding = isset( $entry['main_tab_padding'] ) |
| 609 | 857 | && '' !== (string) $entry['main_tab_padding'] |
| 610 | 858 | ? (int) $entry['main_tab_padding'] |
| @@ -611,9 +859,9 @@ | ||
| 611 | 859 | : 16; |
| 612 | 860 | /** |
| 613 | 861 | * Filters the padding (in px) applied to the auto-generated |
| 614 | 862 | * tab wrap around a native window's template body. The shell |
| 615 | - * emits the wrap as `<wpd-stack padding="N">`; the CSS-as- | |
| 863 | + * emits the wrap as `<os-stack padding="N">`; the CSS-as- | |
| 616 | 864 | * attribute pipeline at the client translates that to |
| 617 | 865 | * `style.padding`. |
| 618 | 866 | * |
| 619 | 867 | * Return `0` for edge-to-edge content. Negative values are |
| @@ -618,15 +866,13 @@ | ||
| 618 | 866 | * |
| 619 | 867 | * Return `0` for edge-to-edge content. Negative values are |
| 620 | 868 | * clamped to 0. |
| 621 | 869 | * |
| 622 | - * @since 0.13.0 | |
| 623 | - * | |
| 624 | 870 | * @param int $padding Default padding in px. |
| 625 | 871 | * @param string $window_id The native window id. |
| 626 | 872 | */ |
| 627 | 873 | $padding = (int) apply_filters( |
| 628 | - 'desktop_mode_native_window_tab_wrap_padding', | |
| 874 | + 'openstation_native_window_tab_wrap_padding', | |
| 629 | 875 | $default_padding, |
| 630 | 876 | (string) $entry['id'] |
| 631 | 877 | ); |
| 632 | 878 | if ( $padding < 0 ) { |
| @@ -632,36 +878,25 @@ | ||
| 632 | 878 | if ( $padding < 0 ) { |
| 633 | 879 | $padding = 0; |
| 634 | 880 | } |
| 635 | 881 | |
| 636 | - $buffer = sprintf( | |
| 637 | - '<wpd-stack gap="12" padding="%d">', | |
| 882 | + $buffer = sprintf( | |
| 883 | + '<os-stack gap="12" padding="%d">', | |
| 638 | 884 | $padding |
| 639 | 885 | ); |
| 640 | - $buffer .= '<wpd-tabs value="' . esc_attr( DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB ) . '">'; | |
| 641 | - foreach ( $tabs as $tab ) { | |
| 642 | - $buffer .= sprintf( | |
| 643 | - '<wpd-tab value="%s">%s</wpd-tab>', | |
| 644 | - esc_attr( $tab['value'] ), | |
| 645 | - esc_html( $tab['label'] ) | |
| 646 | - ); | |
| 647 | - } | |
| 648 | - $buffer .= '</wpd-tabs>'; | |
| 649 | 886 | |
| 650 | 887 | // Stamp `hidden` on every non-active panel directly in the |
| 651 | - // emitted HTML. The client-side `<wpd-tabs>` syncs panel | |
| 652 | - // visibility on `value` changes, but its initial sync runs | |
| 653 | - // inside a microtask — and panel siblings may not have upgraded | |
| 654 | - // in time on first paint. Setting the attribute server-side | |
| 655 | - // makes first paint correct regardless of upgrade order; the JS | |
| 656 | - // keeps owning subsequent transitions. | |
| 888 | + // emitted HTML. The shell takes over panel visibility as soon as | |
| 889 | + // it declares the strip, but that happens after the template is | |
| 890 | + // in the body — setting the attribute server-side makes first | |
| 891 | + // paint correct rather than flashing every pane at once. | |
| 657 | 892 | foreach ( $tabs as $tab ) { |
| 658 | 893 | if ( ! is_callable( $tab['template'] ) ) { |
| 659 | 894 | continue; |
| 660 | 895 | } |
| 661 | - $is_active = DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB === $tab['value']; | |
| 896 | + $is_active = OPENSTATION_NATIVE_WINDOW_MAIN_TAB === $tab['value']; | |
| 662 | 897 | $buffer .= sprintf( |
| 663 | - '<wpd-tabpanel for="%s"%s>', | |
| 898 | + '<os-tabpanel for="%s"%s>', | |
| 664 | 899 | esc_attr( $tab['value'] ), |
| 665 | 900 | $is_active ? '' : ' hidden' |
| 666 | 901 | ); |
| 667 | 902 | ob_start(); |
| @@ -666,37 +901,99 @@ | ||
| 666 | 901 | ); |
| 667 | 902 | ob_start(); |
| 668 | 903 | call_user_func( $tab['template'] ); |
| 669 | 904 | $buffer .= (string) ob_get_clean(); |
| 670 | - $buffer .= '</wpd-tabpanel>'; | |
| 905 | + $buffer .= '</os-tabpanel>'; | |
| 671 | 906 | } |
| 672 | 907 | |
| 673 | - $buffer .= '</wpd-stack>'; | |
| 908 | + $buffer .= '</os-stack>'; | |
| 674 | 909 | return $buffer; |
| 675 | 910 | } |
| 676 | 911 | |
| 677 | 912 | /** |
| 678 | - * Enqueue every registered native window's script when the shell | |
| 679 | - * is active. Runs on `admin_enqueue_scripts` alongside the main | |
| 680 | - * shell enqueue so ordering (shell → plugin scripts) is | |
| 681 | - * deterministic. | |
| 913 | + * Run a native window's registered `config` through the | |
| 914 | + * `openstation_native_window_config` filter, normalized to an array. | |
| 682 | 915 | * |
| 683 | - * @since 0.10.0 | |
| 916 | + * Called at BOTH serialization points — the eager inline-script | |
| 917 | + * attach in `openstation_enqueue_native_window_scripts()` and the | |
| 918 | + * lazy `scriptL10n` synthesis in | |
| 919 | + * `openstation_build_native_windows_payload()` — so the filter sees | |
| 920 | + * every copy of the blob that can reach a browser. | |
| 921 | + * | |
| 922 | + * @param array $entry Registry entry (needs `id`; `config` optional). | |
| 923 | + * @return array Filtered config. Empty array when nothing to ship. | |
| 684 | 924 | */ |
| 685 | -function desktop_mode_enqueue_native_window_scripts() { | |
| 686 | - if ( ! desktop_mode_is_enabled() || desktop_mode_is_chromeless_request() || desktop_mode_is_classic_request() ) { | |
| 925 | +function openstation_filter_native_window_config( $entry ) { | |
| 926 | + $config = isset( $entry['config'] ) && is_array( $entry['config'] ) | |
| 927 | + ? $entry['config'] | |
| 928 | + : array(); | |
| 929 | + | |
| 930 | + /** | |
| 931 | + * Filter a native window's config blob at emit time. | |
| 932 | + * | |
| 933 | + * The registry snapshots `config` when `openstation_register_window()` | |
| 934 | + * runs — usually `init`. This filter runs when the blob is | |
| 935 | + * serialized for the browser (enqueue time on the eager path, | |
| 936 | + * payload-build time on the lazy path), so values that depend on | |
| 937 | + * hooks registered later in the bootstrap can be refreshed without | |
| 938 | + * moving the whole registration. The WP Explorer uses it to | |
| 939 | + * re-collect `previewActions` so plugins may add | |
| 940 | + * `openstation_my_wordpress_preview_actions` callbacks any time | |
| 941 | + * during a normal bootstrap, not just before `init` 99. | |
| 942 | + * | |
| 943 | + * Runs per request, after the current user is determined — | |
| 944 | + * capability-gated values are safe to compute here. | |
| 945 | + * | |
| 946 | + * **Status: Experimental** | |
| 947 | + * | |
| 948 | + * @param array $config Config blob as registered (empty array | |
| 949 | + * when the window registered none). | |
| 950 | + * @param string $window_id Native window id. | |
| 951 | + */ | |
| 952 | + $config = apply_filters( 'openstation_native_window_config', $config, (string) $entry['id'] ); | |
| 953 | + | |
| 954 | + return is_array( $config ) ? $config : array(); | |
| 955 | +} | |
| 956 | + | |
| 957 | +/** | |
| 958 | + * Attach every registered native window's script data, and enqueue | |
| 959 | + * the handful of bundles that asked to load at boot. | |
| 960 | + * | |
| 961 | + * **A native window's bundle is not enqueued here.** It loads the | |
| 962 | + * first time the window opens: the shell reads the render callback | |
| 963 | + * off `window.openStationNativeWindows[ <id> ]` at open time, so a | |
| 964 | + * bundle printed at boot is weight on every admin page the window is | |
| 965 | + * never opened from — and between WP Explorer, Posts, Plugins, | |
| 966 | + * Comments, the Recycle Bin, Content Graph, Games and the agent | |
| 967 | + * runner that came to well over a megabyte before a single window | |
| 968 | + * had been clicked. `preload_script` is the opt-out for a bundle | |
| 969 | + * with a genuine boot-time job. | |
| 970 | + * | |
| 971 | + * What still happens for EVERY window is the data attach: the | |
| 972 | + * localize blob and the `config` inline. Those hang off the | |
| 973 | + * REGISTERED handle whether or not it is enqueued, which is exactly | |
| 974 | + * how the lazy path gets them — `openstation_resolve_script_payload()` | |
| 975 | + * harvests both into the payload for the shell to replay around the | |
| 976 | + * script tag it injects. Hence priority 5: `openstation_enqueue_assets()` | |
| 977 | + * builds that payload at 10, and data attached after it would ship a | |
| 978 | + * bundle with no config. | |
| 979 | + */ | |
| 980 | +function openstation_enqueue_native_window_scripts() { | |
| 981 | + if ( ! openstation_is_shell_request() ) { | |
| 687 | 982 | return; |
| 688 | 983 | } |
| 689 | - $registry = desktop_mode_native_window_registry(); | |
| 984 | + $registry = openstation_native_window_registry(); | |
| 690 | 985 | if ( ! is_array( $registry ) ) { |
| 691 | 986 | return; |
| 692 | 987 | } |
| 693 | 988 | foreach ( $registry as $entry ) { |
| 694 | - // Enqueue per-tab scripts — each tab registration can carry | |
| 695 | - // its own script handle so a tab's JS module stays scoped to | |
| 696 | - // that tab. Main tab uses the window's own `script`; it's | |
| 697 | - // enqueued below alongside the localize call. | |
| 698 | - $tabs = desktop_mode_get_native_window_tabs( $entry['id'] ); | |
| 989 | + $preload = ! empty( $entry['preload_script'] ); | |
| 990 | + | |
| 991 | + // Per-tab scripts stay eager. The shell has no lazy path for | |
| 992 | + // them — a tab's script is not part of the window's own | |
| 993 | + // bundle chain — so deferring here would simply break the | |
| 994 | + // tab. The main tab uses the window's own `script`. | |
| 995 | + $tabs = openstation_get_native_window_tabs( $entry['id'] ); | |
| 699 | 996 | foreach ( $tabs as $tab ) { |
| 700 | 997 | if ( $tab['is_main'] || empty( $tab['script'] ) ) { |
| 701 | 998 | continue; |
| 702 | 999 | } |
| @@ -705,25 +1002,38 @@ | ||
| 705 | 1002 | |
| 706 | 1003 | if ( empty( $entry['script'] ) ) { |
| 707 | 1004 | continue; |
| 708 | 1005 | } |
| 709 | - wp_enqueue_script( $entry['script'] ); | |
| 1006 | + if ( $preload ) { | |
| 1007 | + wp_enqueue_script( $entry['script'] ); | |
| 1008 | + foreach ( (array) $entry['scripts'] as $companion ) { | |
| 1009 | + wp_enqueue_script( $companion ); | |
| 1010 | + } | |
| 1011 | + // Preload means "everything at boot" — companion styles | |
| 1012 | + // ride along so the window paints styled on a preloaded | |
| 1013 | + // first open, same as its scripts are already parsed. | |
| 1014 | + if ( ! empty( $entry['styles'] ) ) { | |
| 1015 | + foreach ( (array) $entry['styles'] as $companion_style ) { | |
| 1016 | + wp_enqueue_style( $companion_style ); | |
| 1017 | + } | |
| 1018 | + } | |
| 1019 | + } | |
| 710 | 1020 | // Localize the config the JS side reads to register itself. |
| 711 | 1021 | wp_localize_script( |
| 712 | 1022 | $entry['script'], |
| 713 | - 'desktopModeNativeWindow_' . str_replace( '-', '_', $entry['id'] ), | |
| 1023 | + 'openStationNativeWindow_' . str_replace( '-', '_', $entry['id'] ), | |
| 714 | 1024 | array( |
| 715 | - 'id' => $entry['id'], | |
| 716 | - 'title' => $entry['title'], | |
| 717 | - 'icon' => $entry['icon'], | |
| 718 | - 'width' => $entry['width'], | |
| 719 | - 'height' => $entry['height'], | |
| 720 | - 'minWidth' => $entry['min_width'], | |
| 721 | - 'minHeight' => $entry['min_height'], | |
| 722 | - 'placement' => $entry['placement'], | |
| 723 | - 'autofocus' => $entry['autofocus'], | |
| 724 | - 'templateId' => 'desktop-mode-native-window-' . $entry['id'], | |
| 725 | - 'tabs' => array_map( | |
| 1025 | + 'id' => $entry['id'], | |
| 1026 | + 'title' => $entry['title'], | |
| 1027 | + 'icon' => $entry['icon'], | |
| 1028 | + 'width' => $entry['width'], | |
| 1029 | + 'height' => $entry['height'], | |
| 1030 | + 'minWidth' => $entry['min_width'], | |
| 1031 | + 'minHeight' => $entry['min_height'], | |
| 1032 | + 'placement' => $entry['placement'], | |
| 1033 | + 'autofocus' => $entry['autofocus'], | |
| 1034 | + 'templateId' => 'os-native-window-' . $entry['id'], | |
| 1035 | + 'tabs' => array_map( | |
| 726 | 1036 | static function ( $tab ) { |
| 727 | 1037 | return array( |
| 728 | 1038 | 'value' => $tab['value'], |
| 729 | 1039 | 'label' => $tab['label'], |
| @@ -734,23 +1044,26 @@ | ||
| 734 | 1044 | ), |
| 735 | 1045 | ) |
| 736 | 1046 | ); |
| 737 | 1047 | |
| 738 | - // Bundle-bound `config` (since 0.6.0). Ships through | |
| 739 | - // `wp_add_inline_script` `'before'` so it lands on the eager | |
| 740 | - // path the same way `wp_localize_script` does, AND through | |
| 741 | - // the lazy-load payload (see `desktop_mode_resolve_script_payload`) | |
| 742 | - // so the same data is available even when the script is | |
| 743 | - // dynamically injected mid-session. The bundle reads it via | |
| 744 | - // `wp.desktop.getWindowConfig( id )` or directly at | |
| 745 | - // `window.desktopModeWindowConfig[ id ]`. | |
| 746 | - if ( ! empty( $entry['config'] ) && is_array( $entry['config'] ) ) { | |
| 1048 | + // Bundle-bound `config`, for the eager print path only. | |
| 1049 | + // `openstation_build_native_windows_payload()` synthesizes the | |
| 1050 | + // same assignment into the payload's `scriptL10n`, which is | |
| 1051 | + // what delivers it on the lazy path — and it has to, because | |
| 1052 | + // that payload is also built inside chromeless iframes, where | |
| 1053 | + // this function returns early. Attaching here unconditionally | |
| 1054 | + // would mean a shell page shipped the identical assignment | |
| 1055 | + // twice: once as `before`, once as `l10n`. The bundle reads it | |
| 1056 | + // via `wp.os.getWindowConfig( id )` or directly at | |
| 1057 | + // `window.openStationWindowConfig[ id ]`. | |
| 1058 | + $config = openstation_filter_native_window_config( $entry ); | |
| 1059 | + if ( $preload && ! empty( $config ) ) { | |
| 747 | 1060 | wp_add_inline_script( |
| 748 | 1061 | $entry['script'], |
| 749 | 1062 | sprintf( |
| 750 | - 'window.desktopModeWindowConfig=window.desktopModeWindowConfig||{};window.desktopModeWindowConfig[%s]=%s;', | |
| 1063 | + 'window.openStationWindowConfig=window.openStationWindowConfig||{};window.openStationWindowConfig[%s]=%s;', | |
| 751 | 1064 | wp_json_encode( $entry['id'] ), |
| 752 | - wp_json_encode( $entry['config'] ) | |
| 1065 | + wp_json_encode( $config ) | |
| 753 | 1066 | ), |
| 754 | 1067 | 'before' |
| 755 | 1068 | ); |
| 756 | 1069 | } |
| @@ -755,23 +1068,21 @@ | ||
| 755 | 1068 | ); |
| 756 | 1069 | } |
| 757 | 1070 | } |
| 758 | 1071 | } |
| 759 | -add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_native_window_scripts', 20 ); | |
| 1072 | +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 5 ); | |
| 760 | 1073 | |
| 761 | 1074 | /** |
| 762 | 1075 | * Emit a `<template>` tag for every registered native window on |
| 763 | 1076 | * `admin_footer` when the shell is active. The JS side resolves |
| 764 | - * these via `document.getElementById( `desktop-mode-native-window-${id}` )` | |
| 1077 | + * these via `document.getElementById( `os-native-window-${id}` )` | |
| 765 | 1078 | * and clones them into each opened window's body. |
| 766 | - * | |
| 767 | - * @since 0.10.0 | |
| 768 | 1079 | */ |
| 769 | -function desktop_mode_render_native_window_templates() { | |
| 770 | - if ( ! desktop_mode_is_enabled() || desktop_mode_is_chromeless_request() || desktop_mode_is_classic_request() ) { | |
| 1080 | +function openstation_render_native_window_templates() { | |
| 1081 | + if ( ! openstation_is_shell_request() ) { | |
| 771 | 1082 | return; |
| 772 | 1083 | } |
| 773 | - $registry = desktop_mode_native_window_registry(); | |
| 1084 | + $registry = openstation_native_window_registry(); | |
| 774 | 1085 | if ( ! is_array( $registry ) ) { |
| 775 | 1086 | return; |
| 776 | 1087 | } |
| 777 | 1088 | foreach ( $registry as $entry ) { |
| @@ -777,21 +1088,38 @@ | ||
| 777 | 1088 | foreach ( $registry as $entry ) { |
| 778 | 1089 | if ( ! is_callable( $entry['template'] ) ) { |
| 779 | 1090 | continue; |
| 780 | 1091 | } |
| 781 | - $html = desktop_mode_build_native_window_template_html( $entry ); | |
| 1092 | + $html = openstation_build_native_window_template_html( $entry ); | |
| 782 | 1093 | if ( '' === $html ) { |
| 783 | 1094 | continue; |
| 784 | 1095 | } |
| 785 | 1096 | printf( |
| 786 | - '<template id="desktop-mode-native-window-%s">', | |
| 1097 | + '<template id="os-native-window-%s">', | |
| 787 | 1098 | esc_attr( $entry['id'] ) |
| 788 | 1099 | ); |
| 789 | - // `desktop_mode_kses_native_window_template()` auto-extends | |
| 790 | - // the allowlist with any `<wpd-*>` tag the template carries | |
| 1100 | + // `openstation_kses_native_window_template()` auto-extends | |
| 1101 | + // the allowlist with any `<os-*>` tag the template carries | |
| 791 | 1102 | // — so plugin authors never have to remember to register |
| 792 | 1103 | // their custom component tags in the kses list. |
| 793 | - echo desktop_mode_kses_native_window_template( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes. | |
| 1104 | + echo openstation_kses_native_window_template( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes. | |
| 794 | 1105 | echo '</template>'; |
| 795 | 1106 | } |
| 796 | 1107 | } |
| 797 | -add_action( 'admin_footer', 'desktop_mode_render_native_window_templates', 20 ); | |
| 1108 | +add_action( 'admin_footer', 'openstation_render_native_window_templates', 20 ); | |
| 1109 | + | |
| 1110 | +/** | |
| 1111 | + * Whether a registered window is offered on the admin this request is | |
| 1112 | + * in: the network admin's shell offers `network` and `any` windows, | |
| 1113 | + * every site's shell offers `site` and `any`. See the `admin` arg of | |
| 1114 | + * {@see openstation_register_window()}. | |
| 1115 | + * | |
| 1116 | + * @param array<string,mixed> $entry Registry entry. | |
| 1117 | + * @return bool | |
| 1118 | + */ | |
| 1119 | +function openstation_native_window_offered_here( $entry ) { | |
| 1120 | + $admin = isset( $entry['admin'] ) ? (string) $entry['admin'] : 'site'; | |
| 1121 | + if ( 'any' === $admin ) { | |
| 1122 | + return true; | |
| 1123 | + } | |
| 1124 | + return is_network_admin() ? 'network' === $admin : 'site' === $admin; | |
| 1125 | +} | |