PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
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 / components.php

components.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at includes/components.php

381 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — PHP helpers for plugin authors.
4 *
5 * Two companion helpers live here:
6 *
7 * - {@see desktop_mode_component()} prints a `<wpd-*>` tag with
8 * safely-escaped attributes (plus its style-array
9 * serializers). The intent is explicit (we're rendering a kit
10 * component, not arbitrary HTML) and the escape discipline is
11 * automatic.
12 *
13 * - {@see desktop_mode_enqueue_script()} wraps
14 * `wp_enqueue_script()` with the `desktop-mode` + `wp-hooks`
15 * dependencies pre-wired, so shell-extending scripts always
16 * load after `wp.desktop.*` and `wp.hooks` are available.
17 *
18 * {@see desktop_mode_register_window()} moved to
19 * `includes/registries/native-windows.php` in 0.8.1.
20 *
21 * @package WPDesktopMode
22 * @since 0.5.0
23 */
24
25 defined( 'ABSPATH' ) || exit;
26
27 /**
28 * Output a `<wpd-*>` component with safely escaped attributes.
29 *
30 * ```php
31 * desktop_mode_component( 'wpd-button', array(
32 * 'variant' => 'primary',
33 * 'data-op' => 'add',
34 * 'aria-label' => __( 'Add', 'my-plugin' ),
35 * ), '+' );
36 * ```
37 *
38 * Attribute values flow through `esc_attr()` — no HTML injection
39 * surface. Content is passed through verbatim; callers that want
40 * to render user text should pre-escape with `esc_html()` /
41 * `wp_kses()` themselves.
42 *
43 * Boolean-style attributes (present with a `true` value or an
44 * empty string) render as bare attributes (`disabled`,
45 * `fill-cell`) — matches the HTML5 boolean-attribute convention
46 * every `<wpd-*>` follows.
47 *
48 * ## Inline styles
49 *
50 * The `style` key accepts either the usual string value or an
51 * associative array of CSS-property → value pairs. The array form
52 * auto-serializes to a CSS declaration list and auto-units bare
53 * integers on length-shaped properties (padding, margin, width,
54 * …) so `'padding' => 0` produces `padding: 0` and
55 * `'padding' => 16` produces `padding: 16px`.
56 *
57 * ```php
58 * desktop_mode_component( 'wpd-stack', array(
59 * 'gap' => 12,
60 * 'style' => array(
61 * 'padding' => 0,
62 * 'background' => 'rgba(0,0,0,0.04)',
63 * 'border-radius' => 8,
64 * ),
65 * ), $children );
66 * // <wpd-stack gap="12" style="padding: 0; background: rgba(0,0,0,0.04); border-radius: 8px">
67 * ```
68 *
69 * Plain string form (for one-line overrides) keeps working:
70 *
71 * ```php
72 * desktop_mode_component( 'wpd-stack', array(
73 * 'style' => 'padding: 0; margin-top: 16px',
74 * ), $children );
75 * ```
76 *
77 * @since 0.5.0
78 * @since 0.5.0 `style` accepts an array of CSS declarations.
79 *
80 * @param string $tag Tag name, e.g. `wpd-button`.
81 * Whitelisted to the `wpd-*` prefix
82 * to prevent the helper being
83 * misused as a generic HTML emitter.
84 * @param array<string,mixed> $attrs Attribute key/value pairs.
85 * `style` may be a string or an
86 * associative array (see above).
87 * @param string $content Inner HTML. Pass pre-escaped.
88 */
89 function desktop_mode_component( $tag, $attrs = array(), $content = '' ) {
90 $tag = strtolower( (string) $tag );
91 if ( ! preg_match( '/^wpd-[a-z][a-z0-9-]*$/', $tag ) ) {
92 // Fail loud in debug so a typo surfaces immediately; silently
93 // drop the output in production so a plugin with a bad tag
94 // doesn't blow up the page.
95 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
96 _doing_it_wrong(
97 __FUNCTION__,
98 sprintf(
99 /* translators: %s: the attempted tag name. */
100 esc_html__( 'desktop_mode_component() only accepts tags with the wpd- prefix; got "%s".', 'desktop-mode' ),
101 esc_html( $tag )
102 ),
103 '0.5.0'
104 );
105 }
106 return;
107 }
108
109 $attr_parts = array();
110 foreach ( (array) $attrs as $key => $value ) {
111 $key = (string) $key;
112 if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9_:.-]*$/', $key ) ) {
113 // Silently skip attribute names that don't match the HTML5
114 // name grammar. Same debug-vs-production split as the tag.
115 continue;
116 }
117 if ( false === $value || null === $value ) {
118 continue;
119 }
120 // Style array — serialize to a CSS declaration list. Plain
121 // string values fall through to the generic attribute path
122 // below so `'style' => 'padding:0'` keeps working.
123 if ( 'style' === strtolower( $key ) && is_array( $value ) ) {
124 $serialized = desktop_mode_serialize_style_array( $value );
125 if ( '' === $serialized ) {
126 continue;
127 }
128 $attr_parts[] = sprintf(
129 'style="%s"',
130 esc_attr( $serialized )
131 );
132 continue;
133 }
134 if ( true === $value || '' === $value ) {
135 // Boolean attribute — render bare.
136 $attr_parts[] = esc_attr( $key );
137 continue;
138 }
139 if ( is_array( $value ) || is_object( $value ) ) {
140 // Wrong-shape value on a non-style key. Without this
141 // guard PHP's string cast would emit `key="Array"` /
142 // `key="Object"` — embarrassing in production, silent
143 // in debug. Surface it loudly under WP_DEBUG and drop
144 // the attribute everywhere else.
145 _doing_it_wrong(
146 __FUNCTION__,
147 sprintf(
148 /* translators: 1: attribute name, 2: tag name. */
149 esc_html__( 'Attribute "%1$s" on <%2$s> received a non-scalar value (array/object). Only the `style` attribute accepts an array; other attributes must be strings, booleans, or null. The attribute was skipped.', 'desktop-mode' ),
150 esc_html( $key ),
151 esc_html( $tag )
152 ),
153 '0.5.2'
154 );
155 continue;
156 }
157 $attr_parts[] = sprintf(
158 '%s="%s"',
159 esc_attr( $key ),
160 esc_attr( (string) $value )
161 );
162 }
163
164 $attr_str = $attr_parts ? ' ' . implode( ' ', $attr_parts ) : '';
165
166 printf(
167 '<%1$s%2$s>%3$s</%1$s>',
168 // `$tag` is validated above against the wpd- allowlist; safe.
169 $tag, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
170 // `$attr_str` is pre-escaped via esc_attr() for each component.
171 $attr_str, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
172 // `$content` is the caller's responsibility to pre-escape.
173 $content // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
174 );
175 }
176
177 /**
178 * CSS properties that treat bare integers as pixels. Mirrors
179 * the length-shaped property list used by plugin JS code when
180 * interpreting raw numeric values — keeping the same list in
181 * one place so PHP `'padding' => 16` and JS `padding: 16` make
182 * the same visual decision.
183 *
184 * @since 0.5.0
185 */
186 const DESKTOP_MODE_LENGTH_CSS_PROPERTIES = array(
187 'width', 'height',
188 'min-width', 'min-height', 'max-width', 'max-height',
189 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
190 'padding-inline', 'padding-inline-start', 'padding-inline-end',
191 'padding-block', 'padding-block-start', 'padding-block-end',
192 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
193 'margin-inline', 'margin-inline-start', 'margin-inline-end',
194 'margin-block', 'margin-block-start', 'margin-block-end',
195 'gap', 'row-gap', 'column-gap',
196 'border-width', 'border-top-width', 'border-right-width',
197 'border-bottom-width', 'border-left-width',
198 'border-radius',
199 'border-top-left-radius', 'border-top-right-radius',
200 'border-bottom-left-radius', 'border-bottom-right-radius',
201 'top', 'right', 'bottom', 'left',
202 'inset',
203 'inset-inline-start', 'inset-inline-end',
204 'inset-block-start', 'inset-block-end',
205 'font-size', 'letter-spacing', 'word-spacing', 'text-indent',
206 'outline-width', 'outline-offset',
207 );
208
209 /**
210 * Serialize an associative array of CSS declarations into a
211 * `prop: value; prop: value` string for the `style` attribute.
212 *
213 * Property names are validated as CSS-shaped (kebab-case letters,
214 * digits, hyphens); malformed names are silently dropped. Bare
215 * integer values on length-shaped properties auto-unit to `px`
216 * so callers can write `'padding' => 16` without remembering the
217 * unit. The literal `0` is left unit-less because CSS treats it
218 * as dimensionally valid on any property.
219 *
220 * @since 0.5.0
221 *
222 * @param array<string,mixed> $styles
223 * @return string CSS declaration list, or empty string when no
224 * valid declarations were produced.
225 */
226 function desktop_mode_serialize_style_array( $styles ) {
227 if ( ! is_array( $styles ) ) {
228 return '';
229 }
230 $parts = array();
231 foreach ( $styles as $prop => $value ) {
232 $prop = strtolower( trim( (string) $prop ) );
233 if ( ! preg_match( '/^-?[a-z][a-z0-9-]*$/', $prop ) ) {
234 continue;
235 }
236 if ( false === $value || null === $value ) {
237 continue;
238 }
239 $serialized = desktop_mode_format_css_value( $prop, $value );
240 if ( '' === $serialized ) {
241 continue;
242 }
243 $parts[] = $prop . ': ' . $serialized;
244 }
245 return implode( '; ', $parts );
246 }
247
248 /**
249 * Serialize a raw PHP value into a CSS declaration value.
250 *
251 * Handles the two conveniences callers want from an ergonomic
252 * style array:
253 *
254 * - Integer + length-shaped property → append `px`
255 * (`'padding' => 16` → `16px`).
256 * - Integer `0` → keep unit-less (`0` is valid everywhere).
257 *
258 * Everything else (strings, floats already unitted, calc(…)
259 * expressions, color keywords) passes through verbatim.
260 *
261 * @since 0.5.0
262 *
263 * @param string $property CSS property name.
264 * @param mixed $value Raw value (int, float, string).
265 * @return string CSS value, or empty string when $value is
266 * not serializable.
267 */
268 function desktop_mode_format_css_value( $property, $value ) {
269 if ( is_bool( $value ) || null === $value ) {
270 return '';
271 }
272 $text = trim( (string) $value );
273 if ( '' === $text ) {
274 return '';
275 }
276 if ( preg_match( '/^-?\d+(\.\d+)?$/', $text ) ) {
277 if ( '0' === $text ) {
278 return '0';
279 }
280 if ( in_array( $property, DESKTOP_MODE_LENGTH_CSS_PROPERTIES, true ) ) {
281 return $text . 'px';
282 }
283 }
284 return $text;
285 }
286
287
288 // Native-windows registry (register_window, allowed_html,
289 // template-html builder, enqueue + render hooks) was moved to
290 // `includes/registries/native-windows.php` in 0.8.1.
291
292
293
294 // Widgets registry was moved to
295 // `includes/registries/widgets.php` in 0.8.1.
296
297
298
299 // Wallpapers registry was moved to
300 // `includes/registries/wallpapers.php` in 0.8.1.
301
302
303 // Desktop-icons registry was moved to
304 // `includes/registries/icons.php` in 0.8.1.
305
306
307
308 // Native-window tabs registry was moved to
309 // `includes/registries/window-tabs.php` in 0.8.1.
310
311
312 /**
313 * Enqueue a plugin script that extends the desktop shell.
314 *
315 * Thin wrapper around `wp_enqueue_script` that pre-wires the correct
316 * dependencies so the script:
317 *
318 * - Runs AFTER `desktop-mode` (the shell bundle) so `wp.desktop.*` is
319 * guaranteed available.
320 * - Runs AFTER `wp-hooks` so `wp.hooks.addAction( 'desktop-mode.init', ... )`
321 * works without the plugin author having to remember that dep.
322 *
323 * Intended to be called from `admin_enqueue_scripts` — the wrapper
324 * itself does not add an `is_admin()` guard.
325 *
326 * Drop-in replacement for the boilerplate:
327 *
328 * ```php
329 * add_action( 'admin_enqueue_scripts', function () {
330 * wp_enqueue_script(
331 * 'my-plugin',
332 * plugins_url( 'my-plugin.js', __FILE__ ),
333 * array( 'desktop-mode', 'wp-hooks' ),
334 * '1.0.0',
335 * true
336 * );
337 * } );
338 * ```
339 *
340 * which becomes:
341 *
342 * ```php
343 * add_action( 'admin_enqueue_scripts', function () {
344 * desktop_mode_enqueue_script(
345 * 'my-plugin',
346 * plugins_url( 'my-plugin.js', __FILE__ ),
347 * array(), // extra deps on top of the desktop defaults
348 * '1.0.0'
349 * );
350 * } );
351 * ```
352 *
353 * @since 0.5.0
354 *
355 * @param string $handle Script handle.
356 * @param string $src Full URL of the script, or path relative
357 * to the WordPress root directory.
358 * @param string[] $extra_deps Additional dependency handles. `desktop-mode`
359 * and `wp-hooks` are always prepended.
360 * @param string|bool|null $version Version string, or `false` for none.
361 * Defaults to `DESKTOP_MODE_VERSION` so plugin authors
362 * don't have to busy-track cache busting.
363 * @param bool $in_footer Whether to enqueue in the footer. Defaults
364 * to `true` — the shell is always in head.
365 * @return void
366 */
367 function desktop_mode_enqueue_script( $handle, $src, $extra_deps = array(), $version = null, $in_footer = true ) {
368 $deps = array_merge(
369 array( 'desktop-mode', 'wp-hooks' ),
370 is_array( $extra_deps ) ? $extra_deps : array()
371 );
372
373 wp_enqueue_script(
374 $handle,
375 $src,
376 $deps,
377 null === $version ? DESKTOP_MODE_VERSION : $version,
378 $in_footer
379 );
380 }
381