| @@ -348,8 +348,11 @@ | ||
| 348 | 348 | } |
| 349 | 349 | $sub_entry = array( |
| 350 | 350 | 'title' => $sub_title, |
| 351 | 351 | 'url' => $sub_url, |
| 352 | + // The registered slug, kept for the window-tab merge | |
| 353 | + // below and stripped again before the payload ships. | |
| 354 | + 'slug' => (string) $row['slug'], | |
| 352 | 355 | ); |
| 353 | 356 | if ( $row['external'] ) { |
| 354 | 357 | // Consumers that route a URL into a window skip these; |
| 355 | 358 | // the ones that can hand a link to the browser mark |
| @@ -399,8 +402,47 @@ | ||
| 399 | 402 | if ( $parent_is_container && $url === $parent_url ) { |
| 400 | 403 | continue; |
| 401 | 404 | } |
| 402 | 405 | |
| 406 | + // A native window in charge of this menu owns its rows too: | |
| 407 | + // whatever it offers as a tab, the dock offers as a row, same | |
| 408 | + // labels and same order, each one tagged with the tab it | |
| 409 | + // opens. The first tab IS the menu's own page, so it becomes | |
| 410 | + // the self-label rather than a second row for the tile's own | |
| 411 | + // destination. See `App::menu()`. | |
| 412 | + $window_tabs = openstation_app_menu_tabs( $identity_slug ); | |
| 413 | + if ( $window_tabs ) { | |
| 414 | + $self_label = $window_tabs[0]['label']; | |
| 415 | + $claimed = array(); | |
| 416 | + foreach ( $window_tabs as $tab ) { | |
| 417 | + if ( '' !== $tab['page'] ) { | |
| 418 | + $claimed[] = $tab['page']; | |
| 419 | + } | |
| 420 | + } | |
| 421 | + // A page this window has no tab for is still a page: a | |
| 422 | + // plugin's screen registered under this menu, a taxonomy | |
| 423 | + // someone added. Dropping those would make them | |
| 424 | + // unreachable from the dock, so they follow the window's | |
| 425 | + // own rows rather than being replaced by them. | |
| 426 | + $kept = array(); | |
| 427 | + foreach ( $sub_items as $sub_entry ) { | |
| 428 | + if ( ! in_array( $sub_entry['slug'], $claimed, true ) ) { | |
| 429 | + $kept[] = $sub_entry; | |
| 430 | + } | |
| 431 | + } | |
| 432 | + $sub_items = array(); | |
| 433 | + foreach ( array_slice( $window_tabs, 1 ) as $tab ) { | |
| 434 | + $sub_items[] = array( | |
| 435 | + 'title' => $tab['label'], | |
| 436 | + 'url' => add_query_arg( 'os_tab', $tab['id'], $url ), | |
| 437 | + ); | |
| 438 | + } | |
| 439 | + $sub_items = array_merge( $sub_items, $kept ); | |
| 440 | + } | |
| 441 | + foreach ( $sub_items as $i => $sub_entry ) { | |
| 442 | + unset( $sub_items[ $i ]['slug'] ); | |
| 443 | + } | |
| 444 | + | |
| 403 | 445 | $dock_item = array( |
| 404 | 446 | 'id' => sanitize_key( $item[5] ?? $item[2] ), |
| 405 | 447 | 'title' => $title, |
| 406 | 448 | 'icon' => $icon, |
| @@ -730,12 +772,13 @@ | ||
| 730 | 772 | |
| 731 | 773 | /** |
| 732 | 774 | * Filters whether a dock item supports multiple open windows. |
| 733 | 775 | * |
| 734 | - * Return true to let the user open more than one window of this page. | |
| 735 | - * A "+" affordance appears on the dock icon and a "Open another" action | |
| 736 | - * becomes available in the window's title-bar menu. Singletons (false) | |
| 737 | - * always focus the existing window when re-opened. | |
| 776 | + * Return true to advertise this page as multi-capable: an instance | |
| 777 | + * rail appears under the dock icon and an "Open another" action | |
| 778 | + * becomes available in the window's title-bar menu. It does not gate | |
| 779 | + * the submenu, which opens a window of its own on every pick either | |
| 780 | + * way; a tile click focuses the menu's open window. | |
| 738 | 781 | * |
| 739 | 782 | * @param bool $multi Whether this page is multi-capable. |
| 740 | 783 | * @param string $menu_slug The menu slug (e.g. `edit.php?post_type=page`). |
| 741 | 784 | */ |
| @@ -1678,8 +1721,13 @@ | ||
| 1678 | 1721 | // the server's capability-gated view). See |
| 1679 | 1722 | // openstation_menu_signature(). |
| 1680 | 1723 | $payload['menuSig'] = openstation_menu_signature(); |
| 1681 | 1724 | |
| 1725 | + // Each script dependency's payload once, entries carry handles. | |
| 1726 | + $script_dep_payloads = array(); | |
| 1727 | + $payload = openstation_compact_script_deps( $payload, $script_dep_payloads ); | |
| 1728 | + $payload['scriptDepPayloads'] = (object) $script_dep_payloads; | |
| 1729 | + | |
| 1682 | 1730 | return $payload; |
| 1683 | 1731 | } |
| 1684 | 1732 | |
| 1685 | 1733 | /** |
| @@ -1899,8 +1947,102 @@ | ||
| 1899 | 1947 | return $out; |
| 1900 | 1948 | } |
| 1901 | 1949 | |
| 1902 | 1950 | /** |
| 1951 | + * Move every entry's `scriptDeps` payloads into one map. | |
| 1952 | + * | |
| 1953 | + * {@see openstation_resolve_script_dependencies()} returns the full | |
| 1954 | + * payload of each dependency (URL, l10n, before/after), and ~20 entry | |
| 1955 | + * builders call it. A dependency shared by N entries was therefore | |
| 1956 | + * serialized N times: one plugin's 5.5 KB localized object became | |
| 1957 | + * ~400 KB of a 489 KB `openStationConfig` (GH#892). This replaces each | |
| 1958 | + * entry's `scriptDeps` list with its handles and puts each handle's | |
| 1959 | + * payload in `$map` once. The shell resolves the handles back before | |
| 1960 | + * any consumer reads them; see `src/script-dep-payloads.ts`. | |
| 1961 | + * | |
| 1962 | + * ENTRY DEPTH ONLY. `$payload` is a payload whose top-level values are | |
| 1963 | + * entry lists (`serverWidgets`, `serverCommandScripts`, ...), and only | |
| 1964 | + * a `scriptDeps` sitting directly on one of those entries is touched. | |
| 1965 | + * The key is the shell's there. Deeper down it is a plugin's metadata | |
| 1966 | + * (`settings => [ 'scriptDeps' => ... ]`), and rewriting it would hoist | |
| 1967 | + * foreign data into the map first-wins and hydrate every real | |
| 1968 | + * dependency of that handle to it. Scoping by depth rather than by a | |
| 1969 | + * list of keys means a new builder is covered without an edit here. | |
| 1970 | + * It also leaves every other branch of the payload unassigned, so | |
| 1971 | + * PHP's copy-on-write never has to copy them. | |
| 1972 | + * | |
| 1973 | + * Every string left in a compacted list has an entry in `$map`: a bare | |
| 1974 | + * handle a builder (or a filter on one) emitted is resolved here, by | |
| 1975 | + * the same rule as {@see openstation_resolve_script_dependencies()}. | |
| 1976 | + * A handle with nothing to fetch and nothing to run is dropped, as it | |
| 1977 | + * is there. The client can then treat a string with no map entry as a | |
| 1978 | + * payload from somewhere else, not a dependency this side dropped. | |
| 1979 | + * | |
| 1980 | + * Runs on the finished payload, so every builder, and every filter on | |
| 1981 | + * a builder's output, still sees the full shape. | |
| 1982 | + * | |
| 1983 | + * @param array $payload Payload whose top-level values are entry lists. | |
| 1984 | + * @param array $map Handle => dependency payload, filled in place. | |
| 1985 | + * @return array The payload with each entry's `scriptDeps` reduced to handles. | |
| 1986 | + */ | |
| 1987 | +function openstation_compact_script_deps( $payload, array &$map ) { | |
| 1988 | + if ( ! is_array( $payload ) ) { | |
| 1989 | + return $payload; | |
| 1990 | + } | |
| 1991 | + foreach ( $payload as $list_key => $entries ) { | |
| 1992 | + if ( ! is_array( $entries ) ) { | |
| 1993 | + continue; | |
| 1994 | + } | |
| 1995 | + foreach ( $entries as $entry_key => $entry ) { | |
| 1996 | + if ( ! is_array( $entry ) || ! isset( $entry['scriptDeps'] ) || ! is_array( $entry['scriptDeps'] ) ) { | |
| 1997 | + continue; | |
| 1998 | + } | |
| 1999 | + $payload[ $list_key ][ $entry_key ]['scriptDeps'] = openstation_compact_script_dep_list( $entry['scriptDeps'], $map ); | |
| 2000 | + } | |
| 2001 | + } | |
| 2002 | + return $payload; | |
| 2003 | +} | |
| 2004 | + | |
| 2005 | +/** | |
| 2006 | + * One entry's `scriptDeps` list, reduced to handles. | |
| 2007 | + * | |
| 2008 | + * @param array $deps Dependency payloads and/or bare handles. | |
| 2009 | + * @param array $map Handle => dependency payload, filled in place. | |
| 2010 | + * @return array Handles, in order; anything unkeyable passes through. | |
| 2011 | + */ | |
| 2012 | +function openstation_compact_script_dep_list( array $deps, array &$map ) { | |
| 2013 | + $handles = array(); | |
| 2014 | + foreach ( $deps as $dep ) { | |
| 2015 | + if ( is_string( $dep ) && '' !== $dep ) { | |
| 2016 | + if ( ! isset( $map[ $dep ] ) ) { | |
| 2017 | + $payload = openstation_resolve_script_payload( $dep ); | |
| 2018 | + if ( '' === $payload['url'] | |
| 2019 | + && empty( $payload['before'] ) | |
| 2020 | + && empty( $payload['after'] ) | |
| 2021 | + && empty( $payload['l10n'] ) ) { | |
| 2022 | + continue; | |
| 2023 | + } | |
| 2024 | + $payload['handle'] = $dep; | |
| 2025 | + $map[ $dep ] = $payload; | |
| 2026 | + } | |
| 2027 | + $handles[] = $dep; | |
| 2028 | + continue; | |
| 2029 | + } | |
| 2030 | + if ( is_array( $dep ) && isset( $dep['handle'] ) && '' !== (string) $dep['handle'] ) { | |
| 2031 | + $handle = (string) $dep['handle']; | |
| 2032 | + if ( ! isset( $map[ $handle ] ) ) { | |
| 2033 | + $map[ $handle ] = $dep; | |
| 2034 | + } | |
| 2035 | + $handles[] = $handle; | |
| 2036 | + continue; | |
| 2037 | + } | |
| 2038 | + // A handle-less payload has nothing to key it by. | |
| 2039 | + $handles[] = $dep; | |
| 2040 | + } | |
| 2041 | + return $handles; | |
| 2042 | +} | |
| 2043 | + | |
| 2044 | +/** | |
| 1903 | 2045 | * Resolve a registered WP script handle into the full payload the |
| 1904 | 2046 | * shell needs to lazy-load it without going through `wp_print_scripts()`. |
| 1905 | 2047 | * |
| 1906 | 2048 | * Returns: |
| @@ -2633,8 +2775,9 @@ | ||
| 2633 | 2775 | 'styleHandle' => $style_handle, |
| 2634 | 2776 | 'styleInline' => $style_payload['inline'], |
| 2635 | 2777 | 'companionStyles' => $companion_styles, |
| 2636 | 2778 | 'tabs' => $tab_descriptors, |
| 2779 | + 'menuPages' => isset( $entry['menu_pages'] ) ? array_values( (array) $entry['menu_pages'] ) : array(), | |
| 2637 | 2780 | ); |
| 2638 | 2781 | } |
| 2639 | 2782 | |
| 2640 | 2783 | // Append each handle's synthesized config set to its map entry — |