| @@ -84,8 +84,61 @@ | ||
| 84 | 84 | * owns the JS render callback. |
| 85 | 85 | * Optional — omit for a purely |
| 86 | 86 | * declarative window whose body is |
| 87 | 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. | |
| 88 | 141 | * @type int $width Initial width (px). Default 520. |
| 89 | 142 | * @type int $height Initial height (px). Default 400. |
| 90 | 143 | * @type int $min_width Minimum width (px). Default 280. |
| 91 | 144 | * @type int $min_height Minimum height (px). Default 220. |
| @@ -91,8 +144,48 @@ | ||
| 91 | 144 | * @type int $min_height Minimum height (px). Default 220. |
| 92 | 145 | * @type string $placement 'dock' | 'none'. Default 'dock'. |
| 93 | 146 | * 'none' skips the tile (plugin |
| 94 | 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. | |
| 95 | 188 | * @type string[] $capabilities User capabilities that gate the |
| 96 | 189 | * registration. ANY miss returns |
| 97 | 190 | * `WP_Error openstation_capability_denied`. |
| 98 | 191 | * @type bool|string $autofocus Passed verbatim to |
| @@ -146,8 +239,11 @@ | ||
| 146 | 239 | 'title' => '', |
| 147 | 240 | 'icon' => 'dashicons-admin-generic', |
| 148 | 241 | 'template' => null, |
| 149 | 242 | 'script' => '', |
| 243 | + 'scripts' => array(), | |
| 244 | + 'styles' => array(), | |
| 245 | + 'preload_script' => false, | |
| 150 | 246 | // Optional WP style handle (registered with `wp_register_style()`). |
| 151 | 247 | // Resolved at payload-build time so the shell can lazy-inject a |
| 152 | 248 | // `<link rel="stylesheet">` when a peer plugin is activated |
| 153 | 249 | // mid-session — without this, the parent shell page already |
| @@ -158,8 +254,12 @@ | ||
| 158 | 254 | 'height' => 400, |
| 159 | 255 | 'min_width' => 280, |
| 160 | 256 | 'min_height' => 220, |
| 161 | 257 | 'placement' => 'dock', |
| 258 | + 'admin' => 'site', | |
| 259 | + 'nav_kind' => 'app', | |
| 260 | + 'dock_order' => 0, | |
| 261 | + 'placeable' => false, | |
| 162 | 262 | 'capabilities' => array(), |
| 163 | 263 | 'autofocus' => false, |
| 164 | 264 | 'main_tab_label' => '', |
| 165 | 265 | 'main_tab_padding' => '', |
| @@ -165,8 +265,11 @@ | ||
| 165 | 265 | 'main_tab_padding' => '', |
| 166 | 266 | 'config' => array(), |
| 167 | 267 | ); |
| 168 | 268 | $args = wp_parse_args( $args, $defaults ); |
| 269 | + if ( ! in_array( $args['admin'], array( 'site', 'network', 'any' ), true ) ) { | |
| 270 | + $args['admin'] = 'site'; | |
| 271 | + } | |
| 169 | 272 | |
| 170 | 273 | // Capability gate — ALL listed caps must match. Fail closed. |
| 171 | 274 | foreach ( (array) $args['capabilities'] as $cap ) { |
| 172 | 275 | if ( ! current_user_can( (string) $cap ) ) { |
| @@ -204,8 +307,17 @@ | ||
| 204 | 307 | $placement = in_array( $args['placement'], array( 'dock', 'none' ), true ) |
| 205 | 308 | ? $args['placement'] |
| 206 | 309 | : 'dock'; |
| 207 | 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 | + | |
| 208 | 320 | $entry = array( |
| 209 | 321 | 'id' => $id, |
| 210 | 322 | 'title' => (string) $args['title'], |
| 211 | 323 | 'icon' => (string) $args['icon'], |
| @@ -210,8 +322,32 @@ | ||
| 210 | 322 | 'title' => (string) $args['title'], |
| 211 | 323 | 'icon' => (string) $args['icon'], |
| 212 | 324 | 'template' => $args['template'], |
| 213 | 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'], | |
| 214 | 350 | 'style' => (string) $args['style'], |
| 215 | 351 | 'width' => (int) $args['width'], |
| 216 | 352 | 'height' => (int) $args['height'], |
| 217 | 353 | 'min_width' => (int) $args['min_width'], |
| @@ -216,8 +352,17 @@ | ||
| 216 | 352 | 'height' => (int) $args['height'], |
| 217 | 353 | 'min_width' => (int) $args['min_width'], |
| 218 | 354 | 'min_height' => (int) $args['min_height'], |
| 219 | 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'], | |
| 220 | 365 | 'autofocus' => $args['autofocus'], |
| 221 | 366 | 'main_tab_label' => (string) $args['main_tab_label'], |
| 222 | 367 | // Stored as-is (string or int). `openstation_build_native_window_template_html` |
| 223 | 368 | // coerces to int and falls back to 16 when absent. |
| @@ -284,9 +429,9 @@ | ||
| 284 | 429 | * Templates are inert until JS clones them out of the `<template>` |
| 285 | 430 | * tag — but Plugin Check still requires escape-on-output. The list |
| 286 | 431 | * extends `wp_kses_allowed_html( 'post' )` with form controls, |
| 287 | 432 | * `<os-*>` web components, and dashicon spans, plus permissive |
| 288 | - * `data-*`, `aria-*`, and component-specific attributes. Plugins | |
| 433 | + * `data-*`, common ARIA, and component-specific attributes. Plugins | |
| 289 | 434 | * registering their own native windows can extend the list via the |
| 290 | 435 | * `openstation_native_window_allowed_html` filter below. |
| 291 | 436 | * |
| 292 | 437 | * @return array<string,array<string,bool>> |
| @@ -308,9 +453,15 @@ | ||
| 308 | 453 | 'dir' => true, |
| 309 | 454 | 'draggable' => true, |
| 310 | 455 | 'contenteditable' => true, |
| 311 | 456 | 'data-*' => true, |
| 312 | - '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, | |
| 313 | 464 | // `full-width` is a layout-level flag honoured by |
| 314 | 465 | // `<os-form>` (and any future os-* container that opts in |
| 315 | 466 | // to row-spanning slotted children). Lives in the global |
| 316 | 467 | // allowlist so a plain `<div full-width>` wrapper isn't |
| @@ -379,8 +530,9 @@ | ||
| 379 | 530 | 'bordered' => true, |
| 380 | 531 | 'compact' => true, |
| 381 | 532 | 'loading' => true, |
| 382 | 533 | 'loading-rows' => true, |
| 534 | + 'empty' => true, | |
| 383 | 535 | 'columns' => true, |
| 384 | 536 | 'rows' => true, |
| 385 | 537 | 'sortable' => true, |
| 386 | 538 | 'expandable' => true, |
| @@ -385,8 +537,9 @@ | ||
| 385 | 537 | 'sortable' => true, |
| 386 | 538 | 'expandable' => true, |
| 387 | 539 | 'preset' => true, |
| 388 | 540 | 'label' => true, |
| 541 | + 'heading' => true, | |
| 389 | 542 | 'description' => true, |
| 390 | 543 | 'orientation' => true, |
| 391 | 544 | 'level' => true, |
| 392 | 545 | 'collapsed' => true, |
| @@ -574,9 +727,9 @@ | ||
| 574 | 727 | |
| 575 | 728 | $allowed = array_merge( $base, $extra ); |
| 576 | 729 | |
| 577 | 730 | // Promote the framework's global attrs (`slot`, `part`, |
| 578 | - // `full-width`, `data-*`, `aria-*`, …) to EVERY allowed tag — | |
| 731 | + // `full-width`, `data-*`, common ARIA, …) to EVERY allowed tag — | |
| 579 | 732 | // otherwise plain wrappers like `<div slot="header">` lose |
| 580 | 733 | // their `slot` attribute on the way through kses and get |
| 581 | 734 | // projected into the default slot instead of the named one. |
| 582 | 735 | // Caught by inspection when the Add User form's header |
| @@ -676,14 +829,23 @@ | ||
| 676 | 829 | call_user_func( $entry['template'] ); |
| 677 | 830 | return (string) ob_get_clean(); |
| 678 | 831 | } |
| 679 | 832 | |
| 680 | - // Multi-tab window — wrap in <os-stack> + <os-tabs> + one | |
| 681 | - // <os-tabpanel> per tab. The default active tab is the main | |
| 682 | - // one (the window's own template). Plugin authors still get to | |
| 683 | - // declare their own tab-change side effects via the | |
| 684 | - // `os-tab-change` event bubbled by <os-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). | |
| 685 | 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 | + // | |
| 686 | 848 | // The wrap's padding is plugin-controllable two ways: |
| 687 | 849 | // 1. `main_tab_padding` arg on `openstation_register_window` — |
| 688 | 850 | // a per-window override. `0` opts into edge-to-edge |
| 689 | 851 | // content. |
| @@ -716,29 +878,18 @@ | ||
| 716 | 878 | if ( $padding < 0 ) { |
| 717 | 879 | $padding = 0; |
| 718 | 880 | } |
| 719 | 881 | |
| 720 | - $buffer = sprintf( | |
| 882 | + $buffer = sprintf( | |
| 721 | 883 | '<os-stack gap="12" padding="%d">', |
| 722 | 884 | $padding |
| 723 | 885 | ); |
| 724 | - $buffer .= '<os-tabs value="' . esc_attr( OPENSTATION_NATIVE_WINDOW_MAIN_TAB ) . '">'; | |
| 725 | - foreach ( $tabs as $tab ) { | |
| 726 | - $buffer .= sprintf( | |
| 727 | - '<os-tab value="%s">%s</os-tab>', | |
| 728 | - esc_attr( $tab['value'] ), | |
| 729 | - esc_html( $tab['label'] ) | |
| 730 | - ); | |
| 731 | - } | |
| 732 | - $buffer .= '</os-tabs>'; | |
| 733 | 886 | |
| 734 | 887 | // Stamp `hidden` on every non-active panel directly in the |
| 735 | - // emitted HTML. The client-side `<os-tabs>` syncs panel | |
| 736 | - // visibility on `value` changes, but its initial sync runs | |
| 737 | - // inside a microtask — and panel siblings may not have upgraded | |
| 738 | - // in time on first paint. Setting the attribute server-side | |
| 739 | - // makes first paint correct regardless of upgrade order; the JS | |
| 740 | - // 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. | |
| 741 | 892 | foreach ( $tabs as $tab ) { |
| 742 | 893 | if ( ! is_callable( $tab['template'] ) ) { |
| 743 | 894 | continue; |
| 744 | 895 | } |
| @@ -758,15 +909,77 @@ | ||
| 758 | 909 | return $buffer; |
| 759 | 910 | } |
| 760 | 911 | |
| 761 | 912 | /** |
| 762 | - * Enqueue every registered native window's script when the shell | |
| 763 | - * is active. Runs on `admin_enqueue_scripts` alongside the main | |
| 764 | - * shell enqueue so ordering (shell → plugin scripts) is | |
| 765 | - * deterministic. | |
| 913 | + * Run a native window's registered `config` through the | |
| 914 | + * `openstation_native_window_config` filter, normalized to an array. | |
| 915 | + * | |
| 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. | |
| 766 | 924 | */ |
| 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 | + */ | |
| 767 | 980 | function openstation_enqueue_native_window_scripts() { |
| 768 | - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) { | |
| 981 | + if ( ! openstation_is_shell_request() ) { | |
| 769 | 982 | return; |
| 770 | 983 | } |
| 771 | 984 | $registry = openstation_native_window_registry(); |
| 772 | 985 | if ( ! is_array( $registry ) ) { |
| @@ -772,12 +985,14 @@ | ||
| 772 | 985 | if ( ! is_array( $registry ) ) { |
| 773 | 986 | return; |
| 774 | 987 | } |
| 775 | 988 | foreach ( $registry as $entry ) { |
| 776 | - // Enqueue per-tab scripts — each tab registration can carry | |
| 777 | - // its own script handle so a tab's JS module stays scoped to | |
| 778 | - // that tab. Main tab uses the window's own `script`; it's | |
| 779 | - // enqueued below alongside the localize call. | |
| 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`. | |
| 780 | 995 | $tabs = openstation_get_native_window_tabs( $entry['id'] ); |
| 781 | 996 | foreach ( $tabs as $tab ) { |
| 782 | 997 | if ( $tab['is_main'] || empty( $tab['script'] ) ) { |
| 783 | 998 | continue; |
| @@ -787,9 +1002,22 @@ | ||
| 787 | 1002 | |
| 788 | 1003 | if ( empty( $entry['script'] ) ) { |
| 789 | 1004 | continue; |
| 790 | 1005 | } |
| 791 | - 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 | + } | |
| 792 | 1020 | // Localize the config the JS side reads to register itself. |
| 793 | 1021 | wp_localize_script( |
| 794 | 1022 | $entry['script'], |
| 795 | 1023 | 'openStationNativeWindow_' . str_replace( '-', '_', $entry['id'] ), |
| @@ -816,23 +1044,26 @@ | ||
| 816 | 1044 | ), |
| 817 | 1045 | ) |
| 818 | 1046 | ); |
| 819 | 1047 | |
| 820 | - // Bundle-bound `config`. Ships through | |
| 821 | - // `wp_add_inline_script` `'before'` so it lands on the eager | |
| 822 | - // path the same way `wp_localize_script` does, AND through | |
| 823 | - // the lazy-load payload (see `openstation_resolve_script_payload`) | |
| 824 | - // so the same data is available even when the script is | |
| 825 | - // dynamically injected mid-session. The bundle reads it via | |
| 826 | - // `wp.os.getWindowConfig( id )` or directly at | |
| 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 | |
| 827 | 1057 | // `window.openStationWindowConfig[ id ]`. |
| 828 | - if ( ! empty( $entry['config'] ) && is_array( $entry['config'] ) ) { | |
| 1058 | + $config = openstation_filter_native_window_config( $entry ); | |
| 1059 | + if ( $preload && ! empty( $config ) ) { | |
| 829 | 1060 | wp_add_inline_script( |
| 830 | 1061 | $entry['script'], |
| 831 | 1062 | sprintf( |
| 832 | 1063 | 'window.openStationWindowConfig=window.openStationWindowConfig||{};window.openStationWindowConfig[%s]=%s;', |
| 833 | 1064 | wp_json_encode( $entry['id'] ), |
| 834 | - wp_json_encode( $entry['config'] ) | |
| 1065 | + wp_json_encode( $config ) | |
| 835 | 1066 | ), |
| 836 | 1067 | 'before' |
| 837 | 1068 | ); |
| 838 | 1069 | } |
| @@ -837,9 +1068,9 @@ | ||
| 837 | 1068 | ); |
| 838 | 1069 | } |
| 839 | 1070 | } |
| 840 | 1071 | } |
| 841 | -add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 20 ); | |
| 1072 | +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 5 ); | |
| 842 | 1073 | |
| 843 | 1074 | /** |
| 844 | 1075 | * Emit a `<template>` tag for every registered native window on |
| 845 | 1076 | * `admin_footer` when the shell is active. The JS side resolves |
| @@ -846,9 +1077,9 @@ | ||
| 846 | 1077 | * these via `document.getElementById( `os-native-window-${id}` )` |
| 847 | 1078 | * and clones them into each opened window's body. |
| 848 | 1079 | */ |
| 849 | 1080 | function openstation_render_native_window_templates() { |
| 850 | - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) { | |
| 1081 | + if ( ! openstation_is_shell_request() ) { | |
| 851 | 1082 | return; |
| 852 | 1083 | } |
| 853 | 1084 | $registry = openstation_native_window_registry(); |
| 854 | 1085 | if ( ! is_array( $registry ) ) { |
| @@ -874,4 +1105,21 @@ | ||
| 874 | 1105 | echo '</template>'; |
| 875 | 1106 | } |
| 876 | 1107 | } |
| 877 | 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 | +} | |