PluginProbe
Booking Calendar / 11.0
Booking Calendar v11.0
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.0, at includes/page-form-builder/form-settings/options-render.php

650 lines 22.2 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'|'range'|'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 ?>
86 <div class="<?php echo esc_attr( $row_class ); ?>"
87 data-scope="<?php echo esc_attr( $scope ); ?>"
88 data-key="<?php echo esc_attr( $key ); ?>"
89 data-type="<?php echo esc_attr( $type ); ?>">
90 <div class="inspector__row" style="justify-content:space-between;">
91 <div class="inspector__control" style="flex:1 1 auto;">
92
93 <?php self::print_control( $args, $id, $key, $scope, $type, $default_value ); ?>
94
95 <?php if ( '' !== $help ) : ?>
96 <p class="wpbc_bfb__help" style="margin:6px 0 0 0;"><?php echo esc_html( $help ); ?></p>
97 <?php endif; ?>
98
99 </div>
100
101 <?php
102 // Global scope only: per-row Save UI.
103 if ( 'global' === $scope ) {
104 self::print_save_ui( $args, $id, $key, $type, $save_ui );
105 }
106 ?>
107 </div>
108 </div>
109 <?php
110 }
111
112 // =================================================================================================
113 // Controls
114 // =================================================================================================
115
116 /**
117 * Print control (toggle/select/radio/length/input/textarea).
118 *
119 * @param array $args
120 * @param string $id
121 * @param string $key
122 * @param string $scope
123 * @param string $type
124 * @param mixed $value
125 */
126 private static function print_control( $args, $id, $key, $scope, $type, $value ) {
127
128 $label = isset( $args['label'] ) ? (string) $args['label'] : '';
129
130 $attr = ( isset( $args['attr'] ) && is_array( $args['attr'] ) ) ? $args['attr'] : array();
131 $attr['id'] = $id;
132
133 // Extract/normalize class.
134 $css_class = '';
135 if ( isset( $attr['class'] ) ) {
136 $css_class = trim( (string) $attr['class'] );
137 unset( $attr['class'] );
138 }
139 $css_class = trim( 'wpbc-setting__control inspector__input ' . $css_class );
140
141 // Toggle.
142 if ( 'toggle' === $type ) {
143 self::print_toggle( $id, $key, $scope, $label, $value, $attr, $css_class );
144 return;
145 }
146
147 // Select.
148 if ( 'select' === $type ) {
149 $options = ( isset( $args['options'] ) && is_array( $args['options'] ) ) ? $args['options'] : array();
150 self::print_select( $id, $key, $scope, $label, $value, $attr, $css_class, $options );
151 return;
152 }
153
154 // Radio.
155 if ( 'radio' === $type ) {
156 $options = ( isset( $args['options'] ) && is_array( $args['options'] ) ) ? $args['options'] : array();
157 $radio_layout = ( isset( $args['radio_layout'] ) && 'inline' === $args['radio_layout'] ) ? 'inline' : 'stack';
158 self::print_radio( $id, $key, $scope, $label, $value, $options, $radio_layout );
159 return;
160 }
161
162 // Length.
163 if ( 'length' === $type ) {
164 $length = ( isset( $args['length'] ) && is_array( $args['length'] ) ) ? $args['length'] : array();
165 self::print_length( $id, $key, $scope, $label, $value, $length );
166 return;
167 }
168
169 // Range (slider number).
170 if ( 'range' === $type ) {
171 $range = ( isset( $args['range'] ) && is_array( $args['range'] ) ) ? $args['range'] : array();
172 self::print_range( $id, $key, $scope, $label, $value, $range, $attr, $css_class );
173 return;
174 }
175
176 // Textarea.
177 if ( 'textarea' === $type ) {
178 $input_attrs = ( isset( $args['input_attrs'] ) && is_array( $args['input_attrs'] ) ) ? $args['input_attrs'] : array();
179 self::print_textarea( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs );
180 return;
181 }
182
183 // Input (default).
184 $input_type = isset( $args['input_type'] ) ? (string) $args['input_type'] : 'text';
185 $input_attrs = ( isset( $args['input_attrs'] ) && is_array( $args['input_attrs'] ) ) ? $args['input_attrs'] : array();
186 self::print_input( $id, $key, $scope, $label, $value, $attr, $css_class, $input_type, $input_attrs );
187 }
188
189 private static function print_toggle( $id, $key, $scope, $label, $value, $attr, $css_class ) {
190
191 $is_on = self::is_on( $value );
192 $initial = $is_on ? 'On' : 'Off';
193
194 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'toggle', $initial );
195 $control_attr['data-wpbc-bfb-fs-type'] = 'toggle';
196
197 $attr_str = self::build_attr_string( $control_attr );
198 ?>
199 <div class="wpbc_ui__toggle">
200 <input type="checkbox"
201 class="<?php echo esc_attr( $css_class ); ?>"
202 value="On"
203 aria-checked="<?php echo $is_on ? 'true' : 'false'; ?>"
204 <?php checked( $is_on ); ?>
205 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
206 />
207 <label class="wpbc_ui__toggle_icon" for="<?php echo esc_attr( $id ); ?>"></label>
208 <label class="wpbc_ui__toggle_label" for="<?php echo esc_attr( $id ); ?>"><strong><?php echo esc_html( $label ); ?></strong></label>
209 </div>
210 <?php
211 }
212
213 private static function print_select( $id, $key, $scope, $label, $value, $attr, $css_class, $options ) {
214
215 $v = is_scalar( $value ) ? (string) $value : '';
216
217 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'select', $v );
218 $control_attr['data-wpbc-bfb-fs-type'] = 'select';
219
220 $attr_str = self::build_attr_string( $control_attr );
221 ?>
222 <label style="display:block;">
223 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
224 <select class="<?php echo esc_attr( $css_class ); ?>"
225 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
226 <?php foreach ( $options as $opt_value => $opt_label ) : ?>
227 <option value="<?php echo esc_attr( (string) $opt_value ); ?>" <?php selected( (string) $opt_value, $v ); ?>>
228 <?php echo esc_html( (string) $opt_label ); ?>
229 </option>
230 <?php endforeach; ?>
231 </select>
232 </label>
233 <?php
234 }
235
236 private static function print_radio( $id, $key, $scope, $label, $value, $options, $radio_layout ) {
237
238 $v = is_scalar( $value ) ? (string) $value : '';
239
240 $wrapper_style = ( 'inline' === $radio_layout )
241 ? 'display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:0;'
242 : 'margin:0;';
243 $item_style = ( 'inline' === $radio_layout )
244 ? ''
245 : 'display:flex;align-items:center;gap:8px;margin:4px 0;';
246
247 // Put fs data on wrapper (JS reads control id from wrapper).
248 $wrap_attr = array(
249 'data-wpbc-bfb-fs-key' => $key,
250 'data-wpbc-bfb-fs-scope' => $scope,
251 'data-wpbc-bfb-fs-type' => 'radio',
252 'data-wpbc-bfb-fs-initial' => $v,
253 'data-wpbc-bfb-fs-controlid' => $id,
254 );
255 $wrap_attr_str = self::build_attr_string( $wrap_attr );
256 ?>
257 <div class="wpbc_bfb__form_setting_radio"
258 <?php echo $wrap_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
259 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
260
261 <div style="<?php echo esc_attr( $wrapper_style ); ?>">
262 <?php
263 $i = 0;
264 foreach ( $options as $opt_value => $opt_label ) :
265 $i++;
266 $rid = $id . '__' . $i;
267 ?>
268 <label style="<?php echo esc_attr( $item_style ); ?>">
269 <input type="radio"
270 name="<?php echo esc_attr( $id ); ?>"
271 id="<?php echo esc_attr( $rid ); ?>"
272 value="<?php echo esc_attr( (string) $opt_value ); ?>"
273 data-wpbc-bfb-fs-radio="1"
274 <?php checked( (string) $opt_value, $v ); ?>
275 />
276 <?php echo esc_html( (string) $opt_label ); ?>
277 </label>
278 <?php endforeach; ?>
279 </div>
280 </div>
281 <?php
282 }
283
284 /**
285 * Print slider number control (range + number).
286 *
287 * IMPORTANT:
288 * - Uses only NEW wpbc_slider_* attributes (no back-compat).
289 * - Number input is the "writer" and carries FS markers + id (Save UI reads #id).
290 *
291 * Markup:
292 * <label for="ID" class="inspector__label">...</label>
293 * <div class="inspector__control">
294 * <div class="wpbc_slider_range_group wpbc_inline_inputs">
295 * <input type="range" data-wpbc_slider_range_range ...>
296 * <input type="number" data-wpbc_slider_range_value data-wpbc_slider_range_writer id="ID" ...>
297 * </div>
298 * </div>
299 */
300 private static function print_range( $id, $key, $scope, $label, $value, $range, $attr, $css_class ) {
301
302 $min = isset( $range['min'] ) ? (float) $range['min'] : 0;
303 $max = isset( $range['max'] ) ? (float) $range['max'] : 100;
304 $step = isset( $range['step'] ) ? (float) $range['step'] : 1;
305
306 $v = is_scalar( $value ) ? (string) $value : '';
307 if ( '' === trim( $v ) ) {
308 $v = (string) $min;
309 }
310
311 // Clamp numeric value.
312 if ( is_numeric( $v ) ) {
313 $n = (float) $v;
314 if ( $n < $min ) { $n = $min; }
315 if ( $n > $max ) { $n = $max; }
316 // Keep formatting simple.
317 $v = (string) $n;
318 }
319
320 // Number input = writer (id stays here, Save UI reads #id).
321 // Keep FS type as "input" for compatibility with existing apply/collect code.
322 $num_attr = self::with_common_fs_data( $attr, $key, $scope, 'input', $v );
323 $num_attr['data-wpbc-bfb-fs-type'] = 'input';
324 $num_attr['data-wpbc_slider_range_value'] = '1';
325 $num_attr['data-wpbc_slider_range_writer'] = '1';
326
327 // Ensure the number input keeps the $id.
328 $num_attr['id'] = $id;
329
330 $num_attr_str = self::build_attr_string( $num_attr );
331
332 // Number input CSS (adds width helper).
333 $num_class = trim( $css_class . ' inspector__w_30' );
334 ?>
335 <label for="<?php echo esc_attr( $id ); ?>" class="inspector__label"><strong><?php echo esc_html( $label ); ?></strong></label>
336
337 <div class="inspector__control">
338 <div class="wpbc_slider_range_group wpbc_inline_inputs">
339 <input type="range"
340 class="inspector__input"
341 data-wpbc_slider_range_range="1"
342 min="<?php echo esc_attr( (string) $min ); ?>"
343 max="<?php echo esc_attr( (string) $max ); ?>"
344 step="<?php echo esc_attr( (string) $step ); ?>"
345 value="<?php echo esc_attr( $v ); ?>"
346 />
347
348 <input type="number"
349 class="<?php echo esc_attr( $num_class ); ?>"
350 autocomplete="off"
351 min="<?php echo esc_attr( (string) $min ); ?>"
352 max="<?php echo esc_attr( (string) $max ); ?>"
353 step="<?php echo esc_attr( (string) $step ); ?>"
354 value="<?php echo esc_attr( $v ); ?>"
355 <?php
356 echo $num_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
357 ?>
358 />
359 </div>
360 </div>
361 <?php
362 }
363
364 private static function print_length( $id, $key, $scope, $label, $value, $length ) {
365
366 $units = ( isset( $length['units'] ) && is_array( $length['units'] ) ) ? $length['units'] : array( '%' => '%' );
367 $default_unit = isset( $length['default_unit'] ) ? (string) $length['default_unit'] : '%';
368 $bounds_map = ( isset( $length['bounds_map'] ) && is_array( $length['bounds_map'] ) ) ? $length['bounds_map'] : array();
369
370 $raw = is_scalar( $value ) ? trim( (string) $value ) : '';
371
372 $num = '';
373 $unit = '';
374
375 if ( preg_match( '/^\s*([\-]?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i', $raw, $m ) ) {
376 $num = isset( $m[1] ) ? (string) $m[1] : '';
377 $unit = isset( $m[2] ) ? (string) $m[2] : '';
378 }
379
380 if ( '' === $unit ) {
381 $unit = $default_unit;
382 }
383 if ( '' === $num && '' !== $raw ) {
384 $num = $raw;
385 }
386
387 // Ensure unit exists in list.
388 if ( ! isset( $units[ $unit ] ) ) {
389 $unit = $default_unit;
390 }
391
392 // Bounds for current unit.
393 $bounds = array();
394 if ( isset( $bounds_map[ $unit ] ) && is_array( $bounds_map[ $unit ] ) ) {
395 $bounds = $bounds_map[ $unit ];
396 } elseif ( isset( $bounds_map[ $default_unit ] ) && is_array( $bounds_map[ $default_unit ] ) ) {
397 $bounds = $bounds_map[ $default_unit ];
398 }
399
400 $min = isset( $bounds['min'] ) ? (float) $bounds['min'] : 0;
401 $max = isset( $bounds['max'] ) ? (float) $bounds['max'] : 128;
402 $step = isset( $bounds['step'] ) ? (float) $bounds['step'] : 1;
403
404 // Clamp numeric value to bounds when it is numeric.
405 if ( '' !== $num && is_numeric( $num ) ) {
406 $n = (float) $num;
407 if ( $n < $min ) {
408 $n = $min;
409 }
410 if ( $n > $max ) {
411 $n = $max;
412 }
413 $num = (string) $n;
414 }
415
416 $combined = ( '' === $num ) ? '' : ( $num . $unit );
417
418 // Writer input (main value) must have the FS markers + special type="length".
419 // Add the new strict writer marker for wpbc_slider_len_groups.js
420 $writer_attr = self::with_common_fs_data( array( 'id' => $id ), $key, $scope, 'length', $combined );
421 $writer_attr['data-wpbc-bfb-fs-type'] = 'length';
422 $writer_attr['data-wpbc_slider_len_writer'] = '1';
423
424 $writer_attr_str = self::build_attr_string( $writer_attr );
425
426 // Bounds JSON for JS.
427 $bounds_json = ( ! empty( $bounds_map ) ) ? wp_json_encode( $bounds_map ) : '';
428
429 ?>
430 <label class="inspector__label"><strong><?php echo esc_html( $label ); ?></strong></label>
431
432 <div class="inspector__control">
433 <div class="wpbc_slider_len_group inspector__w_100"
434 data-wpbc_slider_len_default_unit="<?php echo esc_attr( $default_unit ); ?>"
435 <?php if ( '' !== $bounds_json ) : ?>
436 data-wpbc_slider_len_bounds_map="<?php echo esc_attr( $bounds_json ); ?>"
437 <?php endif; ?>
438 >
439 <div class="wpbc_inline_inputs">
440 <input type="number"
441 class="inspector__input"
442 data-wpbc_slider_len_value
443 autocomplete="off"
444 min="<?php echo esc_attr( (string) $min ); ?>"
445 max="<?php echo esc_attr( (string) $max ); ?>"
446 step="<?php echo esc_attr( (string) $step ); ?>"
447 value="<?php echo esc_attr( $num ); ?>"
448 />
449 <select class="inspector__input" data-wpbc_slider_len_unit>
450 <?php foreach ( $units as $u_value => $u_label ) : ?>
451 <option value="<?php echo esc_attr( (string) $u_value ); ?>" <?php selected( (string) $u_value, (string) $unit ); ?>>
452 <?php echo esc_html( (string) $u_label ); ?>
453 </option>
454 <?php endforeach; ?>
455 </select>
456 </div>
457
458 <input type="range"
459 class="inspector__input"
460 data-wpbc_slider_len_range
461 min="<?php echo esc_attr( (string) $min ); ?>"
462 max="<?php echo esc_attr( (string) $max ); ?>"
463 step="<?php echo esc_attr( (string) $step ); ?>"
464 value="<?php echo esc_attr( ( '' === $num ) ? (string) $min : $num ); ?>"
465 />
466
467 <input type="text"
468 class="inspector__input"
469 style="display:none;"
470 value="<?php echo esc_attr( $combined ); ?>"
471 <?php
472 echo $writer_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
473 ?>
474 />
475 </div>
476 </div>
477 <?php
478 }
479
480 private static function print_input( $id, $key, $scope, $label, $value, $attr, $css_class, $input_type, $input_attrs ) {
481
482 $v = is_scalar( $value ) ? (string) $value : '';
483
484 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'input', $v );
485 $control_attr['data-wpbc-bfb-fs-type'] = 'input';
486
487 $attr_str = self::build_attr_string( $control_attr );
488 $extra_attr_str = self::build_attr_string( $input_attrs );
489 ?>
490 <label style="display:block;">
491 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
492 <input type="<?php echo esc_attr( $input_type ); ?>"
493 class="<?php echo esc_attr( $css_class ); ?>"
494 value="<?php echo esc_attr( $v ); ?>"
495 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
496 <?php echo $extra_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
497 />
498 </label>
499 <?php
500 }
501
502 private static function print_textarea( $id, $key, $scope, $label, $value, $attr, $css_class, $input_attrs ) {
503
504 $v = is_scalar( $value ) ? (string) $value : '';
505
506 $control_attr = self::with_common_fs_data( $attr, $key, $scope, 'textarea', $v );
507 $control_attr['data-wpbc-bfb-fs-type'] = 'textarea';
508
509 $attr_str = self::build_attr_string( $control_attr );
510 $extra_attr_str = self::build_attr_string( $input_attrs );
511 ?>
512 <label style="display:block;">
513 <div style="margin:0 0 6px 0;"><strong><?php echo esc_html( $label ); ?></strong></div>
514 <textarea class="<?php echo esc_attr( $css_class ); ?>"
515 <?php echo $attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
516 <?php echo $extra_attr_str; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
517 ><?php echo esc_textarea( $v ); ?></textarea>
518 </label>
519 <?php
520 }
521
522 // =================================================================================================
523 // Save UI
524 // =================================================================================================
525
526 /**
527 * Print per-row Save UI (global only) using the STABLE wpbc_save_option_from_element() contract.
528 *
529 * @param array $args
530 * @param string $id
531 * @param string $key
532 * @param string $type
533 * @param string $save_ui
534 */
535 private static function print_save_ui( $args, $id, $key, $type, $save_ui ) {
536
537 $save_ui = ( 'always' === $save_ui ) ? 'always' : 'when_changed';
538
539 // Selector used by stable saver to read current value on click.
540 if ( 'radio' === $type ) {
541 $value_from = 'input[name="' . $id . '"]:checked';
542 } else {
543 $value_from = '#' . $id;
544 }
545
546 // Stable saver requires nonce + action.
547 $opt_name = $key; // option name == key
548 $nonce_action = 'wpbc_nonce_' . $opt_name;
549 $nonce_value = wp_create_nonce( $nonce_action );
550
551 $wrap_class = 'wpbc_bfb__form_setting_save';
552 $btn_class = 'button button-primary wpbc_bfb__form_setting_save_btn';
553
554 if ( 'when_changed' === $save_ui ) {
555 $wrap_class .= ' is-hidden';
556 }
557 ?>
558 <div class="<?php echo esc_attr( $wrap_class ); ?>">
559 <button type="button"
560 class="<?php echo esc_attr( $btn_class ); ?>"
561
562 data-wpbc-u-save="1"
563 data-wpbc-u-save-name="<?php echo esc_attr( $opt_name ); ?>"
564 data-wpbc-u-save-nonce="<?php echo esc_attr( $nonce_value ); ?>"
565 data-wpbc-u-save-action="<?php echo esc_attr( $nonce_action ); ?>"
566 data-wpbc-u-busy-text="<?php echo esc_attr__( 'Saving…', 'booking' ); ?>"
567
568 data-wpbc-u-save-ui="<?php echo esc_attr( $save_ui ); ?>"
569 data-wpbc-u-save-scope="global"
570 data-wpbc-u-save-value-from="<?php echo esc_attr( $value_from ); ?>"
571 ><?php echo esc_html__( 'Save', 'booking' ); ?></button>
572 </div>
573 <?php
574 }
575
576
577 // =================================================================================================
578 // Helpers
579 // =================================================================================================
580
581 private static function is_on( $value ) {
582 $v = strtolower( trim( (string) ( ( null === $value ) ? '' : $value ) ) );
583 return ( 'on' === $v || '1' === $v || 'true' === $v || 'yes' === $v );
584 }
585
586 /**
587 * Add common fs-* markers for JS apply/collect bridge.
588 *
589 * @param array $attr
590 * @param string $key
591 * @param string $scope
592 * @param string $type
593 * @param string $initial
594 *
595 * @return array
596 */
597 private static function with_common_fs_data( $attr, $key, $scope, $type, $initial ) {
598
599 if ( ! is_array( $attr ) ) {
600 $attr = array();
601 }
602
603 $attr['data-wpbc-bfb-fs-key'] = $key;
604 $attr['data-wpbc-bfb-fs-scope'] = $scope;
605 $attr['data-wpbc-bfb-fs-type'] = $type;
606 $attr['data-wpbc-bfb-fs-initial'] = (string) $initial;
607
608 return $attr;
609 }
610
611 /**
612 * Build safe HTML attributes string.
613 *
614 * - blocks inline handlers (on*)
615 *
616 * @param array $attrs
617 *
618 * @return string
619 */
620 private static function build_attr_string( $attrs ) {
621
622 $out = '';
623 if ( empty( $attrs ) || ! is_array( $attrs ) ) {
624 return $out;
625 }
626
627 foreach ( $attrs as $k => $v ) {
628
629 if ( ! is_scalar( $k ) || '' === trim( (string) $k ) ) {
630 continue;
631 }
632
633 $attr_key = strtolower( trim( (string) $k ) );
634
635 // Block inline handlers.
636 if ( 0 === strpos( $attr_key, 'on' ) ) {
637 continue;
638 }
639
640 if ( null === $v ) {
641 continue;
642 }
643
644 $out .= ' ' . esc_attr( $attr_key ) . '="' . esc_attr( (string) $v ) . '"';
645 }
646
647 return $out;
648 }
649 }
650