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 / field-packs / calendar / _src / calendar.js

calendar.js in Booking Calendar 11.9, at includes/page-form-builder/field-packs/calendar/_src/calendar.js

1,034 lines 36.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ---------------------------------------------------------------------------------------------------------------------
2 // == File /includes/page-form-builder/field-packs/calendar/_out/calendar.js
3 // == Pack Calendar (WP-template–driven) — minimal, modern, and Builder-focused renderer
4 // == Compatible with PHP pack: ../calendar.php (version 1.2.2)
5 // ---------------------------------------------------------------------------------------------------------------------
6 (function (w) {
7 'use strict';
8
9 // Direct dev logger alias (snake format); no local wrappers.
10 const dev = (w._wpbc && w._wpbc.dev) ? w._wpbc.dev : { log(){}, error(){} };
11
12 // Core singletons from the Builder.
13 var Core = w.WPBC_BFB_Core || {};
14 var Registry = Core.WPBC_BFB_Field_Renderer_Registry;
15 var Field_Base = Core.WPBC_BFB_Field_Base || null;
16
17 if ( !Registry || !Registry.register ) {
18 dev.error( 'WPBC_BFB_Field_Calendar', 'Registry missing — load bfb-core first.' );
19 return;
20 }
21
22 // Localized boot payload from PHP (calendar.php::enqueue_js()).
23 var Boot = w.WPBC_BFB_CalendarBoot || {};
24
25 // Remember which resource IDs already have their data loaded this session.
26 var rid_loaded_cache = Object.create( null );
27
28 // -----------------------------------------------------------------------------------------------------------------
29 // Resource ID Helpers
30 // -----------------------------------------------------------------------------------------------------------------
31 /**
32 * Get configured preview booking resource ID from localized boot data.
33 *
34 * @returns {number}
35 */
36 function get_configured_preview_resource_id() {
37 var rid = Number( Boot.default_preview_resource_id || 1 );
38 rid = isFinite( rid ) ? Math.max( 1, Math.floor( rid ) ) : 1;
39 return rid;
40 }
41
42 /**
43 * Get first existing booking resource ID from localized boot data.
44 *
45 * @returns {number}
46 */
47 function get_first_existing_resource_id() {
48
49 var resources = Array.isArray( Boot.booking_resources ) ? Boot.booking_resources : [];
50
51 for ( var i = 0; i < resources.length; i++ ) {
52 var id = Number( resources[i] && resources[i].booking_type_id );
53 if ( isFinite( id ) && id > 0 ) {
54 return Math.floor( id );
55 }
56 }
57
58 return 1;
59 }
60
61 /**
62 * Resolve effective preview booking resource ID.
63 * Priority:
64 * 1. Configured default preview resource, if it exists
65 * 2. Provided rid, if it exists
66 * 3. First existing booking resource
67 * 4. Fallback to 1
68 *
69 * @param {number} rid_candidate
70 * @returns {number}
71 */
72 function resolve_effective_resource_id(rid_candidate) {
73
74 var resources = Array.isArray( Boot.booking_resources ) ? Boot.booking_resources : [];
75 var configured_rid = get_configured_preview_resource_id();
76 var candidate = Number( rid_candidate || 1 );
77
78 function resource_exists(id) {
79 id = Number( id || 1 );
80 if ( ! isFinite( id ) || id <= 0 ) {
81 return false;
82 }
83
84 for ( var i = 0; i < resources.length; i++ ) {
85 if ( Number( resources[i] && resources[i].booking_type_id ) === id ) {
86 return true;
87 }
88 }
89 return false;
90 }
91
92 if ( resource_exists( configured_rid ) ) {
93 return configured_rid;
94 }
95
96 if ( resource_exists( candidate ) ) {
97 return candidate;
98 }
99
100 return get_first_existing_resource_id();
101 }
102
103 // -----------------------------------------------------------------------------------------------------------------
104 // Small utilities
105 // -----------------------------------------------------------------------------------------------------------------
106
107 /**
108 * Debounce wrapper.
109 *
110 * @param {Function} fn
111 * @param {number} ms
112 * @returns {Function}
113 */
114 function debounce(fn, ms) {
115 var t;
116 return function () {
117 var a = arguments;
118 clearTimeout( t );
119 t = setTimeout( function () {
120 fn.apply( null, a );
121 }, ms );
122 };
123 }
124
125 /**
126 * Wait until calendar API is present in the window (e.g., wpbc_calendar_show).
127 *
128 * @param {Function} cb Callback when ready.
129 * @param {number} max_tries Maximum retry attempts.
130 * @param {number} delay_ms Delay between attempts.
131 */
132 function wait_until_api_ready(cb, max_tries, delay_ms) {
133 var tries = 0;
134 (function tick() {
135 if ( typeof w.wpbc_calendar_show === 'function' ) {
136 try {
137 cb();
138 } catch ( e ) {
139 dev.error( 'api_ready_cb', e );
140 }
141 return;
142 }
143 if ( tries++ >= (max_tries || 40) ) {
144 dev.log( 'calendar_api_not_ready_after_retries' );
145 return;
146 }
147 w.setTimeout( tick, delay_ms || 100 );
148 })();
149 }
150
151 /**
152 * Apply "months in row" class to container element.
153 *
154 * @param {Element} field_el Field root element (wrap).
155 * @param {number} months 1..12
156 */
157 function apply_months_class(field_el, months) {
158 var cont = field_el ? field_el.querySelector( '.wpbc_cal_container' ) : null;
159 if ( ! cont ) {
160 return;
161 }
162 // Remove existing cal_month_num_* safely via classList.
163 Array.from( cont.classList ).forEach( function (c) {
164 if ( /^cal_month_num_\d+$/.test( c ) ) {
165 cont.classList.remove( c );
166 }
167 } );
168 cont.classList.add( 'cal_month_num_' + months );
169 }
170
171 /**
172 * Set secure parameters (nonce, user_id, locale) before any AJAX calls.
173 */
174 function set_secure_params() {
175 try {
176 if ( !(w._wpbc && typeof w._wpbc.set_secure_param === 'function') ) {
177 return;
178 }
179 if ( Boot.nonce ) {
180 w._wpbc.set_secure_param( 'nonce', String( Boot.nonce ) );
181 }
182 if ( Boot.user_id != null ) {
183 w._wpbc.set_secure_param( 'user_id', String( Boot.user_id ) );
184 }
185 if ( Boot.locale ) {
186 w._wpbc.set_secure_param( 'locale', String( Boot.locale ) );
187 }
188 } catch ( e ) {
189 dev.log( 'secure_params_skip', e );
190 }
191 }
192
193 /**
194 * Builder canvas preview starts at next month so it does not mostly show past/unavailable dates near month end.
195 *
196 * @returns {number[]} [year, month]
197 */
198 function get_canvas_next_month_scroll_to() {
199 var today_arr = null;
200
201 try {
202 if ( w._wpbc && typeof w._wpbc.get_other_param === 'function' ) {
203 today_arr = w._wpbc.get_other_param( 'today_arr' );
204 }
205 } catch ( e ) {
206 }
207
208 var base_date = new Date();
209 if ( today_arr && today_arr.length >= 2 ) {
210 base_date = new Date( Number( today_arr[0] ), Number( today_arr[1] ) - 1, 1 );
211 } else {
212 base_date.setDate( 1 );
213 }
214
215 base_date.setMonth( base_date.getMonth() + 1 );
216
217 return [ base_date.getFullYear(), base_date.getMonth() + 1 ];
218 }
219
220 /**
221 * Push calendar environment parameters for a specific resource and months count.
222 * We *directly* set parameters (no global polyfill) to keep this file self-contained.
223 *
224 * @param {number} rid
225 * @param {number} months
226 */
227 function set_calendar_params(rid, months) {
228 var L = Boot || {};
229 try {
230 if ( w._wpbc && typeof w._wpbc.balancer__set_max_threads === 'function' ) {
231 w._wpbc.balancer__set_max_threads( Number( L.balancer_max_threads || 1 ) );
232 }
233 } catch ( e ) {
234 }
235
236 function set_param(k, v) {
237 try {
238 if ( w._wpbc && typeof w._wpbc.calendar__set_param_value === 'function' ) {
239 w._wpbc.calendar__set_param_value( rid, k, v );
240 }
241 } catch ( e ) {
242 }
243 }
244
245 if ( L.booking_max_monthes_in_calendar != null ) {
246 set_param( 'booking_max_monthes_in_calendar', String( L.booking_max_monthes_in_calendar ) );
247 }
248 if ( L.booking_start_day_weeek != null ) {
249 set_param( 'booking_start_day_weeek', String( L.booking_start_day_weeek ) );
250 }
251 set_param( 'calendar_number_of_months', String( months ) );
252 set_param( 'calendar_scroll_to', get_canvas_next_month_scroll_to() );
253
254 if ( L.booking_date_format ) {
255 set_param( 'booking_date_format', String( L.booking_date_format ) );
256 }
257 if ( L.booking_time_format ) {
258 set_param( 'booking_time_format', String( L.booking_time_format ) );
259 }
260
261 var ds = L.days_selection || {};
262 set_param( 'days_select_mode', String( ds.days_select_mode || 'multiple' ) );
263 set_param( 'fixed__days_num', Number( ds.fixed__days_num || 0 ) );
264 if ( ds.fixed__week_days__start != null ) {
265 set_param( 'fixed__week_days__start', [ String( ds.fixed__week_days__start ) ] );
266 }
267 set_param( 'dynamic__days_min', Number( ds.dynamic__days_min || 0 ) );
268 set_param( 'dynamic__days_max', Number( ds.dynamic__days_max || 0 ) );
269 if ( ds.dynamic__days_specific != null ) {
270 var arr = String( ds.dynamic__days_specific || '' ).split( /\s*,\s*/ ).filter( Boolean ).map( Number );
271 set_param( 'dynamic__days_specific', arr );
272 }
273 if ( ds.dynamic__week_days__start != null ) {
274 set_param( 'dynamic__week_days__start', [ String( ds.dynamic__week_days__start ) ] );
275 }
276
277 try {
278 if ( typeof w.wpbc__conditions__SAVE_INITIAL__days_selection_params__bm === 'function' ) {
279 w.wpbc__conditions__SAVE_INITIAL__days_selection_params__bm( rid );
280 }
281 } catch ( e ) {
282 }
283 }
284
285 /**
286 * Extract sanitized rid and months from data or DOM.
287 *
288 * @param {Element} field_el
289 * @param {object} data
290 * @returns {{rid:number, months:number}}
291 */
292 function get_rid_and_months(field_el, data) {
293 var rid = 1;
294 var months = 1;
295
296 // NEW: prefer dataset on the wrap (Inspector edits land here first)
297 var wrap = field_el ? (field_el.closest && field_el.closest('.wpbc_calendar_wraper')) || field_el : null;
298 if (wrap && wrap.dataset) {
299 if (wrap.dataset.resource_id != null && wrap.dataset.resource_id !== '') {
300 rid = Number(wrap.dataset.resource_id);
301 }
302 if (wrap.dataset.months != null && wrap.dataset.months !== '') {
303 months = Number(wrap.dataset.months);
304 }
305 }
306
307 if ( data && data.resource_id != null ) {
308 rid = Number( data.resource_id );
309 }
310 if ( data && data.months != null ) {
311 months = Number( data.months );
312 }
313
314 if ( !data ) {
315 // Fallbacks from DOM when data object is not provided.
316 var n = field_el ? field_el.querySelector( '[id^="calendar_booking"]' ) : null;
317 if ( n && n.id ) {
318 var m1 = n.id.match( /calendar_booking(\d+)/ );
319 if ( m1 ) {
320 rid = Number( m1[1] );
321 }
322 }
323 var cont = field_el ? field_el.querySelector( '.wpbc_cal_container' ) : null;
324 if ( cont && cont.className ) {
325 var m2 = cont.className.match( /cal_month_num_(\d+)/ );
326 if ( m2 ) {
327 months = Number( m2[1] );
328 }
329 }
330 }
331
332 // FixIn: 2026-03-07 Override to always get the default booking resource, for specific user in MU or for existed resourecs !
333 rid = resolve_effective_resource_id( rid );
334
335 months = isFinite( months ) ? Math.max( 1, Math.min( 12, Math.floor( months ) ) ) : 1;
336
337 return { rid: rid, months: months };
338 }
339
340 // -----------------------------------------------------------------------------------------------------------------
341 // Minimal preview bootstrap — load/refresh a calendar for a field
342 // -----------------------------------------------------------------------------------------------------------------
343
344
345 /**
346 * Initialize (or update) a calendar preview for a given field element.
347 *
348 * @param {Element} field_el - calendar field element (wrap)
349 * @param {object} data - builder field data (optional)
350 * @param {boolean} should_reload_data - force AJAX data reload
351 */
352 function init_field(field_el, data, should_reload_data = true) {
353 if ( !field_el ) {
354 return;
355 }
356
357 var pair = get_rid_and_months( field_el, data );
358 var rid = pair.rid;
359
360
361 wait_until_api_ready( function () {
362
363 // 1) Always (re)apply local UI — needed after DOM moves.
364 apply_months_class( field_el, pair.months );
365 set_calendar_params( rid, pair.months );
366
367 try {
368 w.wpbc_calendar_show( String( rid ) );
369 } catch ( e1 ) {
370 dev.error( 'wpbc_calendar_show', e1 );
371 }
372 set_secure_params();
373
374 // 2) Decide on AJAX strictly by RID state + explicit request.
375 var first_time_for_rid = !rid_loaded_cache[rid];
376 // Respect caller intent. Reload only if explicitly asked OR first time for this RID.
377 var do_reload = !!should_reload_data || first_time_for_rid;
378
379 try {
380 if ( typeof w.wpbc_calendar__load_data__ajx === 'function' ) {
381 if ( do_reload ) {
382 w.wpbc_calendar__load_data__ajx( {
383 'resource_id' : rid,
384 'booking_hash' : '',
385 'request_uri' : Boot.request_uri || (w.location ? String( w.location.pathname + w.location.search ) : ''),
386 'custom_form' : 'standard',
387 'aggregate_resource_id_str': '',
388 'aggregate_type' : 'all'
389 } );
390 // Mark this rid as loaded so subsequent DOM churn uses look-only refresh.
391 rid_loaded_cache[rid] = true;
392 } else if ( typeof w.wpbc_calendar__update_look === 'function' ) {
393 w.wpbc_calendar__update_look( rid );
394 }
395 }
396 } catch ( e2 ) {
397 dev.log( 'calendar_data_load_skip', e2 );
398 }
399
400 // Track last config for soft dedupe (UI churn); keep per-element.
401 try { field_el.setAttribute('data-wpbc-cal-init', '1'); field_el.setAttribute('data-wpbc-cal-loaded-rid', String(rid)); } catch (e3) {}
402 try { apply_calendar_legend_preview_from_controls(); } catch (e4) {}
403
404 }, 40, 100 );
405 }
406
407
408 // -----------------------------------------------------------------------------------------------------------------
409 // FOCS: tiny bus & DOM helpers (snake format)
410 // -----------------------------------------------------------------------------------------------------------------
411 const defer_fn = (fn) => (typeof w.requestAnimationFrame === 'function' ? w.requestAnimationFrame( fn ) : setTimeout( fn, 0 ));
412
413 function find_calendar_wrap(el) {
414 if ( ! el ) {
415 return null;
416 }
417 return (el.closest && el.closest( '.wpbc_calendar_wraper' )) || el;
418 }
419
420 function is_calendar_wrap(el) {
421 var wrap = find_calendar_wrap( el );
422 return !!(wrap && (
423 (wrap.dataset && wrap.dataset.type === 'calendar') ||
424 (wrap.querySelector && wrap.querySelector( '[id^="calendar_booking"]' ))
425 ));
426 }
427
428 function look_refresh(target, opts) {
429 var o = opts || {};
430 var wrap = find_calendar_wrap( target );
431 if ( ! wrap || ! document.contains( wrap ) ) {
432 return;
433 }
434 defer_fn( function () {
435 init_field( wrap, null, !!o.reload );
436 } );
437 }
438
439 function on_event(type, handler) {
440 document.addEventListener( type, handler );
441 }
442
443 function get_calendar_skin_url_from_select(select_el) {
444 if ( ! select_el || ! select_el.options ) {
445 return '';
446 }
447
448 var selected_option = select_el.options[ select_el.selectedIndex ];
449 if ( selected_option ) {
450 return String( selected_option.getAttribute( 'data-wpbc-calendar-skin-url' ) || select_el.value || '' );
451 }
452
453 return String( select_el.value || '' );
454 }
455
456 function apply_calendar_skin_from_select(select_el) {
457 var skin_url = get_calendar_skin_url_from_select( select_el );
458 if ( ! skin_url || typeof w.wpbc__calendar__change_skin !== 'function' ) {
459 return;
460 }
461
462 try {
463 w.wpbc__calendar__change_skin( skin_url );
464 } catch ( e ) {
465 dev.error( 'calendar_skin_preview', e );
466 }
467 }
468
469 function bind_calendar_skin_preview_events() {
470 document.addEventListener( 'change', function (e) {
471 var target = e && e.target;
472 if ( target && target.matches && target.matches( '.js-wpbc-bfb-calendar-skin' ) ) {
473 apply_calendar_skin_from_select( target );
474 }
475 }, true );
476 }
477
478 function escape_html(s) {
479 return String( s == null ? '' : s )
480 .replace( /&/g, '&amp;' )
481 .replace( /</g, '&lt;' )
482 .replace( />/g, '&gt;' )
483 .replace( /"/g, '&quot;' )
484 .replace( /'/g, '&#039;' );
485 }
486
487 var legend_item_option_map = {
488 available : { show: 'booking_legend_is_show_item_available', text: 'booking_legend_text_for_item_available' },
489 pending : { show: 'booking_legend_is_show_item_pending', text: 'booking_legend_text_for_item_pending' },
490 approved : { show: 'booking_legend_is_show_item_approved', text: 'booking_legend_text_for_item_approved' },
491 partially : { show: 'booking_legend_is_show_item_partially', text: 'booking_legend_text_for_item_partially' },
492 unavailable: { show: 'booking_legend_is_show_item_unavailable', text: 'booking_legend_text_for_item_unavailable' }
493 };
494
495 function get_checkbox_value_from_id(id, fallback) {
496 var el = document.getElementById( id );
497 if ( el ) {
498 return el.checked ? 'On' : 'Off';
499 }
500 return fallback === 'On' ? 'On' : 'Off';
501 }
502
503 function get_text_value_from_id(id, fallback) {
504 var el = document.getElementById( id );
505 if ( el ) {
506 return String( el.value || '' );
507 }
508 return String( fallback || '' );
509 }
510
511 function get_calendar_legend_values_from_controls() {
512 var boot_legend = Boot.calendar_legend || {};
513 var boot_items = boot_legend.items || {};
514 var values = {
515 show_legend : get_checkbox_value_from_id( 'booking_is_show_legend', boot_legend.show_legend ),
516 show_numbers: get_checkbox_value_from_id( 'booking_legend_is_show_numbers', boot_legend.show_numbers ),
517 is_vertical : get_checkbox_value_from_id( 'booking_legend_is_vertical', boot_legend.is_vertical ),
518 items : {}
519 };
520
521 Object.keys( legend_item_option_map ).forEach( function (item_key) {
522 var map = legend_item_option_map[item_key];
523 var boot_item = boot_items[item_key] || {};
524 values.items[item_key] = {
525 show : get_checkbox_value_from_id( map.show, boot_item.show ),
526 title: get_text_value_from_id( map.text, boot_item.title || boot_item.placeholder || '' )
527 };
528 } );
529
530 return values;
531 }
532
533 function extract_legend_item_html(template_html) {
534 var tmp = document.createElement( 'div' );
535 tmp.innerHTML = String( template_html || '' );
536 var item = tmp.querySelector( '.wpdev_hint_with_text' );
537 return item ? item.outerHTML : '';
538 }
539
540 function render_calendar_legend_html(values) {
541 var boot_legend = Boot.calendar_legend || {};
542 var boot_items = boot_legend.items || {};
543 var order = Array.isArray( boot_legend.items_order ) ? boot_legend.items_order : Object.keys( legend_item_option_map );
544 var html = '';
545
546 if ( ! values || values.show_legend !== 'On' ) {
547 return '';
548 }
549
550 order.forEach( function (item_key) {
551 var item_values = values.items[item_key] || {};
552 var boot_item = boot_items[item_key] || {};
553 var template = (values.show_numbers === 'On') ? boot_item.template_number : boot_item.template_blank;
554 var item_html = extract_legend_item_html( template );
555
556 if ( item_values.show !== 'On' || ! item_html ) {
557 return;
558 }
559
560 item_html = item_html.replace( /__WPBC_LEGEND_TITLE__/g, escape_html( item_values.title || boot_item.placeholder || '' ) );
561 html += item_html;
562 } );
563
564 if ( ! html ) {
565 return '';
566 }
567
568 return '<div class="block_hints datepick ' + (values.is_vertical === 'On' ? ' block_hints_vertical ' : '') + '">' + html + '</div>';
569 }
570
571 function store_calendar_legend_values_in_boot(values) {
572 Boot.calendar_legend = Boot.calendar_legend || {};
573 Boot.calendar_legend.items = Boot.calendar_legend.items || {};
574 Boot.calendar_legend.show_legend = values.show_legend;
575 Boot.calendar_legend.show_numbers = values.show_numbers;
576 Boot.calendar_legend.is_vertical = values.is_vertical;
577
578 Object.keys( values.items || {} ).forEach( function (item_key) {
579 Boot.calendar_legend.items[item_key] = Boot.calendar_legend.items[item_key] || {};
580 Boot.calendar_legend.items[item_key].show = values.items[item_key].show;
581 Boot.calendar_legend.items[item_key].title = values.items[item_key].title;
582 } );
583 }
584
585 function apply_calendar_legend_preview_from_controls() {
586 var values = get_calendar_legend_values_from_controls();
587 var html = render_calendar_legend_html( values );
588 var nodes = document.querySelectorAll( '.js-wpbc-bfb-calendar-legend-preview' );
589
590 store_calendar_legend_values_in_boot( values );
591
592 for ( var i = 0; i < nodes.length; i++ ) {
593 nodes[i].innerHTML = html;
594 }
595 }
596
597 function sync_calendar_legend_inspector_visibility() {
598 var main = document.getElementById( 'booking_is_show_legend' );
599 var options = document.querySelector( '.js-wpbc-bfb-calendar-legend-options' );
600 if ( ! main || ! options ) {
601 return;
602 }
603 options.style.display = main.checked ? '' : 'none';
604 }
605
606 function bind_calendar_legend_preview_events() {
607 function on_inspector_render() {
608 sync_calendar_legend_inspector_visibility();
609 }
610
611 document.addEventListener( 'change', function (e) {
612 var target = e && e.target;
613 if ( target && target.matches && target.matches( '.js-wpbc-bfb-calendar-legend-control' ) ) {
614 sync_calendar_legend_inspector_visibility();
615 apply_calendar_legend_preview_from_controls();
616 }
617 }, true );
618
619 document.addEventListener( 'input', debounce( function (e) {
620 var target = e && e.target;
621 if ( target && target.matches && target.matches( '.js-wpbc-bfb-calendar-legend-control' ) ) {
622 apply_calendar_legend_preview_from_controls();
623 }
624 }, 150 ), true );
625
626 document.addEventListener( 'wpbc_bfb_inspector_ready', on_inspector_render );
627 document.addEventListener( 'wpbc_bfb_inspector_render', on_inspector_render );
628 }
629
630 /**
631 * Initialize all calendars present in the current Builder preview panel.
632 */
633 function init_all_on_page(should_reload_data = true) {
634 var scope = w.document.querySelector( '#wpbc_bfb__pages_panel' ) || w.document;
635 var nodes = scope.querySelectorAll( '[id^="calendar_booking"]' );
636 for ( var i = 0; i < nodes.length; i++ ) {
637 var node = nodes[i];
638 var field_el = node.closest( '.wpbc_calendar_wraper' ) || node.parentElement || node;
639 init_field( field_el, null, should_reload_data );
640 }
641 }
642
643
644 /**
645 * Listen to builder’s bus (events bubble on document via Core.WPBC_BFB_EventBus)
646 */
647 function bind_builder_bus_events() {
648
649 // Alias events once.
650 var EV = Core.WPBC_BFB_Events || {};
651
652 // 1) First structure ready -> look-only render.
653 on_event( EV.STRUCTURE_LOADED, function () {
654 init_all_on_page( false );
655 } );
656
657 // 2) Field/section added -> initialize that node if it’s a calendar (force reload).
658 on_event( EV.FIELD_ADD, function (e) {
659 var el = e && e.detail && e.detail.el;
660 if ( ! el ) {
661 // Defensive fallback: if emitter didn’t provide the element, refresh all calendars (look-only).
662 return defer_fn(function () { init_all_on_page(false); });
663 }
664 if ( is_calendar_wrap( el ) ) {
665 look_refresh( el, { reload: true } ); // builder just inserted; So do Ajax fetch data.
666 }
667 } );
668
669
670 // 3) Generic structure changes.
671 on_event( EV.STRUCTURE_CHANGE, function (e) {
672 var d = (e && e.detail) || {};
673 var reason = d.reason || '';
674
675 // Heavy operations -> cheap look refresh for all calendars, no data reload.
676 if ( reason === 'sort-update' || reason === 'section-move' || reason === 'delete' ) {
677 return defer_fn( function () {
678 init_all_on_page( false );
679 } );
680 }
681
682 // Only care about calendar targets.
683 if ( ! is_calendar_wrap( d.el ) ) return;
684
685 // Only reload data on committed resource_id changes.
686 var k = d.key || '';
687 var phase = (d.phase || '').toLowerCase(); // set by the emitter above.
688 if ( k === 'resource_id' && phase !== 'change' ) {
689 return; // Skip on second input handler: ins.addEventListener( 'input', handler, true ); in ../includes/page-form-builder/__js/core/bfb-ui.js //.
690 }
691 // Replace to TRUE, if needs to FORCE ajax reload of calendar data of resource ID change.
692 var must_reload = (k === 'resource_id' && phase === 'change')
693 ? false
694 : false;
695 look_refresh( d.el, { reload: must_reload } );
696 } );
697 }
698
699 // -----------------------------------------------------------------------------------------------------------------
700 // Field Renderer (class-like, extendable)
701 // -----------------------------------------------------------------------------------------------------------------
702 class WPBC_BFB_Field_Calendar extends (Field_Base || class {}) {
703
704 static template_id = 'wpbc-bfb-field-calendar'; // Underscore template id from PHP printer.
705 static kind = 'calendar';
706
707 /**
708 * Default props — keep in sync with PHP schema defaults.
709 */
710 static get_defaults() {
711 return {
712 type : 'calendar',
713 label : 'Select Date',
714 resource_id: resolve_effective_resource_id( 1 ),
715 months : 1,
716 name : '',
717 html_id : '',
718 cssclass : '',
719 help : '',
720 min_width : '250px'
721 };
722 }
723
724 /**
725 * Called by the Builder after the field is dropped/loaded/previewed.
726 * We (re)initialize the preview for the specific element.
727 *
728 * @param {object} data
729 * @param {Element} field_el
730 * @param {{context:string}} ctx
731 */
732 static on_field_drop(data, field_el, ctx) {
733 try {
734 init_field( field_el, data, false );
735 } catch ( e ) {
736 dev.error( 'WPBC_BFB_Field_Calendar.on_field_drop', e );
737 }
738 }
739
740 /**
741 * Hydrate after preview render (no rebuild). Called by builder.render_preview().
742 */
743 static hydrate(field_el, data, ctx) {
744 try {
745 init_field( field_el, data, false );
746 } catch ( e ) {
747 dev.error( 'WPBC_BFB_Field_Calendar.hydrate', e );
748 }
749 }
750
751 }
752
753 // Register pack renderer with the central registry.
754 try {
755 Registry.register( 'calendar', WPBC_BFB_Field_Calendar );
756 } catch ( e ) {
757 dev.error( 'WPBC_BFB_Field_Calendar.register', e );
758 }
759
760 // Bootstrap: on DOM ready, run a first scan and wire light reactivity.
761 function on_ready(fn) {
762 if ( w.document.readyState === 'interactive' || w.document.readyState === 'complete' ) {
763 try {
764 fn();
765 } catch ( e ) {
766 }
767 } else {
768 w.document.addEventListener( 'DOMContentLoaded', function () {
769 try {
770 fn();
771 } catch ( e ) {
772 }
773 } );
774 }
775 }
776
777 on_ready( function () {
778 setTimeout( function () {
779 init_all_on_page( false );
780 bind_builder_bus_events();
781 bind_calendar_skin_preview_events();
782 bind_calendar_legend_preview_events();
783 sync_calendar_legend_inspector_visibility();
784 }, 0 );
785 } );
786
787 // Optional export (handy for debugging).
788 w.WPBC_BFB_Field_Calendar = WPBC_BFB_Field_Calendar;
789
790
791 // -- Export for "Booking Form" ------------------------------------------------------------------------------------
792
793 /**
794 * Register the "calendar" exporter (lazy: tries now, or waits for exporter-ready).
795 * Output:
796 * • [calendar] only (no rid/months/class/id tokens inside)
797 * • If html_id / cssclass set → wrap shortcode in <span ... style="flex:1;">…</span>
798 * • Label above (when addLabels !== false).
799 * Help text is appended by WPBC_BFB_Exporter.render_field_node().
800 *
801 * Booking Form exporter callback (Advanced Form shortcode).
802 *
803 * This callback is registered per field type via:
804 * WPBC_BFB_Exporter.register( 'shortcode_name', callback )
805 *
806 * Core call site (builder-exporter.js):
807 * WPBC_BFB_Exporter.run_registered_exporter( field, io, cfg, once, ctx )
808 * → callback( field, emit, { io, cfg, once, ctx, core } );
809 *
810 * @callback WPBC_BFB_ExporterCallback
811 * @param {Object} field
812 * Normalized field data coming from the Builder structure.
813 * - field.type {string} Field type, e.g. "text".
814 * - field.name {string} Name as stored on the canvas (already validated).
815 * - field.id / html_id {string} Optional HTML id / user-visible id.
816 * - field.label {string} Visible label in the form (may be empty).
817 * - field.placeholder {string} Placeholder text (may be empty).
818 * - field.required {boolean|number|string} "truthy" if required.
819 * - field.cssclass {string} Extra CSS classes entered in Inspector.
820 * - field.default_value {string} Default text value.
821 * - field.options {Array} Only for option-based fields (select, checkbox, etc.).
822 * - ... (Any other pack-specific props are also present.)
823 *
824 * @param {function(string):void} emit
825 * Emits one line/fragment into the export buffer.
826 * - Each call corresponds to one `push()` in the core exporter.
827 * - For multi-line output (e.g. label + shortcode), call `emit()` multiple times:
828 * emit('<l>Label</l>');
829 * emit('<br>[text* name ...]');
830 *
831 * @param {Object} [extras]
832 * Extra context passed by the core exporter.
833 *
834 * @param {Object} [extras.io]
835 * Low-level writer used internally by the core.
836 * Normally you do NOT need it in packs — prefer `emit()`.
837 * - extras.io.open(str) → open a nested block (increments indentation).
838 * - extras.io.close(str) → close a block (decrements indentation).
839 * - extras.io.push(str) → push raw line (used by `emit()`).
840 * - extras.io.blank() → push an empty line.
841 *
842 * @param {Object} [extras.cfg]
843 * Export configuration (same object passed to WPBC_BFB_Exporter.export_form()).
844 * Useful flags for field packs:
845 * - extras.cfg.addLabels {boolean} Default: true.
846 * If false, packs should NOT emit <l>Label</l> lines.
847 * - extras.cfg.newline {string} Newline separator (usually "\n").
848 * - extras.cfg.gapPercent{number} Layout gap (used only by section/column logic).
849 *
850 * @param {Object} [extras.once]
851 * Shared "once-per-form" guards across all fields.
852 * Counters are incremented by some field types (captcha, coupon, etc.).
853 * Typical shape:
854 * - extras.once.captcha {number}
855 * - extras.once.country {number}
856 * - extras.once.coupon {number}
857 * - extras.once.cost_corrections {number}
858 * - extras.once.submit {number}
859 *
860 * Text field usually does not touch this object, but other packs can use it
861 * to skip duplicates (e.g. only the first [coupon] per form is exported).
862 *
863 * @param {Object} [extras.ctx]
864 * Shared export context for the entire form.
865 * Currently:
866 * - extras.ctx.usedIds {Set<string>}
867 * Set of HTML/shortcode IDs already used in this export.
868 * Helpers like Exp.id_option(field, ctx) use it to ensure uniqueness.
869 *
870 * Packs normally just pass `ctx` into helpers (id_option, etc.) without
871 * mutating it directly.
872 *
873 * @param {Object} [extras.core]
874 * Reference to WPBC_BFB_Core passed from builder-exporter.js.
875 * Primarily used to access sanitizers:
876 * - extras.core.WPBC_BFB_Sanitize.escape_html(...)
877 * - extras.core.WPBC_BFB_Sanitize.escape_for_shortcode(...)
878 * - extras.core.WPBC_BFB_Sanitize.sanitize_html_name(...)
879 * - etc.
880 */
881 function export_shortcode_in_booking_form() {
882
883 const Exp = w.WPBC_BFB_Exporter;
884 if ( ! Exp || typeof Exp.register !== 'function' ) { return false; }
885 if ( typeof Exp.has_exporter === 'function' && Exp.has_exporter( 'calendar' ) ) { return true; }
886
887 // Use sanitize helpers from core (already loaded).
888 const S = Core.WPBC_BFB_Sanitize || (w.WPBC_BFB_Core && w.WPBC_BFB_Core.WPBC_BFB_Sanitize) || {};
889 const esc = S.escape_html || (v => String( v ));
890 const sid = S.sanitize_html_id || (v => String( v ));
891 const scls = S.sanitize_css_classlist || (v => String( v ));
892
893 /**
894 * Per-field exporter for "calendar" in Advanced Form.
895 * @type {WPBC_BFB_ExporterCallback}
896 */
897 Exp.register( 'calendar', (field, emit, extras = {}) => {
898
899 const cfg = extras.cfg || {};
900 const ctx = extras.ctx;
901 const usedIds = (ctx && ctx.usedIds instanceof Set) ? ctx.usedIds : null;
902 const addLabels = cfg.addLabels !== false;
903
904 // Optional wrapper attrs (id/class on outer span, not inside [calendar]).
905 let html_id = field && field.html_id ? sid( String( field.html_id ) ) : '';
906 if ( html_id && usedIds ) {
907 let u = html_id, i = 2;
908 while ( usedIds.has( u ) ) {
909 u = `${html_id}_${i++}`;
910 }
911 usedIds.add( u );
912 html_id = u;
913 }
914
915 const cls_raw = field && (field.cssclass_extra || field.cssclass || field.class) || '';
916 const cls = scls( String( cls_raw ) );
917
918 const hasWrap = !!( html_id || cls );
919 const wrapOpen = hasWrap
920 ? `<span${html_id ? ` id="${esc( html_id )}"` : ''}${cls ? ` class="${esc( cls )}"` : ''} style="flex:1;">`
921 : '';
922 const wrapClose = hasWrap ? '</span>' : '';
923
924 // Calendar body is intentionally minimal; no rid/months/id/class tokens inside shortcode.
925 const body = '[calendar]';
926 const label = (typeof field?.label === 'string') ? field.label.trim() : '';
927
928 if ( label && addLabels ) {
929 emit( `<l>${esc( label )}</l>` );
930 emit( `<br>${wrapOpen}${body}${wrapClose}` );
931 } else {
932 emit( `${wrapOpen}${body}${wrapClose}` );
933 }
934 } );
935
936 return true;
937 }
938
939 // Try now; if exporter isn't ready yet, wait for one-shot event from builder-exporter.
940 if ( ! export_shortcode_in_booking_form() ) {
941 document.addEventListener( 'wpbc:bfb:exporter-ready', export_shortcode_in_booking_form, { once: true } );
942 }
943
944 // -- Export for "Booking Data" ------------------------------------------------------------------------------------
945
946 /**
947 * Register the "calendar" exporter for "Content of booking fields data". Produces e.g.: "<b>Dates</b>:
948 * <f>[dates]</f><br>"
949 *
950 * Booking Data exporter callback ("Content of booking fields data"). Default output: <b>Label</b>:
951 * <f>[field_name]</f><br>
952 *
953 * Registered per field type via:
954 * WPBC_BFB_ContentExporter.register( 'shortcode_name', callback )
955 *
956 * Core call site (builder-exporter.js):
957 * WPBC_BFB_ContentExporter.run_registered_exporter( field, emit, { cfg, core } );
958 *
959 * @callback WPBC_BFB_ContentExporterCallback
960 * @param {Object} field
961 * Normalized field data (same shape as in the main exporter).
962 * Important properties for content templates:
963 * - field.type {string} Field type, e.g. "text".
964 * - field.name {string} Field name used as placeholder token.
965 * - field.label {string} Human-readable label (may be empty).
966 * - field.options {Array} For option-based fields (select, checkbox, radio, etc.).
967 * - Other pack-specific props if needed.
968 *
969 * @param {function(string):void} emit
970 * Emits a raw HTML fragment into the "Content of booking fields data" template.
971 * Core will wrap everything once into:
972 * <div class="standard-content-form">
973 * ... emitted fragments ...
974 * </div>
975 *
976 * Typical usage pattern:
977 * emit('<b>Label</b>: <f>[field_name]</f><br>');
978 *
979 * In most cases, packs call the shared helper:
980 * WPBC_BFB_ContentExporter.emit_line_bold_field(emit, label, token, cfg);
981 *
982 * @param {Object} [extras]
983 * Additional context passed from run_registered_exporter().
984 *
985 * @param {Object} [extras.cfg]
986 * Content exporter configuration:
987 * - extras.cfg.addLabels {boolean} Default: true.
988 * If false, helper may omit the bold label part.
989 * - extras.cfg.sep {string} Label separator, default ": ".
990 * Example: "<b>Label</b>: " vs "<b>Label</b> – ".
991 * - extras.cfg.newline {string} Newline separator when joining lines (usually "\n").
992 *
993 * @param {Object} [extras.core]
994 * Reference to WPBC_BFB_Core (same as in main exporter).
995 * Usually not needed here, because:
996 * - Sanitization and consistent rendering are already done via
997 * WPBC_BFB_ContentExporter.emit_line_bold_field( ... ).
998 */
999 function export_shortcode_in_booking_data() {
1000
1001 var C = w.WPBC_BFB_ContentExporter;
1002 if ( ! C || typeof C.register !== 'function' ) { return false; }
1003 if ( typeof C.has_exporter === 'function' && C.has_exporter( 'calendar' ) ) { return true; }
1004
1005 C.register( 'calendar', function ( field, emit, extras ) {
1006
1007 extras = extras || {};
1008 var cfg = extras.cfg || {};
1009 var label = (typeof field.label === 'string' && field.label.trim()) ? field.label.trim() : 'Dates';
1010
1011 // Reuse shared formatter from builder-exporter - e.g.: emit_line_bold_field(emit, label, token, cfg) -> emit(`<b>${S.escape_html(label)}</b>${sep}<f>[${token}]</f><br>`); .
1012 // C.emit_line_bold_field( emit, label, 'dates', cfg );
1013
1014 if(0) {
1015 // Defensive fallback: keep a simple, backward-compatible output. Just for help in using in other field packs.
1016 var core_local = extras.core || Core || {};
1017 var S_local = core_local.WPBC_BFB_Sanitize || {};
1018 var esc = S_local.escape_html || function (s) { return String( s ); };
1019
1020 var sep = (cfg && typeof cfg.sep === 'string') ? cfg.sep : ': ';
1021 var title = label ? '<b>' + esc( label ) + '</b>' + sep : '';
1022 emit( title + '<f>[dates]</f><br>' );
1023 }
1024 } );
1025
1026 return true;
1027 }
1028
1029 if ( ! export_shortcode_in_booking_data() ) {
1030 document.addEventListener( 'wpbc:bfb:content-exporter-ready', export_shortcode_in_booking_data, { once: true } );
1031 }
1032
1033 })( window );
1034