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

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