PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 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 All 205 releases
booking / includes / page-form-builder / _src / bfb-time-utils.js

bfb-time-utils.js in Booking Calendar 11.9, at includes/page-form-builder/_src/bfb-time-utils.js

726 lines 24.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPBC BFB Core: Time Utilities
3 *
4 * One place for all time parsing/formatting/masking helpers + small UI helpers used by time-based packs.
5 *
6 * - Pure helpers (parse/format minutes, AM/PM conversion)
7 * - iMask integration for "HH:MM" inputs
8 * - Input-node conversion (type=time <-> masked text)
9 * - Small UI helpers for global "time-slot picker" toggle (placeholder row, checkbox sync)
10 * - Debounced init for external "time selector" (wpbc_hook__init_timeselector)
11 *
12 * @package Booking Calendar
13 * @author wpdevelop
14 * @since 11.0.0
15 * @version 1.0.0
16 * @modified: 2025-10-31 12:32
17 *
18 * ../includes/page-form-builder/_out/bfb-time-utils.js
19 */
20
21 /* global window, document */
22 (function (w, d) {
23 'use strict';
24
25 var Core = w.WPBC_BFB_Core || (w.WPBC_BFB_Core = {});
26 var Time = Core.Time || (Core.Time = {});
27
28 var IMask = w.IMask || null;
29
30 // -----------------------------------------------------------------------------------------------------------------
31 // Basic helpers
32 // -----------------------------------------------------------------------------------------------------------------
33
34 /**
35 * Coerce mixed values to boolean.
36 * Accepts booleans, numbers, and common strings: "on"/"off", "true"/"false", "1"/"0", "yes"/"no".
37 * @param {*} v
38 * @return {boolean}
39 */
40 Time.coerce_to_bool = function (v) {
41 if (typeof v === 'boolean') return v;
42 if (typeof v === 'number') return v !== 0;
43 if (typeof v === 'string') {
44 var s = v.trim().toLowerCase();
45 if (s === 'on' || s === 'true' || s === '1' || s === 'yes') return true;
46 if (s === 'off' || s === 'false' || s === '0' || s === 'no' || s === '') return false;
47 }
48 return !!v;
49 };
50
51 /**
52 * Parse "HH:MM" 24h -> minutes since 00:00. Returns NaN on invalid.
53 * @param {string} hhmm
54 * @return {number}
55 */
56 Time.parse_hhmm_24h = function (hhmm) {
57 if (!hhmm) return NaN;
58 var m = String(hhmm).trim().match(/^(\d{1,2})\s*:\s*(\d{2})$/);
59 if (!m) return NaN;
60 var H = Number(m[1]), M = Number(m[2]);
61 if (H < 0 || H > 23 || M < 0 || M > 59) return NaN;
62 return H * 60 + M;
63 };
64
65 /**
66 * Parse "h:MM AM/PM" -> minutes since 00:00. Returns NaN on invalid.
67 * @param {string} txt
68 * @return {number}
69 */
70 Time.parse_ampm_text = function (txt) {
71 if (!txt) return NaN;
72 var m = String(txt).trim().match(/^(\d{1,2})\s*:\s*(\d{2})\s*([AaPp][Mm])$/);
73 if (!m) return NaN;
74 var h12 = Number(m[1]), mm = Number(m[2]), ap = String(m[3]).toUpperCase();
75 if (h12 < 1 || h12 > 12 || mm < 0 || mm > 59) return NaN;
76 var h24 = (h12 % 12) + (ap === 'PM' ? 12 : 0);
77 return h24 * 60 + mm;
78 };
79
80 /**
81 * Try 24h "HH:MM" first, fall back to AM/PM text.
82 * @param {string} v
83 * @return {number}
84 */
85 Time.parse_minutes = function (v) {
86 var s = String(v || '').trim();
87 var m2 = Time.parse_hhmm_24h(s);
88 return isNaN(m2) ? Time.parse_ampm_text(s) : m2;
89 };
90
91 /**
92 * Format minutes -> "HH:MM" 24h.
93 * @param {number} minutes
94 * @return {string}
95 */
96 Time.format_minutes_24h = function (minutes) {
97 var H = Math.floor(minutes / 60) % 24;
98 var M = minutes % 60;
99 var HH = (H < 10 ? '0' + H : '' + H);
100 var MM = (M < 10 ? '0' + M : '' + M);
101 return HH + ':' + MM;
102 };
103
104 /**
105 * Format minutes -> "h:MM AM/PM".
106 * @param {number} minutes
107 * @return {string}
108 */
109 Time.format_minutes_ampm = function (minutes) {
110 var H24 = Math.floor(minutes / 60) % 24;
111 var M = minutes % 60;
112 var is_am = (H24 < 12);
113 var h12 = H24 % 12;
114 if (h12 === 0) h12 = 12;
115 var MM = (M < 10 ? '0' + M : '' + M);
116 return h12 + ':' + MM + ' ' + (is_am ? 'AM' : 'PM');
117 };
118
119 /**
120 * Escape attribute text.
121 * @param {string} v
122 * @return {string}
123 */
124 Time.esc_attr = function (v) {
125 return String(v).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
126 };
127
128 // -----------------------------------------------------------------------------------------------------------------
129 // iMask helpers (used by 24h text inputs)
130 // -----------------------------------------------------------------------------------------------------------------
131
132 /**
133 * Apply iMask "HH:MM" to input.
134 * @param {HTMLInputElement} el
135 */
136 Time.apply_imask_to_input = function (el) {
137 if (!IMask || !el) return;
138 if (el._imask) {
139 try { el._imask.destroy(); } catch (e) {}
140 el._imask = null;
141 }
142 el._imask = IMask(el, {
143 mask: 'HH:MM',
144 blocks: {
145 HH: { mask: IMask.MaskedRange, from: 0, to: 23, maxLength: 2 },
146 MM: { mask: IMask.MaskedRange, from: 0, to: 59, maxLength: 2 }
147 },
148 lazy: false
149 });
150 };
151
152 /**
153 * Destroy iMask instance if present.
154 * @param {HTMLInputElement} el
155 */
156 Time.clear_imask = function (el) {
157 if (el && el._imask) {
158 try { el._imask.destroy(); } catch (e) {}
159 el._imask = null;
160 }
161 };
162
163 // -----------------------------------------------------------------------------------------------------------------
164 // Node conversion: type=time <-> masked text
165 // -----------------------------------------------------------------------------------------------------------------
166
167 /**
168 * Convert a single start/end input node to '24h' (masked text) or 'ampm' (type="time").
169 * @param {HTMLElement} node
170 * @param {'24h'|'ampm'} to_fmt
171 * @param {number} value_minutes
172 * @return {HTMLInputElement}
173 */
174 Time.convert_input_node_to_format = function (node, to_fmt, value_minutes) {
175 var parent = node.parentNode;
176 var cls = node.className;
177 var is_start = node.classList.contains('wpbc_bfb__opt-start');
178
179 var new_el;
180 if (to_fmt === '24h') {
181 new_el = d.createElement('input');
182 new_el.type = 'text';
183 new_el.className = cls.replace(/\bjs-rt-start-time\b|\bjs-rt-end-time\b/g, '').trim();
184 new_el.classList.add('js-rt-mask');
185 new_el.setAttribute('data-mask-kind', '24h');
186 new_el.setAttribute('placeholder', 'HH:MM');
187 new_el.value = isNaN(value_minutes) ? '' : Time.format_minutes_24h(value_minutes);
188 } else {
189 new_el = d.createElement('input');
190 new_el.type = 'time';
191 new_el.step = '300';
192 new_el.className = cls.replace(/\bjs-rt-mask\b/g, '').trim();
193 new_el.classList.add(is_start ? 'js-rt-start-time' : 'js-rt-end-time');
194 // <input type="time"> expects "HH:MM" 24h string
195 new_el.value = isNaN(value_minutes) ? '' : Time.format_minutes_24h(value_minutes);
196 }
197
198 Time.clear_imask(node);
199 parent.replaceChild(new_el, node);
200 return new_el;
201 };
202
203 /**
204 * Rebuild both start/end inputs inside a row to target format.
205 * @param {HTMLElement} row
206 * @param {'24h'|'ampm'} to_fmt
207 */
208 Time.rebuild_row_inputs_to_format = function (row, to_fmt) {
209 var s_el = row.querySelector('.wpbc_bfb__opt-start');
210 var e_el = row.querySelector('.wpbc_bfb__opt-end');
211 if (!s_el || !e_el) return;
212
213 var s_m = Time.parse_minutes(s_el.value);
214 var e_m = Time.parse_minutes(e_el.value);
215
216 var s_new = Time.convert_input_node_to_format(s_el, to_fmt, s_m);
217 var e_new = Time.convert_input_node_to_format(e_el, to_fmt, e_m);
218
219 if (to_fmt === '24h') {
220 Time.apply_imask_to_input(s_new);
221 Time.apply_imask_to_input(e_new);
222 } else {
223 Time.clear_imask(s_new);
224 Time.clear_imask(e_new);
225 }
226 };
227
228 /**
229 * Rebuild all rows under container to target format.
230 * @param {HTMLElement} container
231 * @param {'24h'|'ampm'} to_fmt
232 */
233 Time.rebuild_all_rows_to_format = function (container, to_fmt) {
234 if (!container) return;
235 container.querySelectorAll('.wpbc_bfb__options_row').forEach(function (row) {
236 Time.rebuild_row_inputs_to_format(row, to_fmt);
237 });
238 };
239
240 /**
241 * Apply iMask to all 24h-masked inputs within container.
242 * @param {HTMLElement} container
243 */
244 Time.apply_imask_in_container_24h = function (container) {
245 if ( !IMask || !container ) return;
246 container.querySelectorAll( 'input[data-mask-kind="24h"]' ).forEach( function (el) {
247 Time.apply_imask_to_input( el );
248 } );
249 };
250
251 // -----------------------------------------------------------------------------------------------------------------
252 // Slot generation
253 // -----------------------------------------------------------------------------------------------------------------
254
255 /**
256 * Build slots: [{label, value, selected:false}, ...]
257 * Note: generation expects end > start. (Overnight ranges are entered manually via editor.)
258 * @param {number} start_minutes
259 * @param {number} end_minutes
260 * @param {number} step_minutes
261 * @param {'24h'|'ampm'} label_fmt
262 * @return {Array<{label:string,value:string,selected:boolean}>}
263 */
264 Time.build_time_slots = function (start_minutes, end_minutes, step_minutes, label_fmt) {
265 if (isNaN(start_minutes) || isNaN(end_minutes) || isNaN(step_minutes)) return [];
266 if (end_minutes <= start_minutes || step_minutes <= 0) return [];
267 var out = [];
268 for (var t = start_minutes; (t + step_minutes) <= end_minutes; t += step_minutes) {
269 var t2 = t + step_minutes;
270 var v1 = Time.format_minutes_24h(t);
271 var v2 = Time.format_minutes_24h(t2);
272 var l1 = (label_fmt === '24h') ? v1 : Time.format_minutes_ampm(t);
273 var l2 = (label_fmt === '24h') ? v2 : Time.format_minutes_ampm(t2);
274 out.push({ label: l1 + ' - ' + l2, value: v1 + ' - ' + v2, selected: false });
275 }
276 return out;
277 };
278
279 // -----------------------------------------------------------------------------------------------------------------
280 // Global "time-slot picker" flag helpers
281 // -----------------------------------------------------------------------------------------------------------------
282
283 /**
284 * Read global time-slot picker flag (saved via _wpbc other params).
285 * @return {boolean}
286 */
287 Time.read_picker_enabled = function () {
288 try {
289 if (!(w._wpbc && typeof w._wpbc.get_other_param === 'function')) return false;
290 return Time.coerce_to_bool(w._wpbc.get_other_param('is_enabled_booking_timeslot_picker'));
291 } catch (e) { return false; }
292 };
293
294 /**
295 * Persist global time-slot picker flag.
296 * @param {boolean} enabled
297 */
298 Time.set_picker_enabled = function (enabled) {
299 try {
300 if (w._wpbc && typeof w._wpbc.set_other_param === 'function') {
301 w._wpbc.set_other_param('is_enabled_booking_timeslot_picker', !!enabled);
302 }
303 } catch (e) {}
304 };
305
306 /**
307 * Set toggle + hide/show placeholder row within a single Inspector panel.
308 * @param {HTMLElement} panel
309 * @param {boolean} enabled
310 */
311 Time.ui_set_picker_toggle_for_panel = function (panel, enabled) {
312 if (!panel) return;
313 var chk = panel.querySelector('.js-toggle-timeslot-picker');
314 if (chk) chk.checked = !!enabled;
315
316 var skin_row = panel.querySelector('.js-time-picker-skin-row');
317 if (skin_row) {
318 skin_row.hidden = !enabled;
319 skin_row.style.display = enabled ? '' : 'none';
320 skin_row.setAttribute( 'aria-hidden', enabled ? 'false' : 'true' );
321 }
322
323 var phRow = panel.querySelector('.js-placeholder-row');
324 if (phRow) {
325 if (enabled) { phRow.style.display = 'none'; phRow.hidden = true; }
326 else { phRow.style.display = ''; phRow.hidden = false; }
327 }
328 };
329
330 /**
331 * Apply picker flag to all open Time inspectors.
332 * @param {boolean} enabled
333 */
334 Time.ui_apply_picker_enabled_to_all = function (enabled) {
335 d.querySelectorAll( '.wpbc_bfb__inspector_timepicker' ).forEach( function (panel) {
336 // Set toggle + hide/show placeholder row within a single Inspector panel.
337 Time.ui_set_picker_toggle_for_panel( panel, enabled );
338 } );
339 };
340
341 /**
342 * Apply a time-picker skin URL directly to the Builder document.
343 *
344 * Updating the existing link avoids a no-styles interval. If another
345 * integration omitted the link, create it so Inspector changes still
346 * produce an immediate Canvas preview.
347 *
348 * @param {string} skin_url Public time-picker skin URL.
349 * @return {boolean} Whether a stylesheet URL was applied.
350 */
351 Time.apply_picker_skin_url = function (skin_url) {
352 if ( ! skin_url ) return false;
353
354 var stylesheet = d.getElementById( 'wpbc-time_picker-skin-css' );
355 if ( ! stylesheet ) {
356 stylesheet = d.createElement( 'link' );
357 stylesheet.id = 'wpbc-time_picker-skin-css';
358 stylesheet.rel = 'stylesheet';
359 stylesheet.type = 'text/css';
360 stylesheet.media = 'screen';
361 ( d.head || d.getElementsByTagName( 'head' )[0] ).appendChild( stylesheet );
362 }
363
364 stylesheet.setAttribute( 'href', String( skin_url ) );
365 if ( Time.read_picker_enabled() ) {
366 Time.set_picker_enabled( true );
367 Time.schedule_init_timeselector();
368 }
369
370 return true;
371 };
372
373 /**
374 * Apply a selected time-picker skin to the Builder preview stylesheet.
375 *
376 * @param {HTMLSelectElement} select_control Skin selectbox.
377 * @return {void}
378 */
379 Time.apply_picker_skin_from_select = function (select_control) {
380 if ( ! select_control ) return;
381 var selected_option = select_control.options && select_control.selectedIndex >= 0
382 ? select_control.options[ select_control.selectedIndex ]
383 : null;
384 var skin_url = selected_option ? String( selected_option.getAttribute( 'data-wpbc-time-picker-skin-url' ) || '' ) : '';
385
386 Time.apply_picker_skin_url( skin_url );
387
388 // The style row is available only while the global picker is enabled.
389 // Reconcile the runtime flag as well, so an older Builder session can
390 // immediately construct its Canvas choices without a page reload.
391 var panel = select_control.closest ? select_control.closest( '.wpbc_bfb__inspector_timepicker' ) : null;
392 var picker_toggle = panel ? panel.querySelector( '.js-toggle-timeslot-picker' ) : null;
393 if ( picker_toggle && picker_toggle.checked ) {
394 Time.set_picker_enabled( true );
395 Time.schedule_init_timeselector();
396 }
397 };
398
399 /**
400 * Synchronize all open time-field skin controls to a saved global value.
401 *
402 * @param {string} skin_value Relative time-picker skin path.
403 * @return {void}
404 */
405 Time.ui_set_picker_skin_value = function (skin_value) {
406 d.querySelectorAll( '.js-wpbc-bfb-time-picker-skin' ).forEach( function (select_control) {
407 select_control.value = String( skin_value || '' );
408 } );
409 };
410
411 /**
412 * Synchronize other controls after a global time-picker skin is saved.
413 *
414 * @return {void}
415 */
416 Time.on_picker_skin_saved = function () {
417 var select_control = d.querySelector( '.js-wpbc-bfb-time-picker-skin' );
418 var skin_value = select_control ? String( select_control.value || '' ) : '';
419 var accent_button = d.querySelector( '[data-wpbc-bfb-apply-accent-components="1"]' );
420
421 Time.ui_set_picker_skin_value( skin_value );
422 if ( accent_button ) {
423 accent_button.setAttribute( 'data-wpbc-time-picker-skin-current', skin_value );
424 }
425 };
426
427 // The generic protected option saver resolves successful callbacks by global function name.
428 w.wpbc_bfb_time_picker_skin_control_saved = Time.on_picker_skin_saved;
429
430 // -----------------------------------------------------------------------------------------------------------------
431 // Debounced init for external time selector (canvas preview)
432 // -----------------------------------------------------------------------------------------------------------------
433
434 /**
435 * Debounced call to global initializer (if present): wpbc_hook__init_timeselector()
436 */
437 Time.schedule_init_timeselector = (function () {
438 let scheduled = false;
439 let tid = null;
440 const DELAY = 30;
441 return function () {
442 if (scheduled) return;
443 scheduled = true;
444 clearTimeout(tid);
445 tid = setTimeout(function run() {
446 scheduled = false;
447 if (!d.querySelector('.wpbc_bfb__preview-timepicker')) return;
448 if (typeof w.wpbc_hook__init_timeselector === 'function') {
449 try {
450 w.__wpbc_rt_mo_pause && w.__wpbc_rt_mo_pause();
451 w.__wpbc_st_mo_pause && w.__wpbc_st_mo_pause();
452 w.wpbc_hook__init_timeselector();
453 } catch ( e ) {/* no-op */
454 } finally {
455 w.__wpbc_rt_mo_resume && w.__wpbc_rt_mo_resume();
456 w.__wpbc_st_mo_resume && w.__wpbc_st_mo_resume();
457 }
458 }
459 }, DELAY );
460 };
461 })();
462
463
464 /**
465 * Mirror to Settings UI without firing DOM 'change' (loop-safe).
466 */
467 Time.mirror_settings_toggle = function (enabled) {
468 wpbc_bfb__dispatch_event_safe(
469 'wpbc:bfb:settings:set',
470 {
471 key : 'booking_timeslot_picker',
472 value : enabled ? 'On' : 'Off',
473 source: 'time-utils'
474 }
475 );
476 };
477
478 /**
479 * Preview refresh for time-slot picker toggle.
480 * - ON: just init external time selector.
481 * - OFF: teardown widgets and unhide <select> controls, then soft re-render (no rebuild).
482 */
483 Time.sync_preview_after_flag = function (enabled) {
484 if ( enabled ) {
485 Time.schedule_init_timeselector();
486 return;
487 }
488 try {
489 if ( typeof w.wpbc_hook__destroy_timeselector === 'function' ) {
490 w.wpbc_hook__destroy_timeselector( d );
491 } else {
492 document.querySelectorAll( '.wpbc_times_selector' ).forEach( function (el) {
493 if ( el.parentNode ) el.parentNode.removeChild( el );
494 } );
495 document.querySelectorAll(
496 '.wpbc_bfb__preview-select.wpbc_bfb__preview-rangetime,' +
497 'select[name^="rangetime"], select[name^="starttime"], select[name^="endtime"], select[name^="durationtime"]'
498 ).forEach( function (s) {
499 s.style.removeProperty( 'display' );
500 s.hidden = false;
501 } );
502 }
503 } catch ( e ) {
504 }
505 if ( window.WPBC_BFB_Settings && typeof window.WPBC_BFB_Settings.when_builder_ready === 'function' ) {
506 window.WPBC_BFB_Settings.when_builder_ready( function (b) {
507 if ( !b || !b.preview_mode ) return;
508 if ( typeof b.refresh_canvas === 'function' ) {
509 b.refresh_canvas( {
510 hard : true,
511 rebuild : false, // critical: no load_saved_structure()
512 reinit : false,
513 restore_selection: true,
514 restore_scroll : true,
515 silent_inspector : true,
516 source : 'settings:timeslot'
517 } );
518 } else if ( typeof b.render_preview_all === 'function' ) {
519 b.render_preview_all();
520 }
521 } );
522 }
523 };
524
525 /**
526 * One-call universal setter used by Settings + all time-field inspectors.
527 */
528 Time.set_global_timeslot_picker = function (enabled, opts) {
529 opts = opts || {};
530 Time.set_picker_enabled( enabled ); // persist in-memory flag
531 Time.ui_apply_picker_enabled_to_all( enabled ); // sync all open inspectors
532 if ( opts.mirror_settings !== false ) {
533 Time.mirror_settings_toggle( enabled ); // mirror Settings toggle (no 'change' event)
534 }
535 if ( opts.refresh_preview !== false ) {
536 Time.sync_preview_after_flag( enabled ); // safe preview refresh
537 }
538 };
539
540 // -----------------------------------------------------------------------------------------------------------------
541 // Global binder: select vs. time picker toggle (ONE-TIME, shared by all time-based packs)
542 // -----------------------------------------------------------------------------------------------------------------
543
544 /**
545 * Bind once to:
546 * - initialize all open Inspector panels with the current global flag,
547 * - react to newly added Inspector panels via MutationObserver,
548 * - persist and broadcast changes when the "Show as time picker" checkbox toggles.
549 */
550 Time.ensure_global_timepicker_toggle_binder = function () {
551
552 if (Time.__toggleBinderBound) return;
553 Time.__toggleBinderBound = true;
554
555 // 1) Init all currently open panels
556 function init_all_panels() {
557 Time.ui_apply_picker_enabled_to_all(Time.read_picker_enabled());
558 }
559 (d.readyState === 'loading')
560 ? d.addEventListener('DOMContentLoaded', init_all_panels)
561 : init_all_panels();
562
563 // 2) Observe Inspector panels that appear later
564 try {
565 var mo = new MutationObserver(function (muts) {
566 var enabled = Time.read_picker_enabled();
567 for (var i = 0; i < muts.length; i++) {
568 var m = muts[i];
569 for (var j = 0; j < m.addedNodes.length; j++) {
570 var n = m.addedNodes[j];
571 if (!n || n.nodeType !== 1) continue;
572
573 if (n.matches && n.matches('.wpbc_bfb__inspector_timepicker')) {
574 try { Time.ui_set_picker_toggle_for_panel(n, enabled); } catch (e) {}
575 } else if (n.querySelector) {
576 n.querySelectorAll('.wpbc_bfb__inspector_timepicker').forEach(function (panel) {
577 try { Time.ui_set_picker_toggle_for_panel(panel, enabled); } catch (e) {}
578 });
579 }
580 }
581 }
582 });
583 mo.observe(d.body, { childList: true, subtree: true });
584 // Optional pause/resume hooks if other modules want to suspend observers temporarily:
585 w.__wpbc_timepicker_toggle_mo_pause = function(){ try { mo.disconnect(); } catch(e){} };
586 w.__wpbc_timepicker_toggle_mo_resume = function(){
587 try { mo.observe(d.body, { childList: true, subtree: true }); } catch(e){}
588 };
589 } catch (e) {}
590
591 // 3) Checkbox handler (delegated).
592 // Skin changes use jQuery below because the previous/next selectbox
593 // controls dispatch jQuery's synthetic `change` event.
594 d.addEventListener('change', function (ev) {
595 var t = ev.target;
596 if (!t || !t.classList) return;
597
598 if (t.classList.contains('js-wpbc-bfb-time-picker-skin')) {
599 if ( ! w.jQuery ) Time.apply_picker_skin_from_select(t);
600 return;
601 }
602 if (!t.classList.contains('js-toggle-timeslot-picker')) return;
603
604 var enabled = !!t.checked;
605 Time.set_global_timeslot_picker( enabled, { source: 'inspector' } );
606 });
607
608 if ( w.jQuery ) {
609 w.jQuery( d )
610 .off( 'change.wpbcBfbTimePickerSkin', '.js-wpbc-bfb-time-picker-skin' )
611 .on( 'change.wpbcBfbTimePickerSkin', '.js-wpbc-bfb-time-picker-skin', function () {
612 Time.apply_picker_skin_from_select( this );
613 } );
614 }
615 };
616
617 // Auto-bind on script load.
618 try { Time.ensure_global_timepicker_toggle_binder(); } catch (e) {}
619
620 // -----------------------------------------------------------------------------------------------------------------
621 // Builder canvas refresh hooks (moved out of bfb-builder.js)
622 // -----------------------------------------------------------------------------------------------------------------
623
624 /**
625 * Bind pause/resume hooks to Builder canvas refresh events.
626 *
627 * Why here:
628 * - This module owns the timepicker-toggle MutationObserver and time selector init.
629 * - Builder should not know about pack-specific observers.
630 *
631 * Safety:
632 * - Idempotent (binds once).
633 * - Waits for wpbc_bfb_api.ready.
634 * - No hard dependency: if builder/bus/events are absent, it silently no-ops.
635 *
636 * @returns {void}
637 */
638 Time.ensure_builder_canvas_refresh_hooks = function () {
639
640 if ( Time.__builder_canvas_refresh_hooks_bound ) {
641 return;
642 }
643 Time.__builder_canvas_refresh_hooks_bound = true;
644
645 // Builder API must exist.
646 if ( !w.wpbc_bfb_api || !w.wpbc_bfb_api.ready || (typeof w.wpbc_bfb_api.ready.then !== 'function') ) {
647 return;
648 }
649
650 w.wpbc_bfb_api.ready.then( function (builder) {
651
652 // Builder might resolve null (timeout) – just ignore.
653 if ( !builder || !builder.bus || (typeof builder.bus.on !== 'function') ) {
654 return;
655 }
656
657 var EVS = (w.WPBC_BFB_Core && w.WPBC_BFB_Core.WPBC_BFB_Events) ? w.WPBC_BFB_Core.WPBC_BFB_Events : {};
658 var EV_BEFORE = EVS.CANVAS_REFRESH || 'wpbc:bfb:canvas-refresh';
659 var EV_AFTER = EVS.CANVAS_REFRESHED || 'wpbc:bfb:canvas-refreshed';
660
661 // BEFORE refresh: pause observers to avoid loops / extra work while DOM is being rebuilt.
662 builder.bus.on( EV_BEFORE, function () {
663 try {
664 if ( typeof w.__wpbc_rt_mo_pause === 'function' ) {
665 w.__wpbc_rt_mo_pause();
666 }
667 } catch ( e ) {
668 }
669 try {
670 if ( typeof w.__wpbc_st_mo_pause === 'function' ) {
671 w.__wpbc_st_mo_pause();
672 }
673 } catch ( e ) {
674 }
675 try {
676 if ( typeof w.__wpbc_timepicker_toggle_mo_pause === 'function' ) {
677 w.__wpbc_timepicker_toggle_mo_pause();
678 }
679 } catch ( e ) {
680 }
681 } );
682
683 // AFTER refresh: resume and (if needed) re-init timeselector widgets.
684 builder.bus.on( EV_AFTER, function () {
685 try {
686 if ( typeof w.__wpbc_rt_mo_resume === 'function' ) {
687 w.__wpbc_rt_mo_resume();
688 }
689 } catch ( e ) {
690 }
691 try {
692 if ( typeof w.__wpbc_st_mo_resume === 'function' ) {
693 w.__wpbc_st_mo_resume();
694 }
695 } catch ( e ) {
696 }
697 try {
698 if ( typeof w.__wpbc_timepicker_toggle_mo_resume === 'function' ) {
699 w.__wpbc_timepicker_toggle_mo_resume();
700 }
701 } catch ( e ) {
702 }
703
704 // If time-slot picker is enabled and builder is in preview mode, re-init the time selector UI.
705 try {
706 if ( builder.preview_mode && typeof Time.read_picker_enabled === 'function' && Time.read_picker_enabled() ) {
707 if ( typeof Time.schedule_init_timeselector === 'function' ) {
708 Time.schedule_init_timeselector();
709 }
710 }
711 } catch ( e ) {
712 }
713 } );
714
715 } );
716 };
717
718 // Call once on load.
719 try {
720 Time.ensure_builder_canvas_refresh_hooks();
721 } catch ( e ) {
722 }
723
724
725 })(window, document);
726