PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
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 / registries / window-tabs.php

window-tabs.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.0, at includes/registries/window-tabs.php

330 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native-window tabs registry.
4 *
5 * Multi-tab native windows are a sister-to-the-window
6 * registration: a tab is owned by some window id and its content
7 * is wrapped in `<wpd-tabpanel>` automatically by the shell. The
8 * MAIN_TAB constant reserves the `'main'` value for the window's
9 * own `template` callback so plugins can't accidentally collide
10 * with the built-in main pane.
11 *
12 * Extracted from `components.php` during the architecture-0.8.1
13 * PHP slicing (phase 6).
14 *
15 * @package Desktop_Mode
16 * @since 0.8.1
17 */
18
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Reserved tab value for the window's own `template` output. The
23 * main tab always renders first, its markup comes from the window
24 * registration's `template` callback, and its label is the
25 * window's `main_tab_label` (falling back to the window `title`).
26 *
27 * Plugins cannot register an additional tab with this value —
28 * {@see desktop_mode_register_window_tab()} returns
29 * `desktop_mode_reserved_tab_value` when they try.
30 *
31 * @since 0.11.0
32 */
33 const DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB = 'main';
34
35 /**
36 * Register an additional tab on an existing native window.
37 *
38 * Mirrors the legacy iframe-window ergonomics where submenus
39 * auto-become tabs below the title bar: the window's own
40 * `template` renders as the first tab (labelled by `main_tab_label`
41 * / `title`), and every call to this function adds another tab
42 * alongside it. Cross-plugin extension is supported — a companion
43 * plugin can attach a tab to someone else's window.
44 *
45 * Registering even a single tab turns on the auto-wrap path in
46 * `desktop_mode_build_native_window_template_html()`: the shell wraps the
47 * window body in `<wpd-stack>` + `<wpd-tabs>` + `<wpd-tabpanel>`
48 * elements automatically. Plugin authors no longer hand-write that
49 * markup — the shell provides it and `<wpd-tabpanel>` auto-swap
50 * (from 0.11) handles visibility.
51 *
52 * ```php
53 * // Plugin that owns the window declares its own tabs:
54 * desktop_mode_register_window( 'jorvy', array(
55 * 'title' => 'Jorvy',
56 * 'main_tab_label' => 'Quotes',
57 * 'template' => function () { echo '<p class="quote"></p>'; },
58 * 'script' => 'jorvy-main',
59 * ) );
60 * desktop_mode_register_window_tab( 'jorvy', array(
61 * 'value' => 'about',
62 * 'label' => 'About',
63 * 'template' => function () { echo '<p>Marvel quotes, rotated every 10s.</p>'; },
64 * ) );
65 *
66 * // A companion plugin attaches a tab to someone else's window:
67 * desktop_mode_register_window_tab( 'jorvy', array(
68 * 'value' => 'stats',
69 * 'label' => 'Stats',
70 * 'template' => 'jorvy_stats_pane',
71 * 'script' => 'jorvy-stats',
72 * ) );
73 * ```
74 *
75 * @since 0.11.0
76 *
77 * @param string $window_id Id of the native window this tab belongs to.
78 * @param array $args {
79 * @type string $value Tab id (unique within the window).
80 * Required. Cannot equal the reserved
81 * value `main` — that's the window's
82 * own template tab.
83 * @type string $label Display label on the tab strip. Required.
84 * @type callable $template Callback that echoes the tab's
85 * pane HTML. Wrapped in
86 * `<wpd-tabpanel for="<value>">` by
87 * the shell. Required.
88 * @type string $script Optional script handle enqueued
89 * when the window is active — useful
90 * when a tab needs its own JS module
91 * without bloating the main window
92 * script. Default empty.
93 * @type int $position Sort order among tabs on this
94 * window; lower renders earlier.
95 * Default 100.
96 * @type string[] $capabilities Gate: ALL caps must match. Any
97 * missed cap returns
98 * `WP_Error desktop_mode_capability_denied`.
99 * }
100 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
101 */
102 function desktop_mode_register_window_tab( $window_id, $args = array() ) {
103 $window_id = sanitize_key( (string) $window_id );
104 if ( '' === $window_id ) {
105 return desktop_mode_registration_error(
106 'desktop_mode_missing_window_id',
107 __( 'Window id is required when registering a tab.', 'desktop-mode' )
108 );
109 }
110
111 $defaults = array(
112 'value' => '',
113 'label' => '',
114 'template' => null,
115 'script' => '',
116 'position' => 100,
117 'capabilities' => array(),
118 );
119 $args = wp_parse_args( $args, $defaults );
120
121 foreach ( (array) $args['capabilities'] as $cap ) {
122 if ( ! current_user_can( (string) $cap ) ) {
123 return desktop_mode_registration_error(
124 'desktop_mode_capability_denied',
125 sprintf(
126 /* translators: %s: capability slug. */
127 __( 'Current user lacks the %s capability required to register this window tab.', 'desktop-mode' ),
128 (string) $cap
129 ),
130 array( 'capability' => (string) $cap, 'window_id' => $window_id )
131 );
132 }
133 }
134
135 // Tab values accept both flat slugs ('convert') and a single
136 // `vendor/sub-id` namespace ('plugin/convert') so two plugins
137 // targeting the same window can ship same-named tabs without
138 // stomping each other in the registry. The downstream uses
139 // (`wpd-tabpanel[for="…"]`, `<wpd-tab value="…">`) all pass the
140 // value through esc_attr and use it as an attribute selector,
141 // which tolerates the slash.
142 $value_raw = strtolower( trim( (string) $args['value'] ) );
143 if ( '' === $value_raw ) {
144 return desktop_mode_registration_error(
145 'desktop_mode_missing_tab_value',
146 __( 'Window tab registration requires a non-empty `value`.', 'desktop-mode' ),
147 array( 'window_id' => $window_id )
148 );
149 }
150 if ( ! preg_match( '/^[a-z0-9_-]+(\/[a-z0-9_-]+)?$/', $value_raw ) ) {
151 return desktop_mode_registration_error(
152 'desktop_mode_invalid_tab_value',
153 sprintf(
154 /* translators: %s: the invalid value. */
155 __( 'Window tab `value` "%s" must match /^[a-z0-9_-]+(\/[a-z0-9_-]+)?$/ — lowercase alphanum + hyphen/underscore, with at most one `vendor/sub-id` slash.', 'desktop-mode' ),
156 $value_raw
157 ),
158 array( 'window_id' => $window_id, 'value' => $value_raw )
159 );
160 }
161 $value = $value_raw;
162 if ( DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB === $value ) {
163 return desktop_mode_registration_error(
164 'desktop_mode_reserved_tab_value',
165 sprintf(
166 /* translators: %s: the reserved value. */
167 __( 'The tab value "%s" is reserved for the window\'s own template tab.', 'desktop-mode' ),
168 DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB
169 ),
170 array( 'window_id' => $window_id, 'value' => $value )
171 );
172 }
173 if ( '' === (string) $args['label'] ) {
174 return desktop_mode_registration_error(
175 'desktop_mode_missing_label',
176 __( 'Window tab registration requires a non-empty `label`.', 'desktop-mode' ),
177 array( 'window_id' => $window_id )
178 );
179 }
180 if ( ! is_callable( $args['template'] ) ) {
181 return desktop_mode_registration_error(
182 'desktop_mode_invalid_template',
183 __( 'Window tab registration requires a callable `template` that echoes the pane body.', 'desktop-mode' ),
184 array( 'window_id' => $window_id )
185 );
186 }
187
188 $entry = array(
189 'value' => $value,
190 'label' => (string) $args['label'],
191 'template' => $args['template'],
192 'script' => (string) $args['script'],
193 'position' => (int) $args['position'],
194 );
195 desktop_mode_desktop_window_tab_registry( $window_id, $value, $entry );
196
197 /**
198 * Fires after a native window tab is successfully registered.
199 *
200 * Does NOT fire when `desktop_mode_register_window_tab()` returns
201 * a `WP_Error`.
202 *
203 * @since 0.11.0
204 *
205 * @param string $window_id The window this tab belongs to.
206 * @param string $value The tab value.
207 * @param array $entry The stored registry entry.
208 */
209 do_action( 'desktop_mode_window_tab_registered', $window_id, $value, $entry );
210
211 return true;
212 }
213
214 /**
215 * Internal nested registry for native-window tabs keyed by
216 * `[window_id][value]`. Pass `$entry = null` and any non-empty
217 * `$value` to read a single entry; pass both `$window_id` and
218 * `$value` empty to get the full registry.
219 *
220 * @since 0.11.0
221 * @internal
222 *
223 * @param string $window_id Window id (or '' to read everything).
224 * @param string $value Tab value (or '' to read every tab
225 * on the given window).
226 * @param array|null $entry Entry to store, or null to just read.
227 * @return array|null
228 */
229 function desktop_mode_desktop_window_tab_registry( $window_id = '', $value = '', $entry = null ) {
230 static $store = array();
231
232 if ( '' === (string) $window_id ) {
233 return $store;
234 }
235 if ( ! isset( $store[ $window_id ] ) ) {
236 $store[ $window_id ] = array();
237 }
238 if ( '' === (string) $value ) {
239 return $store[ $window_id ];
240 }
241 if ( null !== $entry ) {
242 $store[ $window_id ][ $value ] = $entry;
243 }
244 return isset( $store[ $window_id ][ $value ] )
245 ? $store[ $window_id ][ $value ]
246 : null;
247 }
248
249 /**
250 * Return the ordered list of tab descriptors for a window. The
251 * main tab (reserved value `main`) is always first; additional
252 * tabs follow in `position` order (ties broken by registration
253 * order).
254 *
255 * Shape per entry: `{ value, label, template, script, is_main, position }`.
256 *
257 * Filterable via `desktop_mode_window_tabs` so a late-loading plugin
258 * can reorder, hide, or relabel tabs another plugin registered —
259 * mirrors the `desktop_mode_wallpapers` filter discipline.
260 *
261 * @since 0.11.0
262 *
263 * @param string $window_id Window id.
264 * @return array[]
265 */
266 function desktop_mode_get_native_window_tabs( $window_id ) {
267 $window = desktop_mode_native_window_registry( (string) $window_id );
268 if ( ! is_array( $window ) ) {
269 return array();
270 }
271
272 $extras = desktop_mode_desktop_window_tab_registry( $window_id );
273 if ( ! is_array( $extras ) ) {
274 $extras = array();
275 }
276
277 // Main tab first — label falls back to the window title when no
278 // `main_tab_label` was set during registration.
279 $main_label = '' !== (string) $window['main_tab_label']
280 ? (string) $window['main_tab_label']
281 : (string) $window['title'];
282 $tabs = array(
283 array(
284 'value' => DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB,
285 'label' => $main_label,
286 'template' => $window['template'],
287 'script' => '',
288 'is_main' => true,
289 'position' => 0,
290 ),
291 );
292
293 // Additional tabs sorted by position. Values are trusted — they
294 // come from sanitize_key() at registration time.
295 $sorted = array_values( $extras );
296 usort( $sorted, static function ( $a, $b ) {
297 if ( $a['position'] === $b['position'] ) {
298 return 0;
299 }
300 return $a['position'] < $b['position'] ? -1 : 1;
301 } );
302 foreach ( $sorted as $tab ) {
303 $tabs[] = array(
304 'value' => $tab['value'],
305 'label' => $tab['label'],
306 'template' => $tab['template'],
307 'script' => $tab['script'],
308 'is_main' => false,
309 'position' => $tab['position'],
310 );
311 }
312
313 /**
314 * Filters the full ordered tab list for a native window right
315 * before the shell renders it. Return a reshaped array to
316 * reorder, hide, or rename tabs — same shape as the input.
317 *
318 * The main tab's `template` is the window's own template
319 * callback; replacing it at filter time is supported but
320 * unusual — prefer updating the window registration itself.
321 *
322 * @since 0.11.0
323 *
324 * @param array[] $tabs Ordered tab descriptors.
325 * @param string $window_id Window id.
326 */
327 $filtered = apply_filters( 'desktop_mode_window_tabs', $tabs, $window_id );
328 return is_array( $filtered ) ? $filtered : $tabs;
329 }
330