PluginProbe
Booking Calendar / 11.6.1
Booking Calendar v11.6.1
11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 10.11.2 All 203 releases
booking / includes / page-form-builder / form-settings / options-render.php

options-render.php in Booking Calendar 11.6.1, at includes/page-form-builder/form-settings/options-render.php

801 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BFB Settings Options: Simple option row printer.
4 *
5 * Conception (simple):
6 * - scope=global -> may show per-row Save UI (save_ui: always|when_changed)
7 * - scope=form -> saved only via "Save Form" (settings_json: {"options":{key:value}})
8 * - scope=ui -> UI-only (no DB save)
9 *
10 * Args shape:
11 * array(
12 * 'type' => 'toggle'|'select'|'radio'|'length'|'spacing'|'range'|'color'|'input'|'textarea',
13 * 'key' => 'booking_timeslot_picker',
14 * 'scope' => 'global'|'form'|'ui',
15 * 'save_ui' => 'always'|'when_changed', // only for scope=global
16 * 'default' => 'On', // initial value for control
17 * 'label' => __( '...', 'booking' ),
18 * 'help' => __( '...', 'booking' ),
19 * 'attr' => array( 'id' => '...', 'class' => '...' ),
20 *
21 * // select/radio
22 * 'options' => array( 'value' => 'Label', ... ),
23 * 'radio_layout' => 'inline'|'stack',
24 *
25 * // input
26 * 'input_type' => 'text'|'number'|...,
27 * 'input_attrs' => array( 'placeholder' => '...', ... ),
28 *
29 * // length
30 * 'length' => array(
31 * 'default_unit' => '%',
32 * 'units' => array( '%' => '%', 'px' => 'px', ... ),
33 * 'bounds_map' => array( '%' => array('min'=>30,'max'=>100,'step'=>1), ... ), // optional
34 * ),
35 *
36 * // range (slider number)
37 * 'range' => array(
38 * 'min' => 0,
39 * 'max' => 100,
40 * 'step' => 1,
41 * ),
42 * )
43 *
44 * @package Booking Calendar
45 * @since 11.0.x
46 * @file ../includes/page-form-builder/form-settings/options-render.php
47 */
48
49 if ( ! defined( 'ABSPATH' ) ) {
50 exit;
51 }
52
53 class WPBC_BFB_Setting_Options {
54
55 /**
56 * Print one option row.
57 *
58 * @param array $args
59 */
60 public static function print_option( $args ) {
61
62 if ( ! is_array( $args ) ) {
63 return;
64 }
65
66 $type = isset( $args['type'] ) ? (string) $args['type'] : 'input';
67 $key = isset( $args['key'] ) ? (string) $args['key'] : '';
68 $scope = isset( $args['scope'] ) ? (string) $args['scope'] : 'ui';
69 $save_ui = isset( $args['save_ui'] ) ? (string) $args['save_ui'] : 'when_changed';
70
71 if ( '' === $key ) {
72 return;
73 }
74
75 $label = isset( $args['label'] ) ? (string) $args['label'] : '';
76 $help = isset( $args['help'] ) ? (string) $args['help'] : '';
77
78 $attr = ( isset( $args['attr'] ) && is_array( $args['attr'] ) ) ? $args['attr'] : array();
79 $id = isset( $attr['id'] ) ? (string) $attr['id'] : 'wpbc_bfb_setting__' . sanitize_html_class( $key );
80
81 $default_value = isset( $args['default'] ) ? $args['default'] : '';
82
83 // Wrapper classes.
84 $row_class = 'wpbc_bfb__form_setting wpbc-setting';
85 if ( ! empty( $args['row_class'] ) && is_scalar( $args['row_class'] ) ) {
86 $row_extra_classes = preg_split( '/\s+/', trim( (string) $args['row_class'] ) );
87 foreach ( $row_extra_classes as $row_extra_class ) {
88 $row_extra_class = sanitize_html_class( $row_extra_class );
89 if ( '' !== $row_extra_class ) {
90 $row_class .= ' ' . $row_extra_class;
91 }
92 }
93 }
94
95 $row_attr = ( isset( $args['row_attr'] ) && is_array( $args['row_attr'] ) ) ? $args['row_attr'] : array();
96 if ( ! empty( $args['row_hidden'] ) ) {
97 $row_attr['hidden'] = 'hidden';
98 $row_attr['aria-hidden'] = 'true';
99 }
100 $row_attr_str = self::build_attr_string( $row_attr );
101 ?>
102 <div class="<?php echo esc_attr( $row_class ); ?>"
103 data-scope="<?php echo esc_attr( $scope ); ?>"
104 data-key="<?php echo esc_attr( $key ); ?>"
105 data-type="<?php echo esc_attr( $type ); ?>"
106 <?php echo $row_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
107 <div class="inspector__row" style="justify-content:space-between;">
108 <div class="inspector__control" style="flex:1 1 auto;">
109
110 <?php self::print_control( $args, $id, $key, $scope, $type, $default_value ); ?>
111
112 <?php if ( '' !== $help ) : ?>
113 <p class="wpbc_bfb__help" style="margin:6px 0 0 0;"><?php echo esc_html( $help ); ?></p>
114 <?php endif; ?>
115
116 </div>
117
118 <?php
119 // Global scope only: per-row Save UI.
120 if ( 'global' === $scope ) {
121 self::print_save_ui( $args, $id, $key, $type, $save_ui );
122 }
123 ?>
124 </div>
125 </div>
126 <?php
127 }
128
129 // =================================================================================================
130 // Controls
131 // =================================================================================================
132
133 /**
134 * Print control (toggle/select/radio/length/input/textarea).
135 *
136 * @param array $args
137 * @param string $id
138 * @param string $key
139 * @param string $scope
140 * @param string $type
141 * @param mixed $value
142 */
143 private static function print_control( $args, $id, $key, $scope, $type, $value ) {
144
145 $label = isset( $args['label'] ) ? (string) $args['label'] : '';
146
147 $attr = ( isset( $args['attr'] ) && is_array( $args['attr'] ) ) ? $args['attr'] : array();
148 $attr['id'] = $id;
149
150 // Extract/normalize class.
151 $css_class = '';
152 if ( isset( $attr['class'] ) ) {
153 $css_class = trim( (string) $attr['class'] );
154 unset( $attr['class'] );
155 }
156 $css_class = trim( 'wpbc-setting__control inspector__input ' . $css_class );
157
158 // Toggle.
159 if ( 'toggle' === $type ) {
160 self::print_toggle( $id, $key, $scope, $label, $value, $attr, $css_class );
161 return;
162 }
163
164 // Select.
165 if ( 'select' === $type ) {
166 $options = ( isset( $args['options'] ) && is_array( $args['options'] ) ) ? $args['options'] : array();
167 self::print_select( $id, $key, $scope, $label, $value, $attr, $css_class, $options );
168 return;
169 }
170
171 // Radio.
172 if ( 'radio' === $type ) {
173 $options = ( isset( $args['options'] ) && is_array( $args['options'] ) ) ? $args['options'] : array();
174 $radio_layout = isset( $args['radio_layout'] ) ? (string) $args['radio_layout'] : 'stack';
175 $radio_layout = in_array( $radio_layout, array( 'inline', 'choice_grid' ), true ) ? $radio_layout : 'stack';
176 self::print_radio( $id, $key, $scope, $label, $value, $options, $radio_layout );
177 return;
178 }
179
180 // Length.
181 if ( 'length' === $type ) {
182 $length = ( isset( $args['length'] ) && is_array( $args['length'] ) ) ? $args['length'] : array();
183 self::print_length( $id, $key, $scope, $label, $value, $length );
184 return;
185 }
186
187 // Spacing: two px number controls saved as CSS shorthand, e.g. "10px 30px".
188 if ( 'spacing' === $type ) {
189 $spacing = ( isset( $args['spacing'] ) && is_array( $args['spacing'] ) ) ? $args['spacing'] : array();
190 self::print_spacing( $id, $key, $scope, $label, $value, $spacing );
191 return;
192 }
193
194 // Range (slider number).
195 if ( 'range' === $type ) {
196 $range = ( isset( $args['range'] ) && is_array( $args['range'] ) ) ? $args['range'] : array();
197 self::print_range( $id, $key, $scope, $label, $value, $range, $attr, $css_class );
198 return;
199 }
200
201 // Color picker (Coloris).
202 if ( 'color' === $type ) {
203 $input_attrs = ( isset( $args['input_attrs'] ) && is_array( $args['input_attrs'] ) ) ? $args['input_attrs'] : array();
204 self::print_color( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs );
205 return;
206 }
207
208 // Textarea.
209 if ( 'textarea' === $type ) {
210 $input_attrs = ( isset( $args['input_attrs'] ) && is_array( $args['input_attrs'] ) ) ? $args['input_attrs'] : array();
211 self::print_textarea( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs );
212 return;
213 }
214
215 // Input (default).
216 $input_type = isset( $args['input_type'] ) ? (string) $args['input_type'] : 'text';
217 $input_attrs = ( isset( $args['input_attrs'] ) && is_array( $args['input_attrs'] ) ) ? $args['input_attrs'] : array();
218 self::print_input( $id, $key, $scope, $label, $value, $attr, $css_class, $input_type, $input_attrs );
219 }
220
221 private static function print_toggle( $id, $key, $scope, $label, $value, $attr, $css_class ) {
222
223 $is_on = self::is_on( $value );
224 $initial = $is_on ? 'On' : 'Off';
225
226 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'toggle', $initial );
227 $control_attr['data-wpbc-bfb-fs-type'] = 'toggle';
228
229 $attr_str = self::build_attr_string( $control_attr );
230 ?>
231 <div class="wpbc_ui__toggle">
232 <input type="checkbox"
233 class="<?php echo esc_attr( $css_class ); ?>"
234 value="On"
235 aria-checked="<?php echo $is_on ? 'true' : 'false'; ?>"
236 <?php checked( $is_on ); ?>
237 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
238 />
239 <label class="wpbc_ui__toggle_icon" for="<?php echo esc_attr( $id ); ?>"></label>
240 <label class="wpbc_ui__toggle_label" for="<?php echo esc_attr( $id ); ?>"><strong><?php echo esc_html( $label ); ?></strong></label>
241 </div>
242 <?php
243 }
244
245 private static function print_select( $id, $key, $scope, $label, $value, $attr, $css_class, $options ) {
246
247 $v = is_scalar( $value ) ? (string) $value : '';
248
249 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'select', $v );
250 $control_attr['data-wpbc-bfb-fs-type'] = 'select';
251
252 $attr_str = self::build_attr_string( $control_attr );
253 ?>
254 <label style="display:block;">
255 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
256 <select class="<?php echo esc_attr( $css_class ); ?>"
257 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
258 <?php foreach ( $options as $opt_value => $opt_label ) : ?>
259 <option value="<?php echo esc_attr( (string) $opt_value ); ?>" <?php selected( (string) $opt_value, $v ); ?>>
260 <?php echo esc_html( (string) $opt_label ); ?>
261 </option>
262 <?php endforeach; ?>
263 </select>
264 </label>
265 <?php
266 }
267
268 private static function print_radio( $id, $key, $scope, $label, $value, $options, $radio_layout ) {
269
270 $v = is_scalar( $value ) ? (string) $value : '';
271
272 $is_choice_grid = ( 'choice_grid' === $radio_layout );
273 $wrapper_class = $is_choice_grid ? 'wpbc_theme_choice_grid wpbc_bfb_style_choice_grid' : '';
274 $wrapper_style = ( 'inline' === $radio_layout )
275 ? 'display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:0;'
276 : 'margin:0;';
277 $item_style = ( 'inline' === $radio_layout )
278 ? ''
279 : 'display:flex;align-items:center;gap:8px;margin:4px 0;';
280 if ( $is_choice_grid ) {
281 $wrapper_style = '';
282 $item_style = '';
283 }
284
285 // Put fs data on wrapper (JS reads control id from wrapper).
286 $wrap_attr = array(
287 'data-wpbc-bfb-fs-key' => $key,
288 'data-wpbc-bfb-fs-scope' => $scope,
289 'data-wpbc-bfb-fs-type' => 'radio',
290 'data-wpbc-bfb-fs-initial' => $v,
291 'data-wpbc-bfb-fs-controlid' => $id,
292 );
293 $wrap_attr_str = self::build_attr_string( $wrap_attr );
294 ?>
295 <div class="wpbc_bfb__form_setting_radio"
296 <?php echo $wrap_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
297 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
298
299 <div class="<?php echo esc_attr( $wrapper_class ); ?>" style="<?php echo esc_attr( $wrapper_style ); ?>">
300 <?php
301 $i = 0;
302 foreach ( $options as $opt_value => $opt_label ) :
303 $i++;
304 $rid = $id . '__' . $i;
305 $item_class = $is_choice_grid ? 'wpbc_theme_choice' : '';
306 if ( $is_choice_grid && (string) $opt_value === $v ) {
307 $item_class .= ' is-selected';
308 }
309 ?>
310 <label class="<?php echo esc_attr( $item_class ); ?>" style="<?php echo esc_attr( $item_style ); ?>">
311 <input type="radio"
312 name="<?php echo esc_attr( $id ); ?>"
313 id="<?php echo esc_attr( $rid ); ?>"
314 value="<?php echo esc_attr( (string) $opt_value ); ?>"
315 data-wpbc-bfb-fs-radio="1"
316 <?php checked( (string) $opt_value, $v ); ?>
317 />
318 <?php if ( $is_choice_grid ) : ?>
319 <span class="wpbc_theme_choice_swatch wpbc_theme_choice_swatch_<?php echo esc_attr( sanitize_html_class( (string) $opt_value ) ); ?>"></span>
320 <span class="wpbc_theme_choice_label"><?php echo esc_html( (string) $opt_label ); ?></span>
321 <?php else : ?>
322 <?php echo esc_html( (string) $opt_label ); ?>
323 <?php endif; ?>
324 </label>
325 <?php endforeach; ?>
326 </div>
327 </div>
328 <?php
329 }
330
331 /**
332 * Print slider number control (range + number).
333 *
334 * IMPORTANT:
335 * - Uses only NEW wpbc_slider_* attributes (no back-compat).
336 * - Number input is the "writer" and carries FS markers + id (Save UI reads #id).
337 *
338 * Markup:
339 * <label for="ID" class="inspector__label">...</label>
340 * <div class="inspector__control">
341 * <div class="wpbc_slider_range_group wpbc_inline_inputs">
342 * <input type="range" data-wpbc_slider_range_range ...>
343 * <input type="number" data-wpbc_slider_range_value data-wpbc_slider_range_writer id="ID" ...>
344 * </div>
345 * </div>
346 */
347 private static function print_range( $id, $key, $scope, $label, $value, $range, $attr, $css_class ) {
348
349 $min = isset( $range['min'] ) ? (float) $range['min'] : 0;
350 $max = isset( $range['max'] ) ? (float) $range['max'] : 100;
351 $step = isset( $range['step'] ) ? (float) $range['step'] : 1;
352
353 $v = is_scalar( $value ) ? (string) $value : '';
354 if ( '' === trim( $v ) ) {
355 $v = (string) $min;
356 }
357
358 // Clamp numeric value.
359 if ( is_numeric( $v ) ) {
360 $n = (float) $v;
361 if ( $n < $min ) { $n = $min; }
362 if ( $n > $max ) { $n = $max; }
363 // Keep formatting simple.
364 $v = (string) $n;
365 }
366
367 // Number input = writer (id stays here, Save UI reads #id).
368 // Keep FS type as "input" for compatibility with existing apply/collect code.
369 $num_attr = self::with_common_fs_data( $attr, $key, $scope, 'input', $v );
370 $num_attr['data-wpbc-bfb-fs-type'] = 'input';
371 $num_attr['data-wpbc_slider_range_value'] = '1';
372 $num_attr['data-wpbc_slider_range_writer'] = '1';
373
374 // Ensure the number input keeps the $id.
375 $num_attr['id'] = $id;
376
377 $num_attr_str = self::build_attr_string( $num_attr );
378
379 // Number input CSS (adds width helper).
380 $num_class = trim( $css_class . ' inspector__w_30' );
381 ?>
382 <label for="<?php echo esc_attr( $id ); ?>" class="inspector__label"><strong><?php echo esc_html( $label ); ?></strong></label>
383
384 <div class="inspector__control">
385 <div class="wpbc_slider_range_group wpbc_inline_inputs">
386 <input type="range"
387 class="inspector__input"
388 data-wpbc_slider_range_range="1"
389 min="<?php echo esc_attr( (string) $min ); ?>"
390 max="<?php echo esc_attr( (string) $max ); ?>"
391 step="<?php echo esc_attr( (string) $step ); ?>"
392 value="<?php echo esc_attr( $v ); ?>"
393 />
394
395 <input type="number"
396 class="<?php echo esc_attr( $num_class ); ?>"
397 autocomplete="off"
398 min="<?php echo esc_attr( (string) $min ); ?>"
399 max="<?php echo esc_attr( (string) $max ); ?>"
400 step="<?php echo esc_attr( (string) $step ); ?>"
401 value="<?php echo esc_attr( $v ); ?>"
402 <?php
403 echo $num_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
404 ?>
405 />
406 </div>
407 </div>
408 <?php
409 }
410
411 private static function print_length( $id, $key, $scope, $label, $value, $length ) {
412
413 $units = ( isset( $length['units'] ) && is_array( $length['units'] ) ) ? $length['units'] : array( '%' => '%' );
414 $default_unit = isset( $length['default_unit'] ) ? (string) $length['default_unit'] : '%';
415 $bounds_map = ( isset( $length['bounds_map'] ) && is_array( $length['bounds_map'] ) ) ? $length['bounds_map'] : array();
416
417 $raw = is_scalar( $value ) ? trim( (string) $value ) : '';
418
419 $num = '';
420 $unit = '';
421
422 if ( preg_match( '/^\s*([\-]?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i', $raw, $m ) ) {
423 $num = isset( $m[1] ) ? (string) $m[1] : '';
424 $unit = isset( $m[2] ) ? (string) $m[2] : '';
425 }
426
427 if ( '' === $unit ) {
428 $unit = $default_unit;
429 }
430 if ( '' === $num && '' !== $raw ) {
431 $num = $raw;
432 }
433
434 // Ensure unit exists in list.
435 if ( ! isset( $units[ $unit ] ) ) {
436 $unit = $default_unit;
437 }
438
439 // Bounds for current unit.
440 $bounds = array();
441 if ( isset( $bounds_map[ $unit ] ) && is_array( $bounds_map[ $unit ] ) ) {
442 $bounds = $bounds_map[ $unit ];
443 } elseif ( isset( $bounds_map[ $default_unit ] ) && is_array( $bounds_map[ $default_unit ] ) ) {
444 $bounds = $bounds_map[ $default_unit ];
445 }
446
447 $min = isset( $bounds['min'] ) ? (float) $bounds['min'] : 0;
448 $max = isset( $bounds['max'] ) ? (float) $bounds['max'] : 128;
449 $step = isset( $bounds['step'] ) ? (float) $bounds['step'] : 1;
450
451 // Clamp numeric value to bounds when it is numeric.
452 if ( '' !== $num && is_numeric( $num ) ) {
453 $n = (float) $num;
454 if ( $n < $min ) {
455 $n = $min;
456 }
457 if ( $n > $max ) {
458 $n = $max;
459 }
460 $num = (string) $n;
461 }
462
463 $combined = ( '' === $num ) ? '' : ( $num . $unit );
464
465 // Writer input (main value) must have the FS markers + special type="length".
466 // Add the new strict writer marker for wpbc_slider_len_groups.js
467 $writer_attr = self::with_common_fs_data( array( 'id' => $id ), $key, $scope, 'length', $combined );
468 $writer_attr['data-wpbc-bfb-fs-type'] = 'length';
469 $writer_attr['data-wpbc_slider_len_writer'] = '1';
470
471 $writer_attr_str = self::build_attr_string( $writer_attr );
472
473 // Bounds JSON for JS.
474 $bounds_json = ( ! empty( $bounds_map ) ) ? wp_json_encode( $bounds_map ) : '';
475
476 ?>
477 <label class="inspector__label"><strong><?php echo esc_html( $label ); ?></strong></label>
478
479 <div class="inspector__control">
480 <div class="wpbc_slider_len_group inspector__w_100"
481 data-wpbc_slider_len_default_unit="<?php echo esc_attr( $default_unit ); ?>"
482 <?php if ( '' !== $bounds_json ) : ?>
483 data-wpbc_slider_len_bounds_map="<?php echo esc_attr( $bounds_json ); ?>"
484 <?php endif; ?>
485 >
486 <div class="wpbc_inline_inputs">
487 <input type="number"
488 class="inspector__input"
489 data-wpbc_slider_len_value
490 autocomplete="off"
491 min="<?php echo esc_attr( (string) $min ); ?>"
492 max="<?php echo esc_attr( (string) $max ); ?>"
493 step="<?php echo esc_attr( (string) $step ); ?>"
494 value="<?php echo esc_attr( $num ); ?>"
495 />
496 <select class="inspector__input" data-wpbc_slider_len_unit>
497 <?php foreach ( $units as $u_value => $u_label ) : ?>
498 <option value="<?php echo esc_attr( (string) $u_value ); ?>" <?php selected( (string) $u_value, (string) $unit ); ?>>
499 <?php echo esc_html( (string) $u_label ); ?>
500 </option>
501 <?php endforeach; ?>
502 </select>
503 </div>
504
505 <input type="range"
506 class="inspector__input"
507 data-wpbc_slider_len_range
508 min="<?php echo esc_attr( (string) $min ); ?>"
509 max="<?php echo esc_attr( (string) $max ); ?>"
510 step="<?php echo esc_attr( (string) $step ); ?>"
511 value="<?php echo esc_attr( ( '' === $num ) ? (string) $min : $num ); ?>"
512 />
513
514 <input type="text"
515 class="inspector__input"
516 style="display:none;"
517 value="<?php echo esc_attr( $combined ); ?>"
518 <?php
519 echo $writer_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
520 ?>
521 />
522 </div>
523 </div>
524 <?php
525 }
526
527 private static function print_spacing( $id, $key, $scope, $label, $value, $spacing ) {
528
529 $min = isset( $spacing['min'] ) ? (float) $spacing['min'] : 0;
530 $max = isset( $spacing['max'] ) ? (float) $spacing['max'] : 120;
531 $step = isset( $spacing['step'] ) ? (float) $spacing['step'] : 1;
532
533 $raw = is_scalar( $value ) ? trim( (string) $value ) : '';
534 if ( '' === $raw ) {
535 $raw = '10px 30px';
536 }
537
538 preg_match_all( '/-?\d+(?:\.\d+)?/', $raw, $matches );
539 $numbers = isset( $matches[0] ) ? $matches[0] : array();
540 $vertical = isset( $numbers[0] ) ? (float) $numbers[0] : 10;
541 $horizontal = isset( $numbers[1] ) ? (float) $numbers[1] : $vertical;
542
543 $vertical = max( $min, min( $max, $vertical ) );
544 $horizontal = max( $min, min( $max, $horizontal ) );
545
546 $vertical_value = (string) ( $vertical + 0 );
547 $horizontal_value = (string) ( $horizontal + 0 );
548 $combined = $vertical_value . 'px ' . $horizontal_value . 'px';
549
550 $writer_attr = self::with_common_fs_data( array( 'id' => $id ), $key, $scope, 'spacing', $combined );
551 $writer_attr['data-wpbc-bfb-fs-type'] = 'spacing';
552 $writer_attr['data-wpbc_spacing_writer'] = '1';
553
554 $common_input_attr = array(
555 'data-wpbc-bfb-fs-key' => $key,
556 'data-wpbc-bfb-fs-scope' => $scope,
557 'data-wpbc-bfb-fs-type' => 'spacing',
558 );
559
560 ?>
561 <label class="inspector__label"><strong><?php echo esc_html( $label ); ?></strong></label>
562
563 <div class="inspector__control">
564 <div class="wpbc_spacing_group wpbc_inline_inputs inspector__w_100">
565 <label style="display:flex;flex-direction:column;gap:4px;flex:1 1 90px;">
566 <span class="wpbc_bfb__help" style="margin:0;"><?php esc_html_e( 'Top / bottom', 'booking' ); ?></span>
567 <input type="number"
568 class="inspector__input"
569 data-wpbc_spacing_vertical="1"
570 autocomplete="off"
571 min="<?php echo esc_attr( (string) $min ); ?>"
572 max="<?php echo esc_attr( (string) $max ); ?>"
573 step="<?php echo esc_attr( (string) $step ); ?>"
574 value="<?php echo esc_attr( $vertical_value ); ?>"
575 <?php echo self::build_attr_string( $common_input_attr ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
576 />
577 </label>
578 <label style="display:flex;flex-direction:column;gap:4px;flex:1 1 90px;">
579 <span class="wpbc_bfb__help" style="margin:0;"><?php esc_html_e( 'Left / right', 'booking' ); ?></span>
580 <input type="number"
581 class="inspector__input"
582 data-wpbc_spacing_horizontal="1"
583 autocomplete="off"
584 min="<?php echo esc_attr( (string) $min ); ?>"
585 max="<?php echo esc_attr( (string) $max ); ?>"
586 step="<?php echo esc_attr( (string) $step ); ?>"
587 value="<?php echo esc_attr( $horizontal_value ); ?>"
588 <?php echo self::build_attr_string( $common_input_attr ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
589 />
590 </label>
591 <input type="text"
592 class="inspector__input"
593 style="display:none;"
594 value="<?php echo esc_attr( $combined ); ?>"
595 <?php echo self::build_attr_string( $writer_attr ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
596 />
597 </div>
598 </div>
599 <?php
600 }
601
602 private static function print_input( $id, $key, $scope, $label, $value, $attr, $css_class, $input_type, $input_attrs ) {
603
604 $v = is_scalar( $value ) ? (string) $value : '';
605
606 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'input', $v );
607 $control_attr['data-wpbc-bfb-fs-type'] = 'input';
608
609 $attr_str = self::build_attr_string( $control_attr );
610 $extra_attr_str = self::build_attr_string( $input_attrs );
611 ?>
612 <label style="display:block;">
613 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
614 <input type="<?php echo esc_attr( $input_type ); ?>"
615 class="<?php echo esc_attr( $css_class ); ?>"
616 value="<?php echo esc_attr( $v ); ?>"
617 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
618 <?php echo $extra_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
619 />
620 </label>
621 <?php
622 }
623
624 private static function print_color( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs ) {
625
626 $v = is_scalar( $value ) ? (string) $value : '';
627
628 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'color', $v );
629 $control_attr['data-wpbc-bfb-fs-type'] = 'color';
630 $control_attr['data-inspector-type'] = 'color';
631 $control_attr['data-coloris'] = '';
632 $control_attr['data-default-color'] = $v;
633 $control_attr['autocomplete'] = 'off';
634 $control_attr['placeholder'] = '#ffffff';
635
636 $attr_str = self::build_attr_string( $control_attr );
637 $extra_attr_str = self::build_attr_string( $input_attrs );
638 ?>
639 <label style="display:block;">
640 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
641 <input type="text"
642 class="<?php echo esc_attr( $css_class ); ?>"
643 value="<?php echo esc_attr( $v ); ?>"
644 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
645 <?php echo $extra_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
646 />
647 </label>
648 <?php
649 }
650
651 private static function print_textarea( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs ) {
652
653 $v = is_scalar( $value ) ? (string) $value : '';
654
655 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'textarea', $v );
656 $control_attr['data-wpbc-bfb-fs-type'] = 'textarea';
657
658 $attr_str = self::build_attr_string( $control_attr );
659 $extra_attr_str = self::build_attr_string( $input_attrs );
660 ?>
661 <label style="display:block;">
662 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
663 <textarea class="<?php echo esc_attr( $css_class ); ?>"
664 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
665 <?php echo $extra_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
666 ><?php echo esc_textarea( $v ); ?></textarea>
667 </label>
668 <?php
669 }
670
671 // =================================================================================================
672 // Save UI
673 // =================================================================================================
674
675 /**
676 * Print per-row Save UI (global only) using the STABLE wpbc_save_option_from_element() contract.
677 *
678 * @param array $args
679 * @param string $id
680 * @param string $key
681 * @param string $type
682 * @param string $save_ui
683 */
684 private static function print_save_ui( $args, $id, $key, $type, $save_ui ) {
685
686 $save_ui = ( 'always' === $save_ui ) ? 'always' : 'when_changed';
687
688 // Selector used by stable saver to read current value on click.
689 if ( 'radio' === $type ) {
690 $value_from = 'input[name="' . $id . '"]:checked';
691 } else {
692 $value_from = '#' . $id;
693 }
694
695 // Stable saver requires nonce + action.
696 $opt_name = $key; // option name == key
697 $nonce_action = 'wpbc_nonce_' . $opt_name;
698 $nonce_value = wp_create_nonce( $nonce_action );
699
700 $wrap_class = 'wpbc_bfb__form_setting_save';
701 $btn_class = 'button button-primary wpbc_bfb__form_setting_save_btn';
702
703 if ( 'when_changed' === $save_ui ) {
704 $wrap_class .= ' is-hidden';
705 }
706 $autosave_attr = ! empty( $args['autosave_on_form_save'] ) ? ' data-wpbc-u-autosave-on-form-save="1"' : '';
707 ?>
708 <div class="<?php echo esc_attr( $wrap_class ); ?>">
709 <button type="button"
710 class="<?php echo esc_attr( $btn_class ); ?>"
711
712 data-wpbc-u-save="1"
713 data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
714 data-wpbc-u-save-nonce="<?php echo esc_attr( $nonce_value ); ?>"
715 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
716 data-wpbc-u-busy-text="<?php echo esc_attr__( 'Saving…', 'booking' ); ?>"
717
718 data-wpbc-u-save-ui="<?php echo esc_attr( $save_ui ); ?>"
719 data-wpbc-u-save-scope="global"
720 data-wpbc-u-save-value-from="<?php echo esc_attr( $value_from ); ?>"
721 <?php echo $autosave_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
722 ><?php echo esc_html__( 'Save', 'booking' ); ?></button>
723 </div>
724 <?php
725 }
726
727
728 // =================================================================================================
729 // Helpers
730 // =================================================================================================
731
732 private static function is_on( $value ) {
733 $v = strtolower( trim( (string) ( ( null === $value ) ? '' : $value ) ) );
734 return ( 'on' === $v || '1' === $v || 'true' === $v || 'yes' === $v );
735 }
736
737 /**
738 * Add common fs-* markers for JS apply/collect bridge.
739 *
740 * @param array $attr
741 * @param string $key
742 * @param string $scope
743 * @param string $type
744 * @param string $initial
745 *
746 * @return array
747 */
748 private static function with_common_fs_data( $attr, $key, $scope, $type, $initial ) {
749
750 if ( ! is_array( $attr ) ) {
751 $attr = array();
752 }
753
754 $attr['data-wpbc-bfb-fs-key'] = $key;
755 $attr['data-wpbc-bfb-fs-scope'] = $scope;
756 $attr['data-wpbc-bfb-fs-type'] = $type;
757 $attr['data-wpbc-bfb-fs-initial'] = (string) $initial;
758
759 return $attr;
760 }
761
762 /**
763 * Build safe HTML attributes string.
764 *
765 * - blocks inline handlers (on*)
766 *
767 * @param array $attrs
768 *
769 * @return string
770 */
771 private static function build_attr_string( $attrs ) {
772
773 $out = '';
774 if ( empty( $attrs ) || ! is_array( $attrs ) ) {
775 return $out;
776 }
777
778 foreach ( $attrs as $k => $v ) {
779
780 if ( ! is_scalar( $k ) || '' === trim( (string) $k ) ) {
781 continue;
782 }
783
784 $attr_key = strtolower( trim( (string) $k ) );
785
786 // Block inline handlers.
787 if ( 0 === strpos( $attr_key, 'on' ) ) {
788 continue;
789 }
790
791 if ( null === $v ) {
792 continue;
793 }
794
795 $out .= ' ' . esc_attr( $attr_key ) . '="' . esc_attr( (string) $v ) . '"';
796 }
797
798 return $out;
799 }
800 }
801