/**
* WPBC BFB Core: Time Utilities
*
* One place for all time parsing/formatting/masking helpers + small UI helpers used by time-based packs.
*
* - Pure helpers (parse/format minutes, AM/PM conversion)
* - iMask integration for "HH:MM" inputs
* - Input-node conversion (type=time <-> masked text)
* - Small UI helpers for global "time-slot picker" toggle (placeholder row, checkbox sync)
* - Debounced init for external "time selector" (wpbc_hook__init_timeselector)
*
* @package Booking Calendar
* @author wpdevelop
* @since 11.0.0
* @version 1.0.0
* @modified: 2025-10-31 12:32
*
* ../includes/page-form-builder/_out/bfb-time-utils.js
*/
/* global window, document */
(function (w, d) {
'use strict';
var Core = w.WPBC_BFB_Core || (w.WPBC_BFB_Core = {});
var Time = Core.Time || (Core.Time = {});
var IMask = w.IMask || null;
// -----------------------------------------------------------------------------------------------------------------
// Basic helpers
// -----------------------------------------------------------------------------------------------------------------
/**
* Coerce mixed values to boolean.
* Accepts booleans, numbers, and common strings: "on"/"off", "true"/"false", "1"/"0", "yes"/"no".
* @param {*} v
* @return {boolean}
*/
Time.coerce_to_bool = function (v) {
if (typeof v === 'boolean') return v;
if (typeof v === 'number') return v !== 0;
if (typeof v === 'string') {
var s = v.trim().toLowerCase();
if (s === 'on' || s === 'true' || s === '1' || s === 'yes') return true;
if (s === 'off' || s === 'false' || s === '0' || s === 'no' || s === '') return false;
}
return !!v;
};
/**
* Parse "HH:MM" 24h -> minutes since 00:00. Returns NaN on invalid.
* @param {string} hhmm
* @return {number}
*/
Time.parse_hhmm_24h = function (hhmm) {
if (!hhmm) return NaN;
var m = String(hhmm).trim().match(/^(\d{1,2})\s*:\s*(\d{2})$/);
if (!m) return NaN;
var H = Number(m[1]), M = Number(m[2]);
if (H < 0 || H > 23 || M < 0 || M > 59) return NaN;
return H * 60 + M;
};
/**
* Parse "h:MM AM/PM" -> minutes since 00:00. Returns NaN on invalid.
* @param {string} txt
* @return {number}
*/
Time.parse_ampm_text = function (txt) {
if (!txt) return NaN;
var m = String(txt).trim().match(/^(\d{1,2})\s*:\s*(\d{2})\s*([AaPp][Mm])$/);
if (!m) return NaN;
var h12 = Number(m[1]), mm = Number(m[2]), ap = String(m[3]).toUpperCase();
if (h12 < 1 || h12 > 12 || mm < 0 || mm > 59) return NaN;
var h24 = (h12 % 12) + (ap === 'PM' ? 12 : 0);
return h24 * 60 + mm;
};
/**
* Try 24h "HH:MM" first, fall back to AM/PM text.
* @param {string} v
* @return {number}
*/
Time.parse_minutes = function (v) {
var s = String(v || '').trim();
var m2 = Time.parse_hhmm_24h(s);
return isNaN(m2) ? Time.parse_ampm_text(s) : m2;
};
/**
* Format minutes -> "HH:MM" 24h.
* @param {number} minutes
* @return {string}
*/
Time.format_minutes_24h = function (minutes) {
var H = Math.floor(minutes / 60) % 24;
var M = minutes % 60;
var HH = (H < 10 ? '0' + H : '' + H);
var MM = (M < 10 ? '0' + M : '' + M);
return HH + ':' + MM;
};
/**
* Format minutes -> "h:MM AM/PM".
* @param {number} minutes
* @return {string}
*/
Time.format_minutes_ampm = function (minutes) {
var H24 = Math.floor(minutes / 60) % 24;
var M = minutes % 60;
var is_am = (H24 < 12);
var h12 = H24 % 12;
if (h12 === 0) h12 = 12;
var MM = (M < 10 ? '0' + M : '' + M);
return h12 + ':' + MM + ' ' + (is_am ? 'AM' : 'PM');
};
/**
* Escape attribute text.
* @param {string} v
* @return {string}
*/
Time.esc_attr = function (v) {
return String(v).replace(/&/g, '&').replace(/"/g, '"').replace(/ masked text
// -----------------------------------------------------------------------------------------------------------------
/**
* Convert a single start/end input node to '24h' (masked text) or 'ampm' (type="time").
* @param {HTMLElement} node
* @param {'24h'|'ampm'} to_fmt
* @param {number} value_minutes
* @return {HTMLInputElement}
*/
Time.convert_input_node_to_format = function (node, to_fmt, value_minutes) {
var parent = node.parentNode;
var cls = node.className;
var is_start = node.classList.contains('wpbc_bfb__opt-start');
var new_el;
if (to_fmt === '24h') {
new_el = d.createElement('input');
new_el.type = 'text';
new_el.className = cls.replace(/\bjs-rt-start-time\b|\bjs-rt-end-time\b/g, '').trim();
new_el.classList.add('js-rt-mask');
new_el.setAttribute('data-mask-kind', '24h');
new_el.setAttribute('placeholder', 'HH:MM');
new_el.value = isNaN(value_minutes) ? '' : Time.format_minutes_24h(value_minutes);
} else {
new_el = d.createElement('input');
new_el.type = 'time';
new_el.step = '300';
new_el.className = cls.replace(/\bjs-rt-mask\b/g, '').trim();
new_el.classList.add(is_start ? 'js-rt-start-time' : 'js-rt-end-time');
// expects "HH:MM" 24h string
new_el.value = isNaN(value_minutes) ? '' : Time.format_minutes_24h(value_minutes);
}
Time.clear_imask(node);
parent.replaceChild(new_el, node);
return new_el;
};
/**
* Rebuild both start/end inputs inside a row to target format.
* @param {HTMLElement} row
* @param {'24h'|'ampm'} to_fmt
*/
Time.rebuild_row_inputs_to_format = function (row, to_fmt) {
var s_el = row.querySelector('.wpbc_bfb__opt-start');
var e_el = row.querySelector('.wpbc_bfb__opt-end');
if (!s_el || !e_el) return;
var s_m = Time.parse_minutes(s_el.value);
var e_m = Time.parse_minutes(e_el.value);
var s_new = Time.convert_input_node_to_format(s_el, to_fmt, s_m);
var e_new = Time.convert_input_node_to_format(e_el, to_fmt, e_m);
if (to_fmt === '24h') {
Time.apply_imask_to_input(s_new);
Time.apply_imask_to_input(e_new);
} else {
Time.clear_imask(s_new);
Time.clear_imask(e_new);
}
};
/**
* Rebuild all rows under container to target format.
* @param {HTMLElement} container
* @param {'24h'|'ampm'} to_fmt
*/
Time.rebuild_all_rows_to_format = function (container, to_fmt) {
if (!container) return;
container.querySelectorAll('.wpbc_bfb__options_row').forEach(function (row) {
Time.rebuild_row_inputs_to_format(row, to_fmt);
});
};
/**
* Apply iMask to all 24h-masked inputs within container.
* @param {HTMLElement} container
*/
Time.apply_imask_in_container_24h = function (container) {
if ( !IMask || !container ) return;
container.querySelectorAll( 'input[data-mask-kind="24h"]' ).forEach( function (el) {
Time.apply_imask_to_input( el );
} );
};
// -----------------------------------------------------------------------------------------------------------------
// Slot generation
// -----------------------------------------------------------------------------------------------------------------
/**
* Build slots: [{label, value, selected:false}, ...]
* Note: generation expects end > start. (Overnight ranges are entered manually via editor.)
* @param {number} start_minutes
* @param {number} end_minutes
* @param {number} step_minutes
* @param {'24h'|'ampm'} label_fmt
* @return {Array<{label:string,value:string,selected:boolean}>}
*/
Time.build_time_slots = function (start_minutes, end_minutes, step_minutes, label_fmt) {
if (isNaN(start_minutes) || isNaN(end_minutes) || isNaN(step_minutes)) return [];
if (end_minutes <= start_minutes || step_minutes <= 0) return [];
var out = [];
for (var t = start_minutes; (t + step_minutes) <= end_minutes; t += step_minutes) {
var t2 = t + step_minutes;
var v1 = Time.format_minutes_24h(t);
var v2 = Time.format_minutes_24h(t2);
var l1 = (label_fmt === '24h') ? v1 : Time.format_minutes_ampm(t);
var l2 = (label_fmt === '24h') ? v2 : Time.format_minutes_ampm(t2);
out.push({ label: l1 + ' - ' + l2, value: v1 + ' - ' + v2, selected: false });
}
return out;
};
// -----------------------------------------------------------------------------------------------------------------
// Global "time-slot picker" flag helpers
// -----------------------------------------------------------------------------------------------------------------
/**
* Read global time-slot picker flag (saved via _wpbc other params).
* @return {boolean}
*/
Time.read_picker_enabled = function () {
try {
if (!(w._wpbc && typeof w._wpbc.get_other_param === 'function')) return false;
return Time.coerce_to_bool(w._wpbc.get_other_param('is_enabled_booking_timeslot_picker'));
} catch (e) { return false; }
};
/**
* Persist global time-slot picker flag.
* @param {boolean} enabled
*/
Time.set_picker_enabled = function (enabled) {
try {
if (w._wpbc && typeof w._wpbc.set_other_param === 'function') {
w._wpbc.set_other_param('is_enabled_booking_timeslot_picker', !!enabled);
}
} catch (e) {}
};
/**
* Set toggle + hide/show placeholder row within a single Inspector panel.
* @param {HTMLElement} panel
* @param {boolean} enabled
*/
Time.ui_set_picker_toggle_for_panel = function (panel, enabled) {
if (!panel) return;
var chk = panel.querySelector('.js-toggle-timeslot-picker');
if (chk) chk.checked = !!enabled;
var skin_row = panel.querySelector('.js-time-picker-skin-row');
if (skin_row) {
skin_row.hidden = !enabled;
skin_row.style.display = enabled ? '' : 'none';
skin_row.setAttribute( 'aria-hidden', enabled ? 'false' : 'true' );
}
var phRow = panel.querySelector('.js-placeholder-row');
if (phRow) {
if (enabled) { phRow.style.display = 'none'; phRow.hidden = true; }
else { phRow.style.display = ''; phRow.hidden = false; }
}
};
/**
* Apply picker flag to all open Time inspectors.
* @param {boolean} enabled
*/
Time.ui_apply_picker_enabled_to_all = function (enabled) {
d.querySelectorAll( '.wpbc_bfb__inspector_timepicker' ).forEach( function (panel) {
// Set toggle + hide/show placeholder row within a single Inspector panel.
Time.ui_set_picker_toggle_for_panel( panel, enabled );
} );
};
/**
* Apply a time-picker skin URL directly to the Builder document.
*
* Updating the existing link avoids a no-styles interval. If another
* integration omitted the link, create it so Inspector changes still
* produce an immediate Canvas preview.
*
* @param {string} skin_url Public time-picker skin URL.
* @return {boolean} Whether a stylesheet URL was applied.
*/
Time.apply_picker_skin_url = function (skin_url) {
if ( ! skin_url ) return false;
var stylesheet = d.getElementById( 'wpbc-time_picker-skin-css' );
if ( ! stylesheet ) {
stylesheet = d.createElement( 'link' );
stylesheet.id = 'wpbc-time_picker-skin-css';
stylesheet.rel = 'stylesheet';
stylesheet.type = 'text/css';
stylesheet.media = 'screen';
( d.head || d.getElementsByTagName( 'head' )[0] ).appendChild( stylesheet );
}
stylesheet.setAttribute( 'href', String( skin_url ) );
if ( Time.read_picker_enabled() ) {
Time.set_picker_enabled( true );
Time.schedule_init_timeselector();
}
return true;
};
/**
* Apply a selected time-picker skin to the Builder preview stylesheet.
*
* @param {HTMLSelectElement} select_control Skin selectbox.
* @return {void}
*/
Time.apply_picker_skin_from_select = function (select_control) {
if ( ! select_control ) return;
var selected_option = select_control.options && select_control.selectedIndex >= 0
? select_control.options[ select_control.selectedIndex ]
: null;
var skin_url = selected_option ? String( selected_option.getAttribute( 'data-wpbc-time-picker-skin-url' ) || '' ) : '';
Time.apply_picker_skin_url( skin_url );
// The style row is available only while the global picker is enabled.
// Reconcile the runtime flag as well, so an older Builder session can
// immediately construct its Canvas choices without a page reload.
var panel = select_control.closest ? select_control.closest( '.wpbc_bfb__inspector_timepicker' ) : null;
var picker_toggle = panel ? panel.querySelector( '.js-toggle-timeslot-picker' ) : null;
if ( picker_toggle && picker_toggle.checked ) {
Time.set_picker_enabled( true );
Time.schedule_init_timeselector();
}
};
/**
* Synchronize all open time-field skin controls to a saved global value.
*
* @param {string} skin_value Relative time-picker skin path.
* @return {void}
*/
Time.ui_set_picker_skin_value = function (skin_value) {
d.querySelectorAll( '.js-wpbc-bfb-time-picker-skin' ).forEach( function (select_control) {
select_control.value = String( skin_value || '' );
} );
};
/**
* Synchronize other controls after a global time-picker skin is saved.
*
* @return {void}
*/
Time.on_picker_skin_saved = function () {
var select_control = d.querySelector( '.js-wpbc-bfb-time-picker-skin' );
var skin_value = select_control ? String( select_control.value || '' ) : '';
var accent_button = d.querySelector( '[data-wpbc-bfb-apply-accent-components="1"]' );
Time.ui_set_picker_skin_value( skin_value );
if ( accent_button ) {
accent_button.setAttribute( 'data-wpbc-time-picker-skin-current', skin_value );
}
};
// The generic protected option saver resolves successful callbacks by global function name.
w.wpbc_bfb_time_picker_skin_control_saved = Time.on_picker_skin_saved;
// -----------------------------------------------------------------------------------------------------------------
// Debounced init for external time selector (canvas preview)
// -----------------------------------------------------------------------------------------------------------------
/**
* Debounced call to global initializer (if present): wpbc_hook__init_timeselector()
*/
Time.schedule_init_timeselector = (function () {
let scheduled = false;
let tid = null;
const DELAY = 30;
return function () {
if (scheduled) return;
scheduled = true;
clearTimeout(tid);
tid = setTimeout(function run() {
scheduled = false;
if (!d.querySelector('.wpbc_bfb__preview-timepicker')) return;
if (typeof w.wpbc_hook__init_timeselector === 'function') {
try {
w.__wpbc_rt_mo_pause && w.__wpbc_rt_mo_pause();
w.__wpbc_st_mo_pause && w.__wpbc_st_mo_pause();
w.wpbc_hook__init_timeselector();
} catch ( e ) {/* no-op */
} finally {
w.__wpbc_rt_mo_resume && w.__wpbc_rt_mo_resume();
w.__wpbc_st_mo_resume && w.__wpbc_st_mo_resume();
}
}
}, DELAY );
};
})();
/**
* Mirror to Settings UI without firing DOM 'change' (loop-safe).
*/
Time.mirror_settings_toggle = function (enabled) {
wpbc_bfb__dispatch_event_safe(
'wpbc:bfb:settings:set',
{
key : 'booking_timeslot_picker',
value : enabled ? 'On' : 'Off',
source: 'time-utils'
}
);
};
/**
* Preview refresh for time-slot picker toggle.
* - ON: just init external time selector.
* - OFF: teardown widgets and unhide