PluginProbe
Booking Calendar / 11.4.3
Booking Calendar v11.4.3
11.8.4 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 All 204 releases
booking / includes / page-form-builder / form-settings / _src / settings.js

settings.js in Booking Calendar 11.4.3, at includes/page-form-builder/form-settings/_src/settings.js

601 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* globals window, document */
2 (function (w, d) {
3 'use strict';
4
5 /**
6 * BFB Form Settings UI bridge.
7 *
8 * Listens to:
9 * - wpbc:bfb:form_settings:apply (from AJAX load) -> apply to controls
10 * - wpbc:bfb:form_settings:collect (from AJAX save) -> collect from controls
11 *
12 * Optional:
13 * - re-apply after Builder STRUCTURE_LOADED (timing hook only)
14 */
15 const api = (w.WPBC_BFB_FormSettings = w.WPBC_BFB_FormSettings || {});
16
17 // Last received settings pack (from AJAX).
18 let last_settings_pack = null;
19
20 // Small retry, because DOM can be re-rendered after apply event.
21 let raf_id = 0;
22 let retry_count = 0;
23 const retry_max = 20;
24 const fallback_form_style_option_keys = [
25 'booking_form_style',
26 'booking_form_custom_background_color',
27 'booking_form_custom_border_color',
28 'booking_form_custom_border_width',
29 'booking_form_custom_border_radius',
30 'booking_form_custom_padding_vertical',
31 'booking_form_custom_padding_horizontal',
32 'booking_form_custom_text_color',
33 'booking_form_custom_field_background_color',
34 'booking_form_custom_field_text_color',
35 'booking_form_custom_field_border_color',
36 'booking_form_custom_button_background_color',
37 'booking_form_custom_button_text_color',
38 'booking_form_custom_button_border_color',
39 'booking_form_custom_button_hover_background_color',
40 'booking_form_custom_button_hover_text_color',
41 'booking_form_custom_button_hover_border_color',
42 'booking_form_custom_secondary_button_background_color',
43 'booking_form_custom_secondary_button_text_color',
44 'booking_form_custom_secondary_button_border_color',
45 'booking_form_custom_secondary_button_hover_background_color',
46 'booking_form_custom_secondary_button_hover_text_color',
47 'booking_form_custom_secondary_button_hover_border_color',
48 'booking_form_theme',
49 'booking_form_container_style',
50 'booking_form_background_color',
51 'booking_form_border_color',
52 'booking_form_border_width',
53 'booking_form_border_radius',
54 'booking_form_padding',
55 'booking_form_text_color',
56 'booking_form_field_background_color',
57 'booking_form_field_text_color',
58 'booking_form_field_border_color'
59 ];
60
61 // -----------------------------------------------------------------------------------------------
62 // Small helpers
63 // -----------------------------------------------------------------------------------------------
64
65 function query_all(root, selector) {
66 return Array.from((root || d).querySelectorAll(selector));
67 }
68
69 function css_escape(value) {
70 const v = String(value == null ? '' : value);
71 if (w.CSS && typeof w.CSS.escape === 'function') return w.CSS.escape(v);
72 return v.replace(/[^a-zA-Z0-9_\-]/g, '\\$&');
73 }
74
75 function is_on(value) {
76 const v = String(value == null ? '' : value).trim().toLowerCase();
77 return (v === 'on' || v === '1' || v === 'true' || v === 'yes');
78 }
79
80 function set_initial_attr(el, value) {
81 if (!el) return;
82 el.setAttribute('data-wpbc-bfb-fs-initial', String(value == null ? '' : value));
83 }
84
85 function trigger_change(el) {
86 if (!el) return;
87 try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) {}
88 }
89
90 function trigger_input(el) {
91 if ( !el ) return;
92 try { el.dispatchEvent( new Event( 'input', { bubbles: true } ) ); } catch ( _ ) {}
93 }
94
95 function find_rows(scope) {
96 const rows = query_all(d, '.wpbc-setting[data-key]');
97 if (!scope) return rows;
98
99 return rows.filter(function (row) {
100 return String(row.getAttribute('data-scope') || '') === String(scope);
101 });
102 }
103
104 function has_any_rows() {
105 return query_all(d, '.wpbc-setting[data-key]').length > 0;
106 }
107
108 function init_coloris_pickers(root) {
109 if ( ! root || ! w.Coloris ) {
110 return;
111 }
112
113 const inputs = query_all(root, 'input[data-wpbc-bfb-fs-type="color"][data-coloris], input[data-inspector-type="color"][data-coloris]');
114 if ( ! inputs.length ) {
115 return;
116 }
117
118 inputs.forEach(function (input) {
119 if (input.classList.contains('wpbc_bfb_coloris')) return;
120 input.classList.add('wpbc_bfb_coloris');
121 });
122
123 try {
124 w.Coloris({
125 el : '.wpbc_bfb_coloris',
126 alpha : false,
127 format : 'hex',
128 themeMode: 'auto',
129 onChange : function (color, input) {
130 if ( ! input ) {
131 return;
132 }
133 try {
134 input.dispatchEvent( new CustomEvent( 'wpbc:bfb:coloris:change', {
135 bubbles: true,
136 detail : {
137 color : color,
138 currentEl: input
139 }
140 } ) );
141 } catch ( _e ) {}
142 }
143 });
144 } catch (e) {
145 console.warn('WPBC Form Settings: Coloris init failed:', e);
146 }
147 }
148
149 function get_default_custom_appearance_settings() {
150 const localized = w.wpbc_bfb_settings_vars && w.wpbc_bfb_settings_vars.custom_form_style_defaults
151 ? w.wpbc_bfb_settings_vars.custom_form_style_defaults
152 : {};
153
154 return Object.assign( {
155 booking_form_custom_background_color : '#ffffff',
156 booking_form_custom_border_color : '#cccccc',
157 booking_form_custom_border_width : '1px',
158 booking_form_custom_border_radius : '2px',
159 booking_form_custom_padding_vertical : '10px',
160 booking_form_custom_padding_horizontal : '30px',
161 booking_form_custom_text_color : '#1d2327',
162 booking_form_custom_field_background_color : '#ffffff',
163 booking_form_custom_field_text_color : '#3c434a',
164 booking_form_custom_field_border_color : '#cccccc',
165 booking_form_custom_button_background_color: '#066aab',
166 booking_form_custom_button_text_color : '#ffffff',
167 booking_form_custom_button_border_color : '#066aab',
168 booking_form_custom_button_hover_background_color: '#055589',
169 booking_form_custom_button_hover_text_color: '#ffffff',
170 booking_form_custom_button_hover_border_color: '#055589',
171 booking_form_custom_secondary_button_background_color: '#fdfdfd',
172 booking_form_custom_secondary_button_text_color: '#444444',
173 booking_form_custom_secondary_button_border_color: '#eeeeee',
174 booking_form_custom_secondary_button_hover_background_color: '#fdfdfd',
175 booking_form_custom_secondary_button_hover_text_color: '#444444',
176 booking_form_custom_secondary_button_hover_border_color: '#4d91cd'
177 }, localized );
178 }
179
180 function get_form_style_option_keys() {
181 const localized = w.wpbc_bfb_settings_vars && Array.isArray( w.wpbc_bfb_settings_vars.form_style_option_keys )
182 ? w.wpbc_bfb_settings_vars.form_style_option_keys
183 : [];
184
185 return localized.length ? localized : fallback_form_style_option_keys;
186 }
187
188 function strip_form_style_options_from_pack(settings_pack) {
189 if (!settings_pack || typeof settings_pack !== 'object') return settings_pack;
190 if (!settings_pack.options || typeof settings_pack.options !== 'object') return settings_pack;
191
192 get_form_style_option_keys().forEach(function (key) {
193 delete settings_pack.options[key];
194 });
195
196 return settings_pack;
197 }
198
199 // -----------------------------------------------------------------------------------------------
200 // Row setter
201 // -----------------------------------------------------------------------------------------------
202
203 function set_value_for_row(row, value, opts) {
204 if (!row) return;
205
206 const row_type = String(row.getAttribute('data-type') || '');
207 const row_key = String(row.getAttribute('data-key') || '');
208 const do_trigger_events = !!(opts && opts.trigger_change);
209
210 if (!row_key) return;
211
212 // Radio group
213 if (row_type === 'radio') {
214 const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]');
215 const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : '';
216 if (!control_id) return;
217
218 const target_value = String(value == null ? '' : value);
219 const radios = query_all(row, 'input[type="radio"][name="' + css_escape(control_id) + '"]');
220
221 let checked_radio = null;
222 radios.forEach(function (radio) {
223 const should_check = (String(radio.value) === target_value);
224 radio.checked = should_check;
225 if (should_check) checked_radio = radio;
226
227 const choice = radio.closest ? radio.closest('.wpbc_theme_choice') : null;
228 if ( choice ) {
229 choice.classList.toggle('is-selected', should_check);
230 }
231 });
232
233 if (wrap) set_initial_attr(wrap, target_value);
234 if (do_trigger_events && checked_radio) trigger_change(checked_radio);
235 return;
236 }
237
238 // Toggle
239 if (row_type === 'toggle') {
240 const checkbox =
241 row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') ||
242 row.querySelector('input[type="checkbox"]');
243
244 if (!checkbox) return;
245
246 const checked = is_on(value);
247 checkbox.checked = checked;
248 checkbox.setAttribute('aria-checked', checked ? 'true' : 'false');
249
250 set_initial_attr(checkbox, checked ? 'On' : 'Off');
251 if (do_trigger_events) trigger_change(checkbox);
252 return;
253 }
254
255 // Select
256 if (row_type === 'select') {
257 const select =
258 row.querySelector('select[data-wpbc-bfb-fs-type="select"]') ||
259 row.querySelector('select');
260
261 if (!select) return;
262
263 select.value = String(value == null ? '' : value);
264 set_initial_attr(select, select.value);
265 if (do_trigger_events) trigger_change(select);
266 return;
267 }
268
269 // Length: hidden combined + num/unit
270 if ( row_type === 'length' ) {
271 // JS slider length control: - hidden writer carries FS markers and must receive input event so wpbc_slider_len_groups.js syncs UI.
272 const writer =
273 row.querySelector( 'input[data-wpbc_slider_len_writer][data-wpbc-bfb-fs-type="length"]' ) ||
274 row.querySelector( 'input[data-wpbc-bfb-fs-type="length"]' );
275 if ( !writer ) return;
276
277 const combined = String( value == null ? '' : value );
278 writer.value = combined;
279 set_initial_attr( writer, combined );
280 if ( do_trigger_events ) trigger_input( writer );
281 return;
282 }
283
284 // Spacing: two number inputs saved into a hidden CSS shorthand writer.
285 if ( row_type === 'spacing' ) {
286 const group = row.querySelector( '.wpbc_spacing_group' );
287 const vertical_input = group ? group.querySelector( 'input[data-wpbc_spacing_vertical]' ) : null;
288 const horizontal_input = group ? group.querySelector( 'input[data-wpbc_spacing_horizontal]' ) : null;
289 const writer = group ? group.querySelector( 'input[data-wpbc_spacing_writer]' ) : null;
290 const parsed = parse_spacing_value( value );
291
292 if ( ! writer ) {
293 return;
294 }
295
296 if ( vertical_input ) {
297 vertical_input.value = parsed.vertical;
298 }
299 if ( horizontal_input ) {
300 horizontal_input.value = parsed.horizontal;
301 }
302 writer.value = parsed.combined;
303 set_initial_attr( writer, parsed.combined );
304 if ( do_trigger_events ) trigger_input( writer );
305 return;
306 }
307
308 // Range (slider number): writer is the number input.
309 if (row_type === 'range') {
310 const writer =
311 row.querySelector('input[data-wpbc_slider_range_writer]') ||
312 row.querySelector('input[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') ||
313 row.querySelector('input[type="number"]');
314 if (!writer) return;
315
316 writer.value = String(value == null ? '' : value);
317 set_initial_attr(writer, writer.value);
318 if (do_trigger_events) trigger_input(writer);
319 return;
320 }
321
322 // Default: input/textarea
323 const control =
324 row.querySelector('[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') ||
325 row.querySelector('input,textarea');
326
327 if (!control) return;
328
329 control.value = String(value == null ? '' : value);
330 set_initial_attr(control, control.value);
331 // For normal inputs, "input" gives better reactivity than "change".
332 if (do_trigger_events) trigger_input(control);
333 }
334
335 // -----------------------------------------------------------------------------------------------
336 // Public API
337 // -----------------------------------------------------------------------------------------------
338
339 /**
340 * Apply FLAT object (key=>value) to rows of some scope.
341 */
342 function apply_flat_settings(flat_settings, scope, opts) {
343 if (!flat_settings || typeof flat_settings !== 'object') return;
344
345 find_rows(scope).forEach(function (row) {
346 const key = String(row.getAttribute('data-key') || '');
347 if (!key) return;
348 if (!Object.prototype.hasOwnProperty.call(flat_settings, key)) return;
349 set_value_for_row(row, flat_settings[key], opts);
350 });
351 }
352
353 /**
354 * Apply settings.
355 *
356 * Supports:
357 * - flat: { booking_form_layout_width: '100%', ... }
358 * - pack: { options: {...}, css_vars: {...} } (Option A)
359 */
360 api.apply = function (settings_pack, scope, opts) {
361 if (!settings_pack || typeof settings_pack !== 'object') return;
362 if (!settings_pack.options || typeof settings_pack.options !== 'object') return; // strict Option A
363 strip_form_style_options_from_pack(settings_pack);
364 apply_flat_settings(settings_pack.options, scope || 'form', opts);
365 };
366
367 api.reset_custom_appearance = function () {
368 const defaults = get_default_custom_appearance_settings();
369
370 if ( ! last_settings_pack || typeof last_settings_pack !== 'object' ) {
371 last_settings_pack = { options: {}, css_vars: {} };
372 }
373 if ( ! last_settings_pack.options || typeof last_settings_pack.options !== 'object' ) {
374 last_settings_pack.options = {};
375 }
376 Object.keys( defaults ).forEach( function (key) {
377 last_settings_pack.options[key] = defaults[key];
378 } );
379
380 apply_flat_settings( defaults, 'global', { trigger_change: true } );
381 init_coloris_pickers( d );
382
383 if ( w.WPBC_BFB_Settings_Effects && typeof w.WPBC_BFB_Settings_Effects.apply_all === 'function' ) {
384 w.WPBC_BFB_Settings_Effects.apply_all( defaults, { source: 'reset-custom-appearance', options: defaults } );
385 }
386
387 try {
388 d.dispatchEvent( new CustomEvent( 'wpbc:bfb:form_settings:changed', {
389 bubbles: true,
390 detail : {
391 source : 'reset-custom-appearance',
392 settings: { options: Object.assign( {}, defaults ) }
393 }
394 } ) );
395 } catch ( _e ) {}
396 };
397
398 /**
399 * Collect current values (flat object).
400 */
401 api.collect = function (scope) {
402 const out = {};
403
404 find_rows(scope || 'form').forEach(function (row) {
405 const key = String(row.getAttribute('data-key') || '');
406 const type = String(row.getAttribute('data-type') || '');
407 if (!key) return;
408
409 if (type === 'radio') {
410 const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]');
411 const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : '';
412 if (!control_id) return;
413
414 const checked = row.querySelector('input[type="radio"][name="' + css_escape(control_id) + '"]:checked');
415 out[key] = checked ? String(checked.value) : '';
416 return;
417 }
418
419 if (type === 'toggle') {
420 const checkbox =
421 row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') ||
422 row.querySelector('input[type="checkbox"]');
423 out[key] = checkbox && checkbox.checked ? 'On' : 'Off';
424 return;
425 }
426
427 if (type === 'select') {
428 const select = row.querySelector('select');
429 out[key] = select ? String(select.value) : '';
430 return;
431 }
432
433 if (type === 'length') {
434 const hidden = row.querySelector('input[data-wpbc-bfb-fs-type="length"]');
435 out[key] = hidden ? String(hidden.value || '') : '';
436 return;
437 }
438
439 if (type === 'spacing') {
440 out[key] = get_spacing_value(row);
441 return;
442 }
443
444 if (type === 'range') {
445 const writer =
446 row.querySelector('input[data-wpbc_slider_range_writer]') ||
447 row.querySelector('input[type="number"]') ||
448 row.querySelector('input[type="range"]');
449 out[key] = writer ? String(writer.value || '') : '';
450 return;
451 }
452 const control = row.querySelector('input,textarea');
453 out[key] = control ? String(control.value || '') : '';
454 });
455
456 return out;
457 };
458
459 function parse_spacing_value(value) {
460 const fallback = { vertical: '10', horizontal: '30', combined: '10px 30px' };
461 const matches = String(value == null ? '' : value).match(/-?\d+(?:\.\d+)?/g) || [];
462 let vertical = matches[0] != null ? String(matches[0]) : fallback.vertical;
463 let horizontal = matches[1] != null ? String(matches[1]) : vertical;
464
465 if (isNaN(Number(vertical))) {
466 vertical = fallback.vertical;
467 }
468 if (isNaN(Number(horizontal))) {
469 horizontal = fallback.horizontal;
470 }
471
472 return {
473 vertical : vertical,
474 horizontal: horizontal,
475 combined : vertical + 'px ' + horizontal + 'px'
476 };
477 }
478
479 function get_spacing_value(row) {
480 const group = row ? row.querySelector('.wpbc_spacing_group') : null;
481 const vertical_input = group ? group.querySelector('input[data-wpbc_spacing_vertical]') : null;
482 const horizontal_input = group ? group.querySelector('input[data-wpbc_spacing_horizontal]') : null;
483 const writer = group ? group.querySelector('input[data-wpbc_spacing_writer]') : null;
484 const vertical = vertical_input ? String(vertical_input.value || '0') : '0';
485 const horizontal = horizontal_input ? String(horizontal_input.value || vertical) : vertical;
486 const combined = parse_spacing_value(vertical + 'px ' + horizontal + 'px').combined;
487
488 if (writer) {
489 writer.value = combined;
490 }
491
492 return combined;
493 }
494
495 /**
496 * Re-apply last received settings (useful after DOM re-render).
497 */
498 api.reapply_last = function () {
499 if (!last_settings_pack) return;
500 api.apply(last_settings_pack, 'form', { trigger_change: true });
501 };
502
503 api.init = function () {
504 init_coloris_pickers(d);
505
506 // If apply event fired before init, try again now.
507 if (last_settings_pack) schedule_apply_retry();
508 };
509
510 // -----------------------------------------------------------------------------------------------
511 // DOM Events (AJAX layer)
512 // -----------------------------------------------------------------------------------------------
513
514 // Save: let modules contribute into { options:{}, css_vars:{} }
515 d.addEventListener('wpbc:bfb:form_settings:collect', function (e) {
516 const detail = (e && e.detail) ? e.detail : {};
517 const target_pack = detail.settings;
518
519 if (!target_pack || typeof target_pack !== 'object') return;
520
521 // Option A: write into target_pack.options
522 if (!target_pack.options || typeof target_pack.options !== 'object') {
523 target_pack.options = {};
524 }
525
526 const collected = api.collect('form');
527 Object.keys(collected).forEach(function (k) {
528 target_pack.options[k] = collected[k];
529 });
530 strip_form_style_options_from_pack(target_pack);
531 });
532
533 // Load: receive settings from AJAX and apply.
534 d.addEventListener('wpbc:bfb:form_settings:apply', function (e) {
535 const detail = (e && e.detail) ? e.detail : {};
536
537 last_settings_pack = detail.settings || null;
538
539 retry_count = 0;
540 schedule_apply_retry();
541 });
542
543 d.addEventListener( 'click', function (e) {
544 const btn = e && e.target && e.target.closest ? e.target.closest( '[data-wpbc-bfb-reset-custom-appearance]' ) : null;
545 if ( ! btn ) {
546 return;
547 }
548
549 e.preventDefault();
550 api.reset_custom_appearance();
551 }, false );
552
553 function schedule_apply_retry() {
554 if (raf_id) return;
555
556 raf_id = w.requestAnimationFrame(function () {
557 raf_id = 0;
558
559 // If settings UI not present yet, retry a few frames.
560 if (!has_any_rows()) {
561 retry_count++;
562 if (retry_count < retry_max) schedule_apply_retry();
563 return;
564 }
565
566 api.reapply_last();
567 init_coloris_pickers(d);
568 });
569 }
570
571 // -----------------------------------------------------------------------------------------------
572 // Optional Builder timing hook (STRUCTURE_LOADED) -> reapply_last()
573 // -----------------------------------------------------------------------------------------------
574
575 function bind_builder_timing_hook(builder_instance) {
576 const core = w.WPBC_BFB_Core;
577 const events = (core && core.WPBC_BFB_Events) ? core.WPBC_BFB_Events : (w.WPBC_BFB_Events || null);
578
579
580 if (!builder_instance || !builder_instance.bus || !events || !events.STRUCTURE_LOADED) return;
581
582 builder_instance.bus.on(events.STRUCTURE_LOADED, function () {
583 // Builder may re-render settings panel after structure load.
584 // Re-apply last settings pack (if any).
585 retry_count = 0;
586 schedule_apply_retry();
587 });
588 }
589
590 if (w.wpbc_bfb_api && w.wpbc_bfb_api.ready && typeof w.wpbc_bfb_api.ready.then === 'function') {
591 w.wpbc_bfb_api.ready.then(bind_builder_timing_hook);
592 } else {
593 setTimeout(function () { if (w.__B) { bind_builder_timing_hook( w.__B ); } }, 0);
594 }
595
596 // DOM ready init.
597 if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', api.init);
598 else api.init();
599
600 })(window, document);
601