PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / render / assets.php

assets.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.7, at includes/render/assets.php

644 lines 30.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Asset enqueue.
4 *
5 * Loads the desktop shell CSS + JS bundles when desktop mode is
6 * active and the request isn't chromeless / classic-overridden.
7 * Owns the entire `desktop_mode_enqueue_assets()` body — the
8 * largest hook in the original render.php and the natural seam
9 * for "what does the shell ship to the browser today?".
10 *
11 * Extracted from `render.php` during the architecture-0.8.1 PHP
12 * slicing (phase 6).
13 *
14 * @package Desktop_Mode
15 * @since 0.8.1
16 */
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * Enqueues the desktop mode shell assets (CSS + JS) when desktop mode is active.
22 *
23 * Only loads the full desktop shell scripts and styles when the user has
24 * desktop mode enabled and the request is not a chromeless iframe load.
25 *
26 * @since 0.1.0
27 */
28 function desktop_mode_enqueue_assets() {
29 if ( ! is_admin() ) {
30 return;
31 }
32
33 // Auto-enqueue the iframe bridge anywhere a desktop-mode user
34 // might land. The bundle self-bails when not inside an iframe
35 // (`window.parent === window`), so it's a no-op on the parent
36 // shell — but cheap insurance against the failure mode the
37 // developer hit: an internal admin navigation drops the
38 // `?desktop_mode_chromeless=1` flag, the chromeless inline bridge doesn't
39 // run, and `wp.desktop.iframe` silently disappears. With this
40 // auto-enqueue, the API is universally present for any same-
41 // origin admin page a desktop-mode user opens — chromeless or
42 // accidentally classic.
43 if ( desktop_mode_is_enabled() ) {
44 wp_enqueue_script( 'desktop-mode-iframe-bridge' );
45
46 // Block Editor cross-window drop receiver. Listens for
47 // `desktop-mode-drop` postMessages from the parent shell and
48 // inserts the matching block. Only enqueue inside the
49 // post-edit Block Editor screens — every other admin page
50 // would be paying for a bundle it never uses.
51 //
52 // `site-editor.php` (full-site editor) deliberately omitted:
53 // the FSE doesn't expose `wp.data.dispatch('core/block-editor')`
54 // until the user opens a template in the canvas iframe, so
55 // drops arriving before that point would silently time out
56 // after the receiver's 5 s `waitForEditor()` poll. Re-enable
57 // once we have a reliable readiness signal in that context.
58 global $hook_suffix;
59 if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) {
60 wp_enqueue_script( 'desktop-mode-gutenberg-drop-receiver' );
61 }
62 }
63
64 // Chromeless requests (iframes) need chromeless styles and overrides.
65 if ( desktop_mode_is_chromeless_request() ) {
66 wp_enqueue_style( 'desktop-mode' );
67 wp_enqueue_style( 'desktop-mode-chromeless' );
68
69 /**
70 * Fires when chromeless styles are enqueued inside a desktop mode iframe.
71 *
72 * Plugin and theme authors can hook here to enqueue their own CSS
73 * overrides for legacy pages rendered in chromeless mode. Use the
74 * `.desktop-mode-chromeless` body class to scope your rules.
75 *
76 * @since 0.1.0
77 */
78 do_action( 'desktop_mode_chromeless_styles' );
79 return;
80 }
81
82 if ( ! desktop_mode_is_enabled() || desktop_mode_is_classic_request() ) {
83 return;
84 }
85
86 // CSS.
87 wp_enqueue_style( 'desktop-mode' );
88 wp_enqueue_style( 'desktop-mode-windows' );
89 wp_enqueue_style( 'desktop-mode-dock' );
90 wp_enqueue_style( 'desktop-mode-dock-peek' );
91 wp_enqueue_style( 'desktop-mode-ai-assistant' );
92 wp_enqueue_style( 'desktop-mode-bug-report' );
93 wp_enqueue_style( 'desktop-mode-files' );
94
95 // JS.
96 wp_enqueue_script( 'desktop-mode' );
97
98 // `wp_enqueue_command_palette_assets()` (WP 6.9+) enqueues the
99 // `wp-commands` store package, the `wp-core-commands` script that
100 // registers the WordPress-wide baseline (Add new post, Manage
101 // plugins, Switch theme, Browse patterns, …) AND — critically —
102 // the inline `wp.coreCommands.initializeCommandPalette( … )` call
103 // that actually populates the `core/commands` data store with the
104 // admin-menu commands. Without that inline init, the script loads
105 // but the store stays empty and `src/commands/shell-harvester.ts`
106 // finds nothing to publish.
107 //
108 // WP normally only calls this on screens that opt in to the native
109 // palette; the shell needs it on every admin URL it might wrap.
110 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
111 // no-ops when the store is missing.
112 if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
113 // `wp_enqueue_command_palette_assets()` calls
114 // `array_key_exists( $menu_slug, $submenu )` without guarding
115 // the global, so an unset `$submenu` (test contexts, edge-case
116 // admin requests where the menu wasn't built yet) blows up
117 // with a TypeError. Initialize defensively before calling.
118 global $menu, $submenu;
119 if ( ! isset( $submenu ) || ! is_array( $submenu ) ) {
120 $submenu = array();
121 }
122 if ( ! isset( $menu ) || ! is_array( $menu ) ) {
123 $menu = array();
124 }
125 wp_enqueue_command_palette_assets();
126
127 // Expose the same menu-commands array WP serializes into
128 // `wp.coreCommands.initializeCommandPalette(...)` on a window
129 // slot the shell harvester can read. Built in PHP from `$menu`
130 // / `$submenu` here (we already guarded that they're arrays
131 // above), then injected as a `before` inline on our own bundle
132 // — that runs synchronously before `desktop.min.js` boots the
133 // shell harvester, so the lookup is guaranteed populated by
134 // the time `src/commands/shell-harvester.ts` classifies any
135 // command. Decoupled from WP's command-palette mount timing
136 // (which fires from a core-registered hook we can't reorder).
137 $menu_map = desktop_mode_build_command_menu_map();
138 wp_add_inline_script(
139 'desktop-mode',
140 'window.__desktopModeMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
141 'before'
142 );
143 }
144
145 // Pass configuration to JavaScript.
146 global $title, $pagenow, $parent_file, $menu;
147
148 $menu_icon = 'dashicons-admin-generic';
149 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
150 foreach ( $menu as $item ) {
151 if ( ! empty( $item[2] ) && $item[2] === $parent_file && ! empty( $item[6] ) ) {
152 $menu_icon = $item[6];
153 break;
154 }
155 }
156 }
157
158 // Build dock items from the admin menu. Core pages are ordered
159 // first (Dashboard, Posts, Plugins, Users, Settings, …), then
160 // plugin-contributed top-level routes. `desktop_mode_dock_placement`
161 // is the per-item filter escape hatch for hiding. Shared with the
162 // REST menu endpoint so live refreshes (post plugin-activation)
163 // produce the same ordering as the boot payload.
164 $menu_payload = desktop_mode_build_menu_payload();
165 $dock_items = $menu_payload['dockItems'];
166 $native_windows = isset( $menu_payload['nativeWindows'] )
167 ? $menu_payload['nativeWindows']
168 : array();
169 $server_widgets = isset( $menu_payload['serverWidgets'] )
170 ? $menu_payload['serverWidgets']
171 : array();
172 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
173 ? $menu_payload['serverWallpapers']
174 : array();
175 $server_command_scripts = isset( $menu_payload['serverCommandScripts'] )
176 ? $menu_payload['serverCommandScripts']
177 : array();
178 $server_commands = isset( $menu_payload['serverCommands'] )
179 ? $menu_payload['serverCommands']
180 : array();
181 $server_settings_tab_scripts = isset( $menu_payload['serverSettingsTabScripts'] )
182 ? $menu_payload['serverSettingsTabScripts']
183 : array();
184 $server_settings_tabs = isset( $menu_payload['serverSettingsTabs'] )
185 ? $menu_payload['serverSettingsTabs']
186 : array();
187 $server_dock_rail_renderer_scripts = isset( $menu_payload['serverDockRailRendererScripts'] )
188 ? $menu_payload['serverDockRailRendererScripts']
189 : array();
190 $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] )
191 ? $menu_payload['serverTitleBarButtonScripts']
192 : array();
193 $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] )
194 ? $menu_payload['serverWindowThemeScripts']
195 : array();
196 $server_window_themes = isset( $menu_payload['serverWindowThemes'] )
197 ? $menu_payload['serverWindowThemes']
198 : array();
199 $server_window_control_scripts = isset( $menu_payload['serverWindowControlScripts'] )
200 ? $menu_payload['serverWindowControlScripts']
201 : array();
202 $server_window_controls = isset( $menu_payload['serverWindowControls'] )
203 ? $menu_payload['serverWindowControls']
204 : array();
205 $server_window_slot_scripts = isset( $menu_payload['serverWindowSlotScripts'] )
206 ? $menu_payload['serverWindowSlotScripts']
207 : array();
208 $server_window_slots = isset( $menu_payload['serverWindowSlots'] )
209 ? $menu_payload['serverWindowSlots']
210 : array();
211 $server_window_chrome_scripts = isset( $menu_payload['serverWindowChromeScripts'] )
212 ? $menu_payload['serverWindowChromeScripts']
213 : array();
214 $server_window_chromes = isset( $menu_payload['serverWindowChromes'] )
215 ? $menu_payload['serverWindowChromes']
216 : array();
217 $server_window_notices = isset( $menu_payload['serverWindowNotices'] )
218 ? $menu_payload['serverWindowNotices']
219 : array();
220 $desktop_icons = isset( $menu_payload['desktopIcons'] )
221 ? $menu_payload['desktopIcons']
222 : array();
223
224 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
225 // file types and openers ship as metadata only; the JS side
226 // holds the executable handlers and resolves on double-click.
227 $server_file_types = function_exists( 'desktop_mode_build_file_types_payload' )
228 ? desktop_mode_build_file_types_payload()
229 : array();
230 $server_file_openers = function_exists( 'desktop_mode_build_file_openers_payload' )
231 ? desktop_mode_build_file_openers_payload()
232 : array();
233 $user_file_associations = function_exists( 'desktop_mode_get_user_file_associations' )
234 ? desktop_mode_get_user_file_associations( get_current_user_id() )
235 : array();
236 $server_wallpaper_menu_items = function_exists( 'desktop_mode_build_wallpaper_menu_items' )
237 ? desktop_mode_build_wallpaper_menu_items()
238 : array();
239
240 /*
241 * OS-file drop config — what the browser drop manager will
242 * accept when the user drags a file from their native desktop
243 * onto any surface inside Desktop Mode (wallpaper, a folder,
244 * a window, or a chromeless iframe). The allowed-mimes list is
245 * the user-scoped `get_allowed_mime_types()` (already capability
246 * gated by WordPress); the size cap is `wp_max_upload_size()`.
247 *
248 * Both are filterable so plugins can narrow or widen the set —
249 * e.g. a media-only plugin can restrict drops to images, or a
250 * docs plugin can opt PDFs in for a specific role.
251 */
252 $drop_allowed_mimes_map = current_user_can( 'upload_files' )
253 ? get_allowed_mime_types( get_current_user_id() )
254 : array();
255 /**
256 * Filter the allowed-mime map used by the OS-file drop manager.
257 *
258 * @since 0.30.0
259 *
260 * @param array<string,string> $mimes_map `ext => mime-type` map (same shape `get_allowed_mime_types()` returns).
261 * @param int $user_id The current user id.
262 */
263 $drop_allowed_mimes_map = apply_filters( 'desktop_mode_drop_allowed_mimes', $drop_allowed_mimes_map, get_current_user_id() );
264 $drop_allowed_mimes_map = is_array( $drop_allowed_mimes_map ) ? $drop_allowed_mimes_map : array();
265 $drop_allowed_mimes = array_values( array_unique( array_values( $drop_allowed_mimes_map ) ) );
266
267 $drop_max_size = (int) wp_max_upload_size();
268 /**
269 * Filter the per-file size cap (in bytes) used by the OS-file
270 * drop manager. Returning `0` disables the client-side cap —
271 * the server still enforces its own.
272 *
273 * @since 0.30.0
274 *
275 * @param int $max_size Default `wp_max_upload_size()`.
276 * @param int $user_id The current user id.
277 */
278 $drop_max_size = (int) apply_filters( 'desktop_mode_drop_max_size', $drop_max_size, get_current_user_id() );
279
280 /**
281 * Filter the master OS-file drop enable gate. Lets plugins
282 * disable the drop manager by role / capability beyond the
283 * default `upload_files` check (e.g. only for admins, or
284 * only on specific multisite blogs).
285 *
286 * @since 0.30.0
287 *
288 * @param bool $enabled Default — `current_user_can( 'upload_files' )`.
289 * @param int $user_id The current user id.
290 */
291 $drop_enabled = (bool) apply_filters(
292 'desktop_mode_drop_enabled',
293 current_user_can( 'upload_files' ),
294 get_current_user_id()
295 );
296
297 $drop_config = array(
298 'enabled' => $drop_enabled,
299 'allowedMimes' => $drop_allowed_mimes,
300 'extToMime' => $drop_allowed_mimes_map,
301 'maxSize' => $drop_max_size,
302 );
303
304 // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant,
305 // About-scene, OS Settings panel, shell-overlays, window-system)
306 // is `<script>`-injected by the main bundle on demand — they don't
307 // go through `wp_register_script`, so they don't pick up WordPress's
308 // usual `?ver=<filemtime>` cache-buster. Without one, the browser
309 // happily serves a stale cached copy across plugin updates that
310 // don't bump `DESKTOP_MODE_VERSION`, and the main bundle's loader
311 // fires a `<script>`-loaded event for a file that's missing the
312 // fresh `window.desktopMode*` factory the new code expects.
313 //
314 // Mirror the `$built_version( … )` helper in `includes/assets.php`:
315 // prefer the on-disk mtime of the actual file, fall back to the
316 // plugin version when the file is missing (dev environments where
317 // the bundle hasn't been built yet).
318 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
319 $lazy_bundle_url = static function ( $base ) use ( $suffix ) {
320 $path = DESKTOP_MODE_DIR . 'assets/js/' . $base . $suffix . '.js';
321 $ver = file_exists( $path )
322 ? (string) filemtime( $path )
323 : DESKTOP_MODE_VERSION;
324 return esc_url_raw(
325 DESKTOP_MODE_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
326 );
327 };
328
329 // Build the current page URL from $pagenow + $_GET. Strip the portal
330 // markers so the derived window ID matches what the dock would produce
331 // for the same page — otherwise auto-opening the entry window and
332 // clicking the same dock icon would create a duplicate.
333 $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
334 unset( $current_query[ DESKTOP_MODE_PORTAL_FLAG ], $current_query[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] );
335 $current_page = admin_url( $pagenow ) . ( ! empty( $current_query ) ? '?' . http_build_query( $current_query ) : '' );
336
337 $from_portal = ! empty( $_GET[ DESKTOP_MODE_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
338 $from_portal_intent = ! empty( $_GET[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
339
340 /**
341 * Filters the desktop shell configuration passed to JavaScript.
342 *
343 * @since 0.1.0
344 *
345 * @param array $config {
346 * Desktop shell configuration.
347 *
348 * @type string $currentPage The current admin page URL.
349 * @type string $currentTitle The current page title.
350 * @type string $currentIcon Dashicon class for the current page.
351 * @type string $adminUrl The base admin URL.
352 * @type string $colorScheme The active admin color scheme.
353 * @type array $dockItems Dock items derived from the admin menu. Core WordPress pages (Dashboard, Posts, Plugins, Users, Settings, CPTs…) are ordered first; plugin-contributed top-level routes (admin.php?page=*) follow. Items hidden via `desktop_mode_dock_placement` are omitted.
354 * @type array $nativeWindows Server-declared native windows (via `desktop_mode_register_window`). Shell registers + syncs tiles based on this list — activation/deactivation is a diff without shell reload.
355 * @type array $serverWidgets Server-declared right-column widgets (via `desktop_mode_register_widget`). Shell syncs the widget registry + dynamically loads plugin scripts so widgets appear in the picker without a shell reload.
356 * @type array $serverWallpapers Server-declared wallpapers (via `desktop_mode_register_wallpaper`). Same lifecycle — shell loads the plugin's JS, reads the full `WallpaperDef` from `window.desktopModeWallpapers[id]`, and registers / unregisters as plugins activate / deactivate.
357 * @type array $serverCommandScripts Script handles opted-in via `desktop_mode_register_command_script`. Shell injects each URL on activation so commands registered by `wp.desktop.registerCommand` appear in the palette live. Deactivation unregisters any commands whose `owner` matches the departing handle.
358 * @type array $serverCommands Server-declared command metadata (via `desktop_mode_register_command`). Advisory today — reserved for future pre-registration shims.
359 * @type array $serverSettingsTabScripts Script handles opted-in via `desktop_mode_register_settings_tab_script`. Shell injects each URL on activation so tabs registered by `wp.desktop.registerSettingsTab` appear in the OS Settings window live. Deactivation unregisters tabs attributable to the departing handle.
360 * @type array $serverSettingsTabs Server-declared settings-tab metadata (via `desktop_mode_register_settings_tab`). Enables live unregistration on plugin deactivation without requiring JS to set `owner`.
361 * @type array $desktopIcons Server-declared desktop icons (via `desktop_mode_register_icon`). Rendered on the wallpaper as clickable shortcut tiles.
362 * @type array $accentColors Swatch list for the OS Settings accent picker. Filterable via `desktop_mode_accent_colors`.
363 * @type array $toastTypes Toast-notification type map. Filterable via `desktop_mode_toast_types`.
364 * @type string $defaultWallpaper Wallpaper slug applied on first boot. Filterable via `desktop_mode_default_wallpaper`.
365 * @type array $session Saved session (windows, focused, updated).
366 * @type string $sessionUrl REST endpoint for saving the session.
367 * @type string $mediaUrl REST endpoint for media uploads (wp/v2/media).
368 * @type string $defaultWindowUrl REST endpoint for saving the default-window preference.
369 * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference.
370 * @type bool $canUpload Whether the user holds the `upload_files` capability.
371 * @type string $pluginUrl Plugin base URL (no trailing slash). Used by the shell to locate vendor assets and by plugins to build asset URLs.
372 * @type string $pluginVersion Plugin semver string. Surfaced in the OS Settings → About tab; plugins can read it to gate features by version.
373 * @type string $restNonce Nonce for the session REST endpoint.
374 * @type string $portalUrl Canonical `/desktop-mode/` URL.
375 * @type bool $fromPortal Whether the shell was reached via the portal.
376 * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/desktop-mode/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for.
377 * @type array $seenIntros Slugs of one-time intro dialogs the user has dismissed (e.g. `['posts']`). Native windows gate their first-open intro on this list.
378 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
379 * }
380 */
381 $config = apply_filters(
382 'desktop_mode_shell_config',
383 array(
384 'currentPage' => esc_url( $current_page ),
385 'currentTitle' => wp_strip_all_tags( $title ),
386 'currentIcon' => sanitize_html_class( $menu_icon ),
387 'adminUrl' => esc_url( admin_url() ),
388 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ),
389 'dockItems' => $dock_items,
390 'nativeWindows' => $native_windows,
391 'serverWidgets' => $server_widgets,
392 'serverWallpapers' => $server_wallpapers,
393 'serverCommandScripts' => $server_command_scripts,
394 'serverCommands' => $server_commands,
395 'serverSettingsTabScripts' => $server_settings_tab_scripts,
396 'serverSettingsTabs' => $server_settings_tabs,
397 'serverDockRailRendererScripts' => $server_dock_rail_renderer_scripts,
398 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts,
399 'serverWindowThemeScripts' => $server_window_theme_scripts,
400 'serverWindowThemes' => $server_window_themes,
401 'serverWindowControlScripts' => $server_window_control_scripts,
402 'serverWindowControls' => $server_window_controls,
403 'serverWindowSlotScripts' => $server_window_slot_scripts,
404 'serverWindowSlots' => $server_window_slots,
405 'serverWindowChromeScripts' => $server_window_chrome_scripts,
406 'serverWindowChromes' => $server_window_chromes,
407 'serverWindowNotices' => $server_window_notices,
408 'desktopIcons' => $desktop_icons,
409 'serverFileTypes' => $server_file_types,
410 'serverFileOpeners' => $server_file_openers,
411 'userFileAssociations' => $user_file_associations,
412 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
413 'serverWallpaperMenuItems' => $server_wallpaper_menu_items,
414 'accentColors' => desktop_mode_get_accent_colors(),
415 'toastTypes' => desktop_mode_get_toast_types(),
416 'defaultWallpaper' => desktop_mode_get_default_wallpaper(),
417 'session' => desktop_mode_get_session( get_current_user_id() ),
418 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
419 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
420 'dropConfig' => $drop_config,
421 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
422 'defaultWindow' => desktop_mode_get_default_window( get_current_user_id() ),
423 'canUpload' => current_user_can( 'upload_files' ),
424 'pluginUrl' => esc_url_raw( untrailingslashit( DESKTOP_MODE_URL ) ),
425 'pluginVersion' => DESKTOP_MODE_VERSION,
426 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
427 // URL of the AI Assistant lazy bundle. The main bundle
428 // ships a stub matching the public `wp.desktop.ai` API; the
429 // stub `<script>`-injects this URL the first time the user
430 // opens the assistant. Picking `.js` vs `.min.js` here keeps
431 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
432 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
433 // URL of the About-scene lazy bundle. The OS Settings →
434 // About tab loads this on first mount; ~25 kB PixiJS
435 // particle scene that would otherwise ship in the main
436 // bundle for every shell load.
437 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ),
438 // URL of the OS Settings panel lazy bundle. Injected by
439 // the main bundle's `OsSettings.renderPanel()` stub on
440 // the user's first Settings open. Holds every section
441 // renderer + the `<wpd-*>` components only the panel
442 // uses, so nothing about Settings ships in
443 // `desktop.min.js` for users who never open it.
444 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
445 // URL of the shell-overlays lazy bundle. Pre-loaded by
446 // the main bundle after first paint so action-triggered
447 // overlays (toast, confirm dialog, context menus) feel
448 // instant the first time they fire.
449 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
450 // URL of the lazy window-system bundle (Stage 11).
451 // Holds the `Window` class and its DOM / pointer / tab /
452 // chrome helpers — the single largest module in the pre-
453 // 0.8.4 main bundle. Loaded on first `windowManager.open()`
454 // / `openNew()` call (both async since 0.8.4); pre-loaded
455 // by the shell after first paint when no session is being
456 // restored and no `openCurrentPage` will fire.
457 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
458 'restNonce' => wp_create_nonce( 'wp_rest' ),
459 'osSettings' => desktop_mode_get_os_settings( get_current_user_id() ),
460 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ),
461 'seenIntros' => desktop_mode_get_seen_intros( get_current_user_id() ),
462 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ),
463 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
464 'aiSearchStreamUrl' => esc_url_raw( add_query_arg( 'action', 'desktop_mode_ai_search_stream', admin_url( 'admin-ajax.php' ) ) ),
465 'aiPlatformSettings' => current_user_can( 'manage_options' ) ? desktop_mode_ai_get_platform_settings() : null,
466 'aiPlatformSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/platform-settings' ) ),
467 'aiProviders' => desktop_mode_ai_get_providers_for_config(),
468 'extendedOptions' => current_user_can( 'manage_options' ) ? desktop_mode_get_extended_options() : null,
469 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ),
470 // Comments-window AI moderation toggle — surfaced at the
471 // shell level so the OS Settings → Features tab can render
472 // the toggle without depending on the Comments window
473 // being registered for this user. URL is the same
474 // endpoint the comments-window config exposes; state is
475 // `null` for non-admins (the UI hides the row entirely).
476 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
477 'commentsAi' => current_user_can( 'manage_options' )
478 ? array(
479 'enabled' => function_exists( 'desktop_mode_comments_ai_is_enabled' )
480 ? desktop_mode_comments_ai_is_enabled()
481 : false,
482 'providerConfigured' => function_exists( 'desktop_mode_comments_ai_provider_configured' )
483 ? desktop_mode_comments_ai_provider_configured()
484 : false,
485 )
486 : null,
487 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
488 'portalUrl' => esc_url( desktop_mode_portal_url() ),
489 'fromPortal' => $from_portal,
490 'fromPortalIntent' => $from_portal_intent,
491 'pwa' => array(
492 'manifestUrl' => esc_url_raw( desktop_mode_pwa_manifest_url() ),
493 'swUrl' => esc_url_raw( desktop_mode_pwa_sw_url() ),
494 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
495 'state' => desktop_mode_pwa_get_user_state( get_current_user_id() ),
496 // Mirrors the manifest's `name` field — used by the
497 // install pill so the button reads "Install <site>"
498 // rather than "Install <current page>" (which would
499 // be misleading: we install the whole site as an
500 // app, not the dashboard window the user happens to
501 // be viewing).
502 'appName' => get_bloginfo( 'name' ),
503 // Operators set the `desktop_mode_pwa_force_replace_sw`
504 // filter to `true` when another root-scope service
505 // worker on the origin is blocking desktop-mode
506 // installability (foreign-SW guard in
507 // `src/pwa/sw-register.ts`). Default `false` preserves
508 // the polite behaviour where we yield to existing PWAs.
509 'forceReplaceSw' => desktop_mode_pwa_force_replace_sw(),
510 ),
511 )
512 );
513
514 wp_localize_script( 'desktop-mode', 'desktopModeConfig', $config );
515
516 /**
517 * Fires when desktop mode assets are enqueued.
518 *
519 * @since 0.1.0
520 */
521 do_action( 'desktop_mode_mode_init' );
522 }
523 add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_assets' );
524
525 /**
526 * Build the admin-menu command map (name → URL) and expose it on
527 * `window.__desktopModeMenuCommands`. The shell command harvester
528 * (`src/commands/shell-harvester.ts`) reads this slot to resolve URLs
529 * for "Go to: …" commands whose JS callbacks
530 * (`document.location = menuCommand.url`) close over a variable URL
531 * we can't extract from source. Without this map those commands
532 * either get skipped (no URL recoverable) or — if the location
533 * shadow misses — navigate the SHELL out of desktop mode.
534 *
535 * Mirrors what WordPress core's `wp_enqueue_command_palette_assets()`
536 * builds for `wp.coreCommands.initializeCommandPalette(...)`. We
537 * duplicate the logic here (instead of monkey-patching the JS init
538 * which is timing-sensitive — WP registers its hook during core load,
539 * so it always emits its inline before any plugin-added inline on the
540 * same handle) and ship the result through `wp_add_inline_script` on
541 * our own bundle handle. That decouples us entirely from WP's command-
542 * palette mount timing.
543 *
544 * @since 0.8.4
545 *
546 * @global array $menu
547 * @global array $submenu
548 * @return array<int, array{label:string, url:string, name:string}>
549 */
550 function desktop_mode_build_command_menu_map() {
551 global $menu, $submenu;
552 if ( ! is_array( $menu ) ) {
553 return array();
554 }
555 $out = array();
556
557 $extract_root_text = static function ( $label ) {
558 if ( '' === $label || ! is_string( $label ) ) {
559 return '';
560 }
561 if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
562 $processor = new WP_HTML_Tag_Processor( $label );
563 $text = '';
564 $depth = 0;
565 while ( $processor->next_token() ) {
566 $token_type = $processor->get_token_type();
567 if ( '#text' === $token_type && 0 === $depth ) {
568 $text .= $processor->get_modifiable_text();
569 }
570 if ( '#tag' === $token_type ) {
571 if ( $processor->is_tag_closer() ) {
572 if ( $depth > 0 ) {
573 --$depth;
574 }
575 continue;
576 }
577 $name = $processor->get_tag();
578 if ( $name && ! ( class_exists( 'WP_HTML_Processor' ) && WP_HTML_Processor::is_void( $name ) ) ) {
579 ++$depth;
580 }
581 }
582 }
583 return trim( $text );
584 }
585 return trim( wp_strip_all_tags( $label ) );
586 };
587
588 foreach ( $menu as $menu_item ) {
589 if ( empty( $menu_item[0] ) || ! is_string( $menu_item[0] ) ) {
590 continue;
591 }
592 if ( ! empty( $menu_item[1] ) && ! current_user_can( $menu_item[1] ) ) {
593 continue;
594 }
595 $menu_label = $extract_root_text( $menu_item[0] );
596 $menu_slug = $menu_item[2];
597 $menu_url = '';
598 if ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) {
599 $menu_url = $menu_slug;
600 } elseif ( ! empty( menu_page_url( $menu_slug, false ) ) ) {
601 $menu_url = menu_page_url( $menu_slug, false );
602 }
603 if ( '' !== $menu_url ) {
604 $out[] = array(
605 'label' => $menu_label,
606 'url' => $menu_url,
607 'name' => $menu_slug,
608 );
609 }
610 if ( ! empty( $submenu ) && is_array( $submenu ) && array_key_exists( $menu_slug, $submenu ) ) {
611 foreach ( $submenu[ $menu_slug ] as $submenu_item ) {
612 if ( empty( $submenu_item[0] ) ) {
613 continue;
614 }
615 if ( ! empty( $submenu_item[1] ) && ! current_user_can( $submenu_item[1] ) ) {
616 continue;
617 }
618 $submenu_label = $extract_root_text( $submenu_item[0] );
619 $submenu_slug = $submenu_item[2];
620 $submenu_url = '';
621 if ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) {
622 $submenu_url = $submenu_slug;
623 } elseif ( ! empty( menu_page_url( $submenu_slug, false ) ) ) {
624 $submenu_url = menu_page_url( $submenu_slug, false );
625 }
626 if ( '' === $submenu_url ) {
627 continue;
628 }
629 $out[] = array(
630 'label' => sprintf(
631 /* translators: 1: parent menu label, 2: submenu label */
632 __( '%1$s > %2$s', 'desktop-mode' ),
633 $menu_label,
634 $submenu_label
635 ),
636 'url' => $submenu_url,
637 'name' => $menu_slug . '-' . $submenu_item[2],
638 );
639 }
640 }
641 }
642 return $out;
643 }
644