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 / _src / settings.js

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

358 lines 11.6 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
25 // -----------------------------------------------------------------------------------------------
26 // Small helpers
27 // -----------------------------------------------------------------------------------------------
28
29 function query_all(root, selector) {
30 return Array.from((root || d).querySelectorAll(selector));
31 }
32
33 function css_escape(value) {
34 const v = String(value == null ? '' : value);
35 if (w.CSS && typeof w.CSS.escape === 'function') return w.CSS.escape(v);
36 return v.replace(/[^a-zA-Z0-9_\-]/g, '\\$&');
37 }
38
39 function is_on(value) {
40 const v = String(value == null ? '' : value).trim().toLowerCase();
41 return (v === 'on' || v === '1' || v === 'true' || v === 'yes');
42 }
43
44 function set_initial_attr(el, value) {
45 if (!el) return;
46 el.setAttribute('data-wpbc-bfb-fs-initial', String(value == null ? '' : value));
47 }
48
49 function trigger_change(el) {
50 if (!el) return;
51 try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) {}
52 }
53
54 function trigger_input(el) {
55 if ( !el ) return;
56 try { el.dispatchEvent( new Event( 'input', { bubbles: true } ) ); } catch ( _ ) {}
57 }
58
59 function find_rows(scope) {
60 const rows = query_all(d, '.wpbc-setting[data-key]');
61 if (!scope) return rows;
62
63 return rows.filter(function (row) {
64 return String(row.getAttribute('data-scope') || '') === String(scope);
65 });
66 }
67
68 function has_any_rows() {
69 return query_all(d, '.wpbc-setting[data-key]').length > 0;
70 }
71
72 // -----------------------------------------------------------------------------------------------
73 // Row setter
74 // -----------------------------------------------------------------------------------------------
75
76 function set_value_for_row(row, value, opts) {
77 if (!row) return;
78
79 const row_type = String(row.getAttribute('data-type') || '');
80 const row_key = String(row.getAttribute('data-key') || '');
81 const do_trigger_events = !!(opts && opts.trigger_change);
82
83 if (!row_key) return;
84
85 // Radio group
86 if (row_type === 'radio') {
87 const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]');
88 const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : '';
89 if (!control_id) return;
90
91 const target_value = String(value == null ? '' : value);
92 const radios = query_all(row, 'input[type="radio"][name="' + css_escape(control_id) + '"]');
93
94 let checked_radio = null;
95 radios.forEach(function (radio) {
96 const should_check = (String(radio.value) === target_value);
97 radio.checked = should_check;
98 if (should_check) checked_radio = radio;
99 });
100
101 if (wrap) set_initial_attr(wrap, target_value);
102 if (do_trigger_events && checked_radio) trigger_change(checked_radio);
103 return;
104 }
105
106 // Toggle
107 if (row_type === 'toggle') {
108 const checkbox =
109 row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') ||
110 row.querySelector('input[type="checkbox"]');
111
112 if (!checkbox) return;
113
114 const checked = is_on(value);
115 checkbox.checked = checked;
116 checkbox.setAttribute('aria-checked', checked ? 'true' : 'false');
117
118 set_initial_attr(checkbox, checked ? 'On' : 'Off');
119 if (do_trigger_events) trigger_change(checkbox);
120 return;
121 }
122
123 // Select
124 if (row_type === 'select') {
125 const select =
126 row.querySelector('select[data-wpbc-bfb-fs-type="select"]') ||
127 row.querySelector('select');
128
129 if (!select) return;
130
131 select.value = String(value == null ? '' : value);
132 set_initial_attr(select, select.value);
133 if (do_trigger_events) trigger_change(select);
134 return;
135 }
136
137 // Length: hidden combined + num/unit
138 if ( row_type === 'length' ) {
139 // JS slider length control: - hidden writer carries FS markers and must receive input event so wpbc_slider_len_groups.js syncs UI.
140 const writer =
141 row.querySelector( 'input[data-wpbc_slider_len_writer][data-wpbc-bfb-fs-type="length"]' ) ||
142 row.querySelector( 'input[data-wpbc-bfb-fs-type="length"]' );
143 if ( !writer ) return;
144
145 const combined = String( value == null ? '' : value );
146 writer.value = combined;
147 set_initial_attr( writer, combined );
148 if ( do_trigger_events ) trigger_input( writer );
149 return;
150 }
151
152 // Range (slider number): writer is the number input.
153 if (row_type === 'range') {
154 const writer =
155 row.querySelector('input[data-wpbc_slider_range_writer]') ||
156 row.querySelector('input[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') ||
157 row.querySelector('input[type="number"]');
158 if (!writer) return;
159
160 writer.value = String(value == null ? '' : value);
161 set_initial_attr(writer, writer.value);
162 if (do_trigger_events) trigger_input(writer);
163 return;
164 }
165
166 // Default: input/textarea
167 const control =
168 row.querySelector('[data-wpbc-bfb-fs-key="' + css_escape(row_key) + '"]') ||
169 row.querySelector('input,textarea');
170
171 if (!control) return;
172
173 control.value = String(value == null ? '' : value);
174 set_initial_attr(control, control.value);
175 // For normal inputs, "input" gives better reactivity than "change".
176 if (do_trigger_events) trigger_input(control);
177 }
178
179 // -----------------------------------------------------------------------------------------------
180 // Public API
181 // -----------------------------------------------------------------------------------------------
182
183 /**
184 * Apply FLAT object (key=>value) to rows of some scope.
185 */
186 function apply_flat_settings(flat_settings, scope, opts) {
187 if (!flat_settings || typeof flat_settings !== 'object') return;
188
189 find_rows(scope).forEach(function (row) {
190 const key = String(row.getAttribute('data-key') || '');
191 if (!key) return;
192 if (!Object.prototype.hasOwnProperty.call(flat_settings, key)) return;
193 set_value_for_row(row, flat_settings[key], opts);
194 });
195 }
196
197 /**
198 * Apply settings.
199 *
200 * Supports:
201 * - flat: { booking_form_layout_width: '100%', ... }
202 * - pack: { options: {...}, css_vars: {...} } (Option A)
203 */
204 api.apply = function (settings_pack, scope, opts) {
205 if (!settings_pack || typeof settings_pack !== 'object') return;
206 if (!settings_pack.options || typeof settings_pack.options !== 'object') return; // strict Option A
207 apply_flat_settings(settings_pack.options, scope || 'form', opts);
208 };
209
210 /**
211 * Collect current values (flat object).
212 */
213 api.collect = function (scope) {
214 const out = {};
215
216 find_rows(scope || 'form').forEach(function (row) {
217 const key = String(row.getAttribute('data-key') || '');
218 const type = String(row.getAttribute('data-type') || '');
219 if (!key) return;
220
221 if (type === 'radio') {
222 const wrap = row.querySelector('.wpbc_bfb__form_setting_radio[data-wpbc-bfb-fs-controlid]');
223 const control_id = wrap ? String(wrap.getAttribute('data-wpbc-bfb-fs-controlid') || '') : '';
224 if (!control_id) return;
225
226 const checked = row.querySelector('input[type="radio"][name="' + css_escape(control_id) + '"]:checked');
227 out[key] = checked ? String(checked.value) : '';
228 return;
229 }
230
231 if (type === 'toggle') {
232 const checkbox =
233 row.querySelector('input[type="checkbox"][data-wpbc-bfb-fs-type="toggle"]') ||
234 row.querySelector('input[type="checkbox"]');
235 out[key] = checkbox && checkbox.checked ? 'On' : 'Off';
236 return;
237 }
238
239 if (type === 'select') {
240 const select = row.querySelector('select');
241 out[key] = select ? String(select.value) : '';
242 return;
243 }
244
245 if (type === 'length') {
246 const hidden = row.querySelector('input[data-wpbc-bfb-fs-type="length"]');
247 out[key] = hidden ? String(hidden.value || '') : '';
248 return;
249 }
250
251 if (type === 'range') {
252 const writer =
253 row.querySelector('input[data-wpbc_slider_range_writer]') ||
254 row.querySelector('input[type="number"]') ||
255 row.querySelector('input[type="range"]');
256 out[key] = writer ? String(writer.value || '') : '';
257 return;
258 }
259 const control = row.querySelector('input,textarea');
260 out[key] = control ? String(control.value || '') : '';
261 });
262
263 return out;
264 };
265
266 /**
267 * Re-apply last received settings (useful after DOM re-render).
268 */
269 api.reapply_last = function () {
270 if (!last_settings_pack) return;
271 api.apply(last_settings_pack, 'form', { trigger_change: true });
272 };
273
274 api.init = function () {
275 // If apply event fired before init, try again now.
276 if (last_settings_pack) schedule_apply_retry();
277 };
278
279 // -----------------------------------------------------------------------------------------------
280 // DOM Events (AJAX layer)
281 // -----------------------------------------------------------------------------------------------
282
283 // Save: let modules contribute into { options:{}, css_vars:{} }
284 d.addEventListener('wpbc:bfb:form_settings:collect', function (e) {
285 const detail = (e && e.detail) ? e.detail : {};
286 const target_pack = detail.settings;
287
288 if (!target_pack || typeof target_pack !== 'object') return;
289
290 // Option A: write into target_pack.options
291 if (!target_pack.options || typeof target_pack.options !== 'object') {
292 target_pack.options = {};
293 }
294
295 const collected = api.collect('form');
296 Object.keys(collected).forEach(function (k) {
297 target_pack.options[k] = collected[k];
298 });
299 });
300
301 // Load: receive settings from AJAX and apply.
302 d.addEventListener('wpbc:bfb:form_settings:apply', function (e) {
303 const detail = (e && e.detail) ? e.detail : {};
304
305 last_settings_pack = detail.settings || null;
306
307 retry_count = 0;
308 schedule_apply_retry();
309 });
310
311 function schedule_apply_retry() {
312 if (raf_id) return;
313
314 raf_id = w.requestAnimationFrame(function () {
315 raf_id = 0;
316
317 // If settings UI not present yet, retry a few frames.
318 if (!has_any_rows()) {
319 retry_count++;
320 if (retry_count < retry_max) schedule_apply_retry();
321 return;
322 }
323
324 api.reapply_last();
325 });
326 }
327
328 // -----------------------------------------------------------------------------------------------
329 // Optional Builder timing hook (STRUCTURE_LOADED) -> reapply_last()
330 // -----------------------------------------------------------------------------------------------
331
332 function bind_builder_timing_hook(builder_instance) {
333 const core = w.WPBC_BFB_Core;
334 const events = (core && core.WPBC_BFB_Events) ? core.WPBC_BFB_Events : (w.WPBC_BFB_Events || null);
335
336
337 if (!builder_instance || !builder_instance.bus || !events || !events.STRUCTURE_LOADED) return;
338
339 builder_instance.bus.on(events.STRUCTURE_LOADED, function () {
340 // Builder may re-render settings panel after structure load.
341 // Re-apply last settings pack (if any).
342 retry_count = 0;
343 schedule_apply_retry();
344 });
345 }
346
347 if (w.wpbc_bfb_api && w.wpbc_bfb_api.ready && typeof w.wpbc_bfb_api.ready.then === 'function') {
348 w.wpbc_bfb_api.ready.then(bind_builder_timing_hook);
349 } else {
350 setTimeout(function () { if (w.__B) { bind_builder_timing_hook( w.__B ); } }, 0);
351 }
352
353 // DOM ready init.
354 if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', api.init);
355 else api.init();
356
357 })(window, document);
358