PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/registries/native-windows.php +245 -24 1.1.01.1.10 View file →
@@ -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,27 @@
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.
95 167 * @type int $dock_order Sort key among system tiles,
96 168 * ascending; ties keep registration
97 169 * order. Default 0, which places the
98 170 * tile ahead of the shell's own
@@ -167,8 +239,11 @@
167 239 'title' => '',
168 240 'icon' => 'dashicons-admin-generic',
169 241 'template' => null,
170 242 'script' => '',
243 + 'scripts' => array(),
244 + 'styles' => array(),
245 + 'preload_script' => false,
171 246 // Optional WP style handle (registered with `wp_register_style()`).
172 247 // Resolved at payload-build time so the shell can lazy-inject a
173 248 // `<link rel="stylesheet">` when a peer plugin is activated
174 249 // mid-session — without this, the parent shell page already
@@ -179,8 +254,10 @@
179 254 'height' => 400,
180 255 'min_width' => 280,
181 256 'min_height' => 220,
182 257 'placement' => 'dock',
258 + 'admin' => 'site',
259 + 'nav_kind' => 'app',
183 260 'dock_order' => 0,
184 261 'placeable' => false,
185 262 'capabilities' => array(),
186 263 'autofocus' => false,
@@ -188,8 +265,11 @@
188 265 'main_tab_padding' => '',
189 266 'config' => array(),
190 267 );
191 268 $args = wp_parse_args( $args, $defaults );
269 + if ( ! in_array( $args['admin'], array( 'site', 'network', 'any' ), true ) ) {
270 + $args['admin'] = 'site';
271 + }
192 272
193 273 // Capability gate — ALL listed caps must match. Fail closed.
194 274 foreach ( (array) $args['capabilities'] as $cap ) {
195 275 if ( ! current_user_can( (string) $cap ) ) {
@@ -227,8 +307,17 @@
227 307 $placement = in_array( $args['placement'], array( 'dock', 'none' ), true )
228 308 ? $args['placement']
229 309 : 'dock';
230 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 +
231 320 $entry = array(
232 321 'id' => $id,
233 322 'title' => (string) $args['title'],
234 323 'icon' => (string) $args['icon'],
@@ -233,8 +322,32 @@
233 322 'title' => (string) $args['title'],
234 323 'icon' => (string) $args['icon'],
235 324 'template' => $args['template'],
236 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'],
237 350 'style' => (string) $args['style'],
238 351 'width' => (int) $args['width'],
239 352 'height' => (int) $args['height'],
240 353 'min_width' => (int) $args['min_width'],
@@ -239,8 +352,11 @@
239 352 'height' => (int) $args['height'],
240 353 'min_width' => (int) $args['min_width'],
241 354 'min_height' => (int) $args['min_height'],
242 355 'placement' => $placement,
356 + 'nav_kind' => $nav_kind,
357 + // Which admin's shell offers it; see the `admin` arg.
358 + 'admin' => $args['admin'],
243 359 // Sort key among system tiles, ascending. `0` (the default)
244 360 // puts a plugin's tile ahead of the shell's own trailing
245 361 // cluster — Mio 10, Overview 20, System 30 — which is where a
246 362 // launcher belongs. Trash uses 40 to sit at the very end.
@@ -313,9 +429,9 @@
313 429 * Templates are inert until JS clones them out of the `<template>`
314 430 * tag — but Plugin Check still requires escape-on-output. The list
315 431 * extends `wp_kses_allowed_html( 'post' )` with form controls,
316 432 * `<os-*>` web components, and dashicon spans, plus permissive
317 - * `data-*`, `aria-*`, and component-specific attributes. Plugins
433 + * `data-*`, common ARIA, and component-specific attributes. Plugins
318 434 * registering their own native windows can extend the list via the
319 435 * `openstation_native_window_allowed_html` filter below.
320 436 *
321 437 * @return array<string,array<string,bool>>
@@ -337,9 +453,15 @@
337 453 'dir' => true,
338 454 'draggable' => true,
339 455 'contenteditable' => true,
340 456 'data-*' => true,
341 - '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,
342 464 // `full-width` is a layout-level flag honoured by
343 465 // `<os-form>` (and any future os-* container that opts in
344 466 // to row-spanning slotted children). Lives in the global
345 467 // allowlist so a plain `<div full-width>` wrapper isn't
@@ -408,8 +530,9 @@
408 530 'bordered' => true,
409 531 'compact' => true,
410 532 'loading' => true,
411 533 'loading-rows' => true,
534 + 'empty' => true,
412 535 'columns' => true,
413 536 'rows' => true,
414 537 'sortable' => true,
415 538 'expandable' => true,
@@ -414,8 +537,9 @@
414 537 'sortable' => true,
415 538 'expandable' => true,
416 539 'preset' => true,
417 540 'label' => true,
541 + 'heading' => true,
418 542 'description' => true,
419 543 'orientation' => true,
420 544 'level' => true,
421 545 'collapsed' => true,
@@ -603,9 +727,9 @@
603 727
604 728 $allowed = array_merge( $base, $extra );
605 729
606 730 // Promote the framework's global attrs (`slot`, `part`,
607 - // `full-width`, `data-*`, `aria-*`, …) to EVERY allowed tag —
731 + // `full-width`, `data-*`, common ARIA, …) to EVERY allowed tag —
608 732 // otherwise plain wrappers like `<div slot="header">` lose
609 733 // their `slot` attribute on the way through kses and get
610 734 // projected into the default slot instead of the named one.
611 735 // Caught by inspection when the Add User form's header
@@ -785,15 +909,77 @@
785 909 return $buffer;
786 910 }
787 911
788 912 /**
789 - * Enqueue every registered native window's script when the shell
790 - * is active. Runs on `admin_enqueue_scripts` alongside the main
791 - * shell enqueue so ordering (shell → plugin scripts) is
792 - * 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.
793 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 + */
794 980 function openstation_enqueue_native_window_scripts() {
795 - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) {
981 + if ( ! openstation_is_shell_request() ) {
796 982 return;
797 983 }
798 984 $registry = openstation_native_window_registry();
799 985 if ( ! is_array( $registry ) ) {
@@ -799,12 +985,14 @@
799 985 if ( ! is_array( $registry ) ) {
800 986 return;
801 987 }
802 988 foreach ( $registry as $entry ) {
803 - // Enqueue per-tab scripts — each tab registration can carry
804 - // its own script handle so a tab's JS module stays scoped to
805 - // that tab. Main tab uses the window's own `script`; it's
806 - // 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`.
807 995 $tabs = openstation_get_native_window_tabs( $entry['id'] );
808 996 foreach ( $tabs as $tab ) {
809 997 if ( $tab['is_main'] || empty( $tab['script'] ) ) {
810 998 continue;
@@ -814,9 +1002,22 @@
814 1002
815 1003 if ( empty( $entry['script'] ) ) {
816 1004 continue;
817 1005 }
818 - 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 + }
819 1020 // Localize the config the JS side reads to register itself.
820 1021 wp_localize_script(
821 1022 $entry['script'],
822 1023 'openStationNativeWindow_' . str_replace( '-', '_', $entry['id'] ),
@@ -843,23 +1044,26 @@
843 1044 ),
844 1045 )
845 1046 );
846 1047
847 - // Bundle-bound `config`. Ships through
848 - // `wp_add_inline_script` `'before'` so it lands on the eager
849 - // path the same way `wp_localize_script` does, AND through
850 - // the lazy-load payload (see `openstation_resolve_script_payload`)
851 - // so the same data is available even when the script is
852 - // dynamically injected mid-session. The bundle reads it via
853 - // `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
854 1057 // `window.openStationWindowConfig[ id ]`.
855 - if ( ! empty( $entry['config'] ) && is_array( $entry['config'] ) ) {
1058 + $config = openstation_filter_native_window_config( $entry );
1059 + if ( $preload && ! empty( $config ) ) {
856 1060 wp_add_inline_script(
857 1061 $entry['script'],
858 1062 sprintf(
859 1063 'window.openStationWindowConfig=window.openStationWindowConfig||{};window.openStationWindowConfig[%s]=%s;',
860 1064 wp_json_encode( $entry['id'] ),
861 - wp_json_encode( $entry['config'] )
1065 + wp_json_encode( $config )
862 1066 ),
863 1067 'before'
864 1068 );
865 1069 }
@@ -864,9 +1068,9 @@
864 1068 );
865 1069 }
866 1070 }
867 1071 }
868 -add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 20 );
1072 +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 5 );
869 1073
870 1074 /**
871 1075 * Emit a `<template>` tag for every registered native window on
872 1076 * `admin_footer` when the shell is active. The JS side resolves
@@ -873,9 +1077,9 @@
873 1077 * these via `document.getElementById( `os-native-window-${id}` )`
874 1078 * and clones them into each opened window's body.
875 1079 */
876 1080 function openstation_render_native_window_templates() {
877 - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) {
1081 + if ( ! openstation_is_shell_request() ) {
878 1082 return;
879 1083 }
880 1084 $registry = openstation_native_window_registry();
881 1085 if ( ! is_array( $registry ) ) {
@@ -901,4 +1105,21 @@
901 1105 echo '</template>';
902 1106 }
903 1107 }
904 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 +}