| 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, '&').replace(/"/g, '"').replace(/</g, '<'); |
| 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 |
|