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 / _src / bfb-time-utils.js

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

610 lines 20.4 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 phRow = panel.querySelector('.js-placeholder-row');
317 if (phRow) {
318 if (enabled) { phRow.style.display = 'none'; phRow.hidden = true; }
319 else { phRow.style.display = ''; phRow.hidden = false; }
320 }
321 };
322
323 /**
324 * Apply picker flag to all open Time inspectors.
325 * @param {boolean} enabled
326 */
327 Time.ui_apply_picker_enabled_to_all = function (enabled) {
328 d.querySelectorAll( '.wpbc_bfb__inspector_timepicker' ).forEach( function (panel) {
329 // Set toggle + hide/show placeholder row within a single Inspector panel.
330 Time.ui_set_picker_toggle_for_panel( panel, enabled );
331 } );
332 };
333
334 // -----------------------------------------------------------------------------------------------------------------
335 // Debounced init for external time selector (canvas preview)
336 // -----------------------------------------------------------------------------------------------------------------
337
338 /**
339 * Debounced call to global initializer (if present): wpbc_hook__init_timeselector()
340 */
341 Time.schedule_init_timeselector = (function () {
342 let scheduled = false;
343 let tid = null;
344 const DELAY = 30;
345 return function () {
346 if (scheduled) return;
347 scheduled = true;
348 clearTimeout(tid);
349 tid = setTimeout(function run() {
350 scheduled = false;
351 if (!d.querySelector('.wpbc_bfb__preview-timepicker')) return;
352 if (typeof w.wpbc_hook__init_timeselector === 'function') {
353 try {
354 w.__wpbc_rt_mo_pause && w.__wpbc_rt_mo_pause();
355 w.__wpbc_st_mo_pause && w.__wpbc_st_mo_pause();
356 w.wpbc_hook__init_timeselector();
357 } catch ( e ) {/* no-op */
358 } finally {
359 w.__wpbc_rt_mo_resume && w.__wpbc_rt_mo_resume();
360 w.__wpbc_st_mo_resume && w.__wpbc_st_mo_resume();
361 }
362 }
363 }, DELAY );
364 };
365 })();
366
367
368 /**
369 * Mirror to Settings UI without firing DOM 'change' (loop-safe).
370 */
371 Time.mirror_settings_toggle = function (enabled) {
372 wpbc_bfb__dispatch_event_safe(
373 'wpbc:bfb:settings:set',
374 {
375 key : 'booking_timeslot_picker',
376 value : enabled ? 'On' : 'Off',
377 source: 'time-utils'
378 }
379 );
380 };
381
382 /**
383 * Preview refresh for time-slot picker toggle.
384 * - ON: just init external time selector.
385 * - OFF: teardown widgets and unhide <select> controls, then soft re-render (no rebuild).
386 */
387 Time.sync_preview_after_flag = function (enabled) {
388 if ( enabled ) {
389 Time.schedule_init_timeselector();
390 return;
391 }
392 try {
393 document.querySelectorAll( '.wpbc_times_selector' ).forEach( function (el) {
394 if ( el.parentNode ) el.parentNode.removeChild( el );
395 } );
396 document.querySelectorAll(
397 '.wpbc_bfb__preview-select.wpbc_bfb__preview-rangetime,' +
398 'select[name^="rangetime"], select[name^="starttime"], select[name^="endtime"], select[name^="durationtime"]'
399 ).forEach( function (s) {
400 s.style.removeProperty( 'display' );
401 s.hidden = false;
402 } );
403 } catch ( e ) {
404 }
405 if ( window.WPBC_BFB_Settings && typeof window.WPBC_BFB_Settings.when_builder_ready === 'function' ) {
406 window.WPBC_BFB_Settings.when_builder_ready( function (b) {
407 if ( !b || !b.preview_mode ) return;
408 if ( typeof b.refresh_canvas === 'function' ) {
409 b.refresh_canvas( {
410 hard : true,
411 rebuild : false, // critical: no load_saved_structure()
412 reinit : false,
413 restore_selection: true,
414 restore_scroll : true,
415 silent_inspector : true,
416 source : 'settings:timeslot'
417 } );
418 } else if ( typeof b.render_preview_all === 'function' ) {
419 b.render_preview_all();
420 }
421 } );
422 }
423 };
424
425 /**
426 * One-call universal setter used by Settings + all time-field inspectors.
427 */
428 Time.set_global_timeslot_picker = function (enabled, opts) {
429 opts = opts || {};
430 Time.set_picker_enabled( enabled ); // persist in-memory flag
431 Time.ui_apply_picker_enabled_to_all( enabled ); // sync all open inspectors
432 if ( opts.mirror_settings !== false ) {
433 Time.mirror_settings_toggle( enabled ); // mirror Settings toggle (no 'change' event)
434 }
435 if ( opts.refresh_preview !== false ) {
436 Time.sync_preview_after_flag( enabled ); // safe preview refresh
437 }
438 };
439
440 // -----------------------------------------------------------------------------------------------------------------
441 // Global binder: select vs. time picker toggle (ONE-TIME, shared by all time-based packs)
442 // -----------------------------------------------------------------------------------------------------------------
443
444 /**
445 * Bind once to:
446 * - initialize all open Inspector panels with the current global flag,
447 * - react to newly added Inspector panels via MutationObserver,
448 * - persist and broadcast changes when the "Show as time picker" checkbox toggles.
449 */
450 Time.ensure_global_timepicker_toggle_binder = function () {
451
452 if (Time.__toggleBinderBound) return;
453 Time.__toggleBinderBound = true;
454
455 // 1) Init all currently open panels
456 function init_all_panels() {
457 Time.ui_apply_picker_enabled_to_all(Time.read_picker_enabled());
458 }
459 (d.readyState === 'loading')
460 ? d.addEventListener('DOMContentLoaded', init_all_panels)
461 : init_all_panels();
462
463 // 2) Observe Inspector panels that appear later
464 try {
465 var mo = new MutationObserver(function (muts) {
466 var enabled = Time.read_picker_enabled();
467 for (var i = 0; i < muts.length; i++) {
468 var m = muts[i];
469 for (var j = 0; j < m.addedNodes.length; j++) {
470 var n = m.addedNodes[j];
471 if (!n || n.nodeType !== 1) continue;
472
473 if (n.matches && n.matches('.wpbc_bfb__inspector_timepicker')) {
474 try { Time.ui_set_picker_toggle_for_panel(n, enabled); } catch (e) {}
475 } else if (n.querySelector) {
476 n.querySelectorAll('.wpbc_bfb__inspector_timepicker').forEach(function (panel) {
477 try { Time.ui_set_picker_toggle_for_panel(panel, enabled); } catch (e) {}
478 });
479 }
480 }
481 }
482 });
483 mo.observe(d.body, { childList: true, subtree: true });
484 // Optional pause/resume hooks if other modules want to suspend observers temporarily:
485 w.__wpbc_timepicker_toggle_mo_pause = function(){ try { mo.disconnect(); } catch(e){} };
486 w.__wpbc_timepicker_toggle_mo_resume = function(){
487 try { mo.observe(d.body, { childList: true, subtree: true }); } catch(e){}
488 };
489 } catch (e) {}
490
491 // 3) Checkbox handler (delegated)
492 d.addEventListener('change', function (ev) {
493 var t = ev.target;
494 if (!t || !t.classList || !t.classList.contains('js-toggle-timeslot-picker')) return;
495
496 var enabled = !!t.checked;
497 Time.set_global_timeslot_picker( enabled, { source: 'inspector' } );
498 });
499 };
500
501 // Auto-bind on script load.
502 try { Time.ensure_global_timepicker_toggle_binder(); } catch (e) {}
503
504 // -----------------------------------------------------------------------------------------------------------------
505 // Builder canvas refresh hooks (moved out of bfb-builder.js)
506 // -----------------------------------------------------------------------------------------------------------------
507
508 /**
509 * Bind pause/resume hooks to Builder canvas refresh events.
510 *
511 * Why here:
512 * - This module owns the timepicker-toggle MutationObserver and time selector init.
513 * - Builder should not know about pack-specific observers.
514 *
515 * Safety:
516 * - Idempotent (binds once).
517 * - Waits for wpbc_bfb_api.ready.
518 * - No hard dependency: if builder/bus/events are absent, it silently no-ops.
519 *
520 * @returns {void}
521 */
522 Time.ensure_builder_canvas_refresh_hooks = function () {
523
524 if ( Time.__builder_canvas_refresh_hooks_bound ) {
525 return;
526 }
527 Time.__builder_canvas_refresh_hooks_bound = true;
528
529 // Builder API must exist.
530 if ( !w.wpbc_bfb_api || !w.wpbc_bfb_api.ready || (typeof w.wpbc_bfb_api.ready.then !== 'function') ) {
531 return;
532 }
533
534 w.wpbc_bfb_api.ready.then( function (builder) {
535
536 // Builder might resolve null (timeout) – just ignore.
537 if ( !builder || !builder.bus || (typeof builder.bus.on !== 'function') ) {
538 return;
539 }
540
541 var EVS = (w.WPBC_BFB_Core && w.WPBC_BFB_Core.WPBC_BFB_Events) ? w.WPBC_BFB_Core.WPBC_BFB_Events : {};
542 var EV_BEFORE = EVS.CANVAS_REFRESH || 'wpbc:bfb:canvas-refresh';
543 var EV_AFTER = EVS.CANVAS_REFRESHED || 'wpbc:bfb:canvas-refreshed';
544
545 // BEFORE refresh: pause observers to avoid loops / extra work while DOM is being rebuilt.
546 builder.bus.on( EV_BEFORE, function () {
547 try {
548 if ( typeof w.__wpbc_rt_mo_pause === 'function' ) {
549 w.__wpbc_rt_mo_pause();
550 }
551 } catch ( e ) {
552 }
553 try {
554 if ( typeof w.__wpbc_st_mo_pause === 'function' ) {
555 w.__wpbc_st_mo_pause();
556 }
557 } catch ( e ) {
558 }
559 try {
560 if ( typeof w.__wpbc_timepicker_toggle_mo_pause === 'function' ) {
561 w.__wpbc_timepicker_toggle_mo_pause();
562 }
563 } catch ( e ) {
564 }
565 } );
566
567 // AFTER refresh: resume and (if needed) re-init timeselector widgets.
568 builder.bus.on( EV_AFTER, function () {
569 try {
570 if ( typeof w.__wpbc_rt_mo_resume === 'function' ) {
571 w.__wpbc_rt_mo_resume();
572 }
573 } catch ( e ) {
574 }
575 try {
576 if ( typeof w.__wpbc_st_mo_resume === 'function' ) {
577 w.__wpbc_st_mo_resume();
578 }
579 } catch ( e ) {
580 }
581 try {
582 if ( typeof w.__wpbc_timepicker_toggle_mo_resume === 'function' ) {
583 w.__wpbc_timepicker_toggle_mo_resume();
584 }
585 } catch ( e ) {
586 }
587
588 // If time-slot picker is enabled and builder is in preview mode, re-init the time selector UI.
589 try {
590 if ( builder.preview_mode && typeof Time.read_picker_enabled === 'function' && Time.read_picker_enabled() ) {
591 if ( typeof Time.schedule_init_timeselector === 'function' ) {
592 Time.schedule_init_timeselector();
593 }
594 }
595 } catch ( e ) {
596 }
597 } );
598
599 } );
600 };
601
602 // Call once on load.
603 try {
604 Time.ensure_builder_canvas_refresh_hooks();
605 } catch ( e ) {
606 }
607
608
609 })(window, document);
610