PluginProbe
Booking Calendar / 11.8.3
Booking Calendar v11.8.3
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 / _src / bfb-time-utils.js

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

722 lines 24.3 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 document.querySelectorAll( '.wpbc_times_selector' ).forEach( function (el) {
490 if ( el.parentNode ) el.parentNode.removeChild( el );
491 } );
492 document.querySelectorAll(
493 '.wpbc_bfb__preview-select.wpbc_bfb__preview-rangetime,' +
494 'select[name^="rangetime"], select[name^="starttime"], select[name^="endtime"], select[name^="durationtime"]'
495 ).forEach( function (s) {
496 s.style.removeProperty( 'display' );
497 s.hidden = false;
498 } );
499 } catch ( e ) {
500 }
501 if ( window.WPBC_BFB_Settings && typeof window.WPBC_BFB_Settings.when_builder_ready === 'function' ) {
502 window.WPBC_BFB_Settings.when_builder_ready( function (b) {
503 if ( !b || !b.preview_mode ) return;
504 if ( typeof b.refresh_canvas === 'function' ) {
505 b.refresh_canvas( {
506 hard : true,
507 rebuild : false, // critical: no load_saved_structure()
508 reinit : false,
509 restore_selection: true,
510 restore_scroll : true,
511 silent_inspector : true,
512 source : 'settings:timeslot'
513 } );
514 } else if ( typeof b.render_preview_all === 'function' ) {
515 b.render_preview_all();
516 }
517 } );
518 }
519 };
520
521 /**
522 * One-call universal setter used by Settings + all time-field inspectors.
523 */
524 Time.set_global_timeslot_picker = function (enabled, opts) {
525 opts = opts || {};
526 Time.set_picker_enabled( enabled ); // persist in-memory flag
527 Time.ui_apply_picker_enabled_to_all( enabled ); // sync all open inspectors
528 if ( opts.mirror_settings !== false ) {
529 Time.mirror_settings_toggle( enabled ); // mirror Settings toggle (no 'change' event)
530 }
531 if ( opts.refresh_preview !== false ) {
532 Time.sync_preview_after_flag( enabled ); // safe preview refresh
533 }
534 };
535
536 // -----------------------------------------------------------------------------------------------------------------
537 // Global binder: select vs. time picker toggle (ONE-TIME, shared by all time-based packs)
538 // -----------------------------------------------------------------------------------------------------------------
539
540 /**
541 * Bind once to:
542 * - initialize all open Inspector panels with the current global flag,
543 * - react to newly added Inspector panels via MutationObserver,
544 * - persist and broadcast changes when the "Show as time picker" checkbox toggles.
545 */
546 Time.ensure_global_timepicker_toggle_binder = function () {
547
548 if (Time.__toggleBinderBound) return;
549 Time.__toggleBinderBound = true;
550
551 // 1) Init all currently open panels
552 function init_all_panels() {
553 Time.ui_apply_picker_enabled_to_all(Time.read_picker_enabled());
554 }
555 (d.readyState === 'loading')
556 ? d.addEventListener('DOMContentLoaded', init_all_panels)
557 : init_all_panels();
558
559 // 2) Observe Inspector panels that appear later
560 try {
561 var mo = new MutationObserver(function (muts) {
562 var enabled = Time.read_picker_enabled();
563 for (var i = 0; i < muts.length; i++) {
564 var m = muts[i];
565 for (var j = 0; j < m.addedNodes.length; j++) {
566 var n = m.addedNodes[j];
567 if (!n || n.nodeType !== 1) continue;
568
569 if (n.matches && n.matches('.wpbc_bfb__inspector_timepicker')) {
570 try { Time.ui_set_picker_toggle_for_panel(n, enabled); } catch (e) {}
571 } else if (n.querySelector) {
572 n.querySelectorAll('.wpbc_bfb__inspector_timepicker').forEach(function (panel) {
573 try { Time.ui_set_picker_toggle_for_panel(panel, enabled); } catch (e) {}
574 });
575 }
576 }
577 }
578 });
579 mo.observe(d.body, { childList: true, subtree: true });
580 // Optional pause/resume hooks if other modules want to suspend observers temporarily:
581 w.__wpbc_timepicker_toggle_mo_pause = function(){ try { mo.disconnect(); } catch(e){} };
582 w.__wpbc_timepicker_toggle_mo_resume = function(){
583 try { mo.observe(d.body, { childList: true, subtree: true }); } catch(e){}
584 };
585 } catch (e) {}
586
587 // 3) Checkbox handler (delegated).
588 // Skin changes use jQuery below because the previous/next selectbox
589 // controls dispatch jQuery's synthetic `change` event.
590 d.addEventListener('change', function (ev) {
591 var t = ev.target;
592 if (!t || !t.classList) return;
593
594 if (t.classList.contains('js-wpbc-bfb-time-picker-skin')) {
595 if ( ! w.jQuery ) Time.apply_picker_skin_from_select(t);
596 return;
597 }
598 if (!t.classList.contains('js-toggle-timeslot-picker')) return;
599
600 var enabled = !!t.checked;
601 Time.set_global_timeslot_picker( enabled, { source: 'inspector' } );
602 });
603
604 if ( w.jQuery ) {
605 w.jQuery( d )
606 .off( 'change.wpbcBfbTimePickerSkin', '.js-wpbc-bfb-time-picker-skin' )
607 .on( 'change.wpbcBfbTimePickerSkin', '.js-wpbc-bfb-time-picker-skin', function () {
608 Time.apply_picker_skin_from_select( this );
609 } );
610 }
611 };
612
613 // Auto-bind on script load.
614 try { Time.ensure_global_timepicker_toggle_binder(); } catch (e) {}
615
616 // -----------------------------------------------------------------------------------------------------------------
617 // Builder canvas refresh hooks (moved out of bfb-builder.js)
618 // -----------------------------------------------------------------------------------------------------------------
619
620 /**
621 * Bind pause/resume hooks to Builder canvas refresh events.
622 *
623 * Why here:
624 * - This module owns the timepicker-toggle MutationObserver and time selector init.
625 * - Builder should not know about pack-specific observers.
626 *
627 * Safety:
628 * - Idempotent (binds once).
629 * - Waits for wpbc_bfb_api.ready.
630 * - No hard dependency: if builder/bus/events are absent, it silently no-ops.
631 *
632 * @returns {void}
633 */
634 Time.ensure_builder_canvas_refresh_hooks = function () {
635
636 if ( Time.__builder_canvas_refresh_hooks_bound ) {
637 return;
638 }
639 Time.__builder_canvas_refresh_hooks_bound = true;
640
641 // Builder API must exist.
642 if ( !w.wpbc_bfb_api || !w.wpbc_bfb_api.ready || (typeof w.wpbc_bfb_api.ready.then !== 'function') ) {
643 return;
644 }
645
646 w.wpbc_bfb_api.ready.then( function (builder) {
647
648 // Builder might resolve null (timeout) – just ignore.
649 if ( !builder || !builder.bus || (typeof builder.bus.on !== 'function') ) {
650 return;
651 }
652
653 var EVS = (w.WPBC_BFB_Core && w.WPBC_BFB_Core.WPBC_BFB_Events) ? w.WPBC_BFB_Core.WPBC_BFB_Events : {};
654 var EV_BEFORE = EVS.CANVAS_REFRESH || 'wpbc:bfb:canvas-refresh';
655 var EV_AFTER = EVS.CANVAS_REFRESHED || 'wpbc:bfb:canvas-refreshed';
656
657 // BEFORE refresh: pause observers to avoid loops / extra work while DOM is being rebuilt.
658 builder.bus.on( EV_BEFORE, function () {
659 try {
660 if ( typeof w.__wpbc_rt_mo_pause === 'function' ) {
661 w.__wpbc_rt_mo_pause();
662 }
663 } catch ( e ) {
664 }
665 try {
666 if ( typeof w.__wpbc_st_mo_pause === 'function' ) {
667 w.__wpbc_st_mo_pause();
668 }
669 } catch ( e ) {
670 }
671 try {
672 if ( typeof w.__wpbc_timepicker_toggle_mo_pause === 'function' ) {
673 w.__wpbc_timepicker_toggle_mo_pause();
674 }
675 } catch ( e ) {
676 }
677 } );
678
679 // AFTER refresh: resume and (if needed) re-init timeselector widgets.
680 builder.bus.on( EV_AFTER, function () {
681 try {
682 if ( typeof w.__wpbc_rt_mo_resume === 'function' ) {
683 w.__wpbc_rt_mo_resume();
684 }
685 } catch ( e ) {
686 }
687 try {
688 if ( typeof w.__wpbc_st_mo_resume === 'function' ) {
689 w.__wpbc_st_mo_resume();
690 }
691 } catch ( e ) {
692 }
693 try {
694 if ( typeof w.__wpbc_timepicker_toggle_mo_resume === 'function' ) {
695 w.__wpbc_timepicker_toggle_mo_resume();
696 }
697 } catch ( e ) {
698 }
699
700 // If time-slot picker is enabled and builder is in preview mode, re-init the time selector UI.
701 try {
702 if ( builder.preview_mode && typeof Time.read_picker_enabled === 'function' && Time.read_picker_enabled() ) {
703 if ( typeof Time.schedule_init_timeselector === 'function' ) {
704 Time.schedule_init_timeselector();
705 }
706 }
707 } catch ( e ) {
708 }
709 } );
710
711 } );
712 };
713
714 // Call once on load.
715 try {
716 Time.ensure_builder_canvas_refresh_hooks();
717 } catch ( e ) {
718 }
719
720
721 })(window, document);
722