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-setup-wizard / step-date-selection / _src / step-date-selection.js

step-date-selection.js in Booking Calendar 11.9, at includes/page-setup-wizard/step-date-selection/_src/step-date-selection.js

1,100 lines 41.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Drive the Setup Wizard Date Selection editor and real calendar preview.
3 *
4 * Canonical Booking Calendar helpers remain responsible for selection rules.
5 * This adapter changes only request-local calendar parameters and draft fields;
6 * it does not save settings or submit a booking.
7 *
8 * @package Booking Calendar
9 */
10 ( function ( window, document ) {
11 'use strict';
12
13 var module_config = window.wpbc_setup_wizard_date_selection || { i18n: {} };
14
15 /**
16 * Create one isolated Date Selection step adapter.
17 *
18 * @param {Object} config Localized limits and messages.
19 * @return {Object} Adapter accepted by the shared wizard shell.
20 */
21 function create_date_selection_adapter( config ) {
22 var shell_api = null;
23 var editor = null;
24 var apply_timers = [];
25 var preview_data_timer = null;
26 var last_selected_mode = '';
27
28 /**
29 * Return one localized message with a safe fallback.
30 *
31 * @param {string} key Translation key.
32 * @param {string} fallback Fallback message.
33 * @return {string} Translation or fallback.
34 */
35 function get_message( key, fallback ) {
36 return config.i18n && config.i18n[ key ] ? String( config.i18n[ key ] ) : fallback;
37 }
38
39 /**
40 * Return the selected mode identifier.
41 *
42 * @return {string} Selected mode or an empty string.
43 */
44 function get_selected_mode() {
45 var selected = get_selected_mode_control();
46
47 return selected ? selected.value : '';
48 }
49
50 /**
51 * Return the selected server-authorized mode control.
52 *
53 * @return {HTMLElement|null} Selected radio control or null.
54 */
55 function get_selected_mode_control() {
56 return editor ? editor.node.querySelector( '[data-wpbc-date-selection-mode]:checked' ) : null;
57 }
58
59 /**
60 * Read one dependent-setting state from the selected mode policy.
61 *
62 * @param {string} setting_name Policy setting name in camel case.
63 * @return {string} Policy state or `hidden_off`.
64 */
65 function get_mode_policy_state( setting_name ) {
66 var selected_mode_control = get_selected_mode_control();
67
68 if ( ! selected_mode_control || ! selected_mode_control.dataset[ setting_name ] ) {
69 return 'hidden_off';
70 }
71
72 return String( selected_mode_control.dataset[ setting_name ] );
73 }
74
75 /**
76 * Apply defaults only when the customer changes the Date Selection mode.
77 *
78 * Time-based Customer Journeys default recurrent selected-time behavior to
79 * on for Multiple days, while leaving the visible option editable afterward.
80 * Tracking the previous mode prevents later synchronization from overwriting
81 * an explicit customer choice.
82 *
83 * @param {HTMLElement|null} mode_control Changed mode radio control.
84 * @return {void}
85 */
86 function apply_selected_mode_defaults( mode_control ) {
87 var selected_mode;
88 var recurrent_policy;
89 var recurrent_toggle;
90
91 if ( ! editor || ! mode_control || ! mode_control.checked ) {
92 return;
93 }
94
95 selected_mode = String( mode_control.value || '' );
96 if ( selected_mode === last_selected_mode ) {
97 return;
98 }
99 last_selected_mode = selected_mode;
100 recurrent_policy = String( mode_control.dataset.wpbcDateSelectionPolicyRecurrentTime || '' );
101 if ( 'optional_default_on' !== recurrent_policy ) {
102 return;
103 }
104
105 recurrent_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_recurrent_time"]' );
106 if ( recurrent_toggle ) {
107 recurrent_toggle.checked = true;
108 }
109 }
110
111 /**
112 * Parse one bounded integer control.
113 *
114 * @param {string} selector Control selector below the editor.
115 * @param {number} fallback Value used for an absent or invalid control.
116 * @return {number} Parsed integer.
117 */
118 function get_integer( selector, fallback ) {
119 var control = editor ? editor.node.querySelector( selector ) : null;
120 var parsed = control ? parseInt( control.value, 10 ) : NaN;
121
122 return isNaN( parsed ) ? fallback : parsed;
123 }
124
125 /**
126 * Convert one weekday transport field to the canonical number array.
127 *
128 * @param {string} mode Range mode owning the transport field.
129 * @return {number[]} Weekday numbers or the unrestricted marker.
130 */
131 function get_weekdays( mode ) {
132 var transport = editor ? editor.node.querySelector( '[data-wpbc-date-selection-weekday-transport="' + mode + '"]' ) : null;
133 var values = transport ? String( transport.value || '' ).split( ',' ) : [];
134 var weekdays = values.map( function ( value ) {
135 return parseInt( value, 10 );
136 } ).filter( function ( value ) {
137 return -1 === value || ( value >= 0 && value <= 6 );
138 } );
139
140 return weekdays.length ? weekdays : [ -1 ];
141 }
142
143 /**
144 * Parse the released comma-and-range syntax into canonical day lengths.
145 *
146 * This intentionally mirrors the server validator instead of evaluating
147 * input text. An empty expression means that every length within the
148 * configured minimum and maximum is allowed.
149 *
150 * @param {string} raw_specific_days Untrusted text field value.
151 * @return {number[]|null} Sorted unique lengths, an empty array, or null when invalid.
152 */
153 function parse_specific_days( raw_specific_days ) {
154 var maximum_days = parseInt( config.max_dynamic_days || 1095, 10 );
155 var specific_expression = String( raw_specific_days || '' ).trim();
156 var specific_days = [];
157
158 if ( '' === specific_expression ) {
159 return specific_days;
160 }
161 if (
162 specific_expression.length > 500
163 || ! /^\d+(?:\s*-\s*\d+)?(?:\s*,\s*\d+(?:\s*-\s*\d+)?)*$/.test( specific_expression )
164 ) {
165 return null;
166 }
167
168 specific_expression.split( ',' ).forEach( function ( specific_part ) {
169 var range_bounds = specific_part.trim().split( /\s*-\s*/ ).map( function ( range_bound ) {
170 return parseInt( range_bound, 10 );
171 } );
172 var range_start = Math.min.apply( null, range_bounds );
173 var range_end = Math.max.apply( null, range_bounds );
174 var specific_day;
175
176 if ( null === specific_days ) {
177 return;
178 }
179
180 if ( range_start < 1 || range_end > maximum_days ) {
181 specific_days = null;
182 return;
183 }
184
185 for ( specific_day = range_start; specific_day <= range_end; specific_day++ ) {
186 if ( -1 === specific_days.indexOf( specific_day ) ) {
187 specific_days.push( specific_day );
188 }
189 }
190 } );
191
192 return null === specific_days ? null : specific_days.sort( function ( first_day, second_day ) {
193 return first_day - second_day;
194 } );
195 }
196
197 /**
198 * Return the specific flexible-range lengths from the current editor.
199 *
200 * @return {number[]|null} Canonical lengths, an empty array, or null when invalid.
201 */
202 function get_specific_days() {
203 var control = editor ? editor.node.querySelector( '[data-wpbc-date-selection-specific]' ) : null;
204
205 return parse_specific_days( control ? control.value : '' );
206 }
207
208 /**
209 * Write visible weekday button state into its scalar transport field.
210 *
211 * @param {string} mode Range mode owning the controls.
212 * @return {void}
213 */
214 function sync_weekday_transport( mode ) {
215 var group = editor ? editor.node.querySelector( '[data-wpbc-date-selection-weekdays="' + mode + '"]' ) : null;
216 var transport = editor ? editor.node.querySelector( '[data-wpbc-date-selection-weekday-transport="' + mode + '"]' ) : null;
217 var selected = [];
218
219 if ( ! group || ! transport ) {
220 return;
221 }
222
223 group.querySelectorAll( '[data-weekday][aria-pressed="true"]' ).forEach( function ( button ) {
224 selected.push( parseInt( button.dataset.weekday, 10 ) );
225 } );
226 selected = selected.filter( function ( value ) {
227 return ! isNaN( value ) && value >= 0 && value <= 6;
228 } ).sort( function ( first_value, second_value ) {
229 return first_value - second_value;
230 } );
231
232 transport.value = 7 === selected.length || 0 === selected.length ? '-1' : selected.join( ',' );
233 }
234
235 /**
236 * Apply a canonical weekday preset to the visible buttons and transport.
237 *
238 * The `-1` unrestricted marker is presented as all seven weekdays selected
239 * so the allowed-start-days state remains explicit to sighted users.
240 *
241 * @param {string} mode Range mode owning the controls.
242 * @param {string} raw_weekdays Canonical comma list or `-1` for any day.
243 * @return {void}
244 */
245 function apply_weekday_preset( mode, raw_weekdays ) {
246 var group = editor ? editor.node.querySelector( '[data-wpbc-date-selection-weekdays="' + mode + '"]' ) : null;
247 var weekdays = '-1' === raw_weekdays ? [ 0, 1, 2, 3, 4, 5, 6 ] : String( raw_weekdays || '' ).split( ',' ).map( function ( raw_weekday ) {
248 return parseInt( raw_weekday, 10 );
249 } ).filter( function ( weekday ) {
250 return ! isNaN( weekday ) && weekday >= 0 && weekday <= 6;
251 } );
252
253 if ( ! group ) {
254 return;
255 }
256
257 group.querySelectorAll( '[data-weekday]' ).forEach( function ( button ) {
258 var is_selected = -1 !== weekdays.indexOf( parseInt( button.dataset.weekday, 10 ) );
259
260 button.setAttribute( 'aria-pressed', is_selected ? 'true' : 'false' );
261 button.classList.toggle( 'is-selected', is_selected );
262 } );
263 sync_weekday_transport( mode );
264 }
265
266 /**
267 * Synchronize visible switches into their allow-listed hidden fields.
268 *
269 * @return {void}
270 */
271 function sync_toggle_transports() {
272 if ( ! editor ) {
273 return;
274 }
275
276 editor.node.querySelectorAll( '[data-wpbc-date-selection-toggle]' ).forEach( function ( toggle ) {
277 var field_name = toggle.dataset.wpbcDateSelectionToggle;
278 var transport = editor.node.querySelector( '[name="' + field_name + '"]' );
279 var policy_state = String( toggle.dataset.wpbcDateSelectionPolicyState || '' );
280
281 if ( transport ) {
282 if ( 'required_on' === policy_state || 'hidden_on' === policy_state ) {
283 transport.value = 'On';
284 } else if ( 'hidden_off' === policy_state ) {
285 transport.value = 'Off';
286 } else {
287 transport.value = toggle.checked && ! toggle.disabled ? 'On' : 'Off';
288 }
289 }
290 } );
291 }
292
293 /**
294 * Explain configuration-driven disabled toggle states.
295 *
296 * Native disabled inputs do not receive pointer or keyboard focus. The
297 * surrounding toggle remains hoverable and becomes keyboard focusable only
298 * while a documented dependency or edition rule disables its input.
299 *
300 * @return {void}
301 */
302 function sync_disabled_toggle_tooltips() {
303 if ( ! editor ) {
304 return;
305 }
306
307 editor.node.querySelectorAll( '[data-wpbc-date-selection-disabled-tooltip]' ).forEach( function ( tooltip_wrapper ) {
308 var toggle = tooltip_wrapper.querySelector( '[data-wpbc-date-selection-toggle]' );
309 var description_id = String( tooltip_wrapper.dataset.wpbcDateSelectionDisabledTooltip || '' );
310 var description = description_id ? document.getElementById( description_id ) : null;
311 var disabled_reason = toggle && toggle.disabled ? String( toggle.dataset.wpbcDateSelectionDisabledReason || '' ) : '';
312
313 if ( disabled_reason ) {
314 tooltip_wrapper.setAttribute( 'title', disabled_reason );
315 tooltip_wrapper.setAttribute( 'tabindex', '0' );
316 tooltip_wrapper.setAttribute( 'aria-describedby', description_id );
317 tooltip_wrapper.dataset.wpbcDateSelectionDisabledTooltipActive = '1';
318 } else {
319 tooltip_wrapper.removeAttribute( 'title' );
320 tooltip_wrapper.removeAttribute( 'tabindex' );
321 tooltip_wrapper.removeAttribute( 'aria-describedby' );
322 delete tooltip_wrapper.dataset.wpbcDateSelectionDisabledTooltipActive;
323 }
324
325 if ( description ) {
326 description.textContent = disabled_reason;
327 }
328 } );
329 }
330
331 /**
332 * Enforce canonical changeover, recurrent-time, and check-out dependencies.
333 *
334 * Booking Calendar treats changeover processing as mutually exclusive with
335 * recurrent time and an independently available check-out date. A check-out
336 * date also has no meaning for single or independent-date selections. Hide
337 * incompatible controls and normalize their request-local draft state.
338 *
339 * @return {void}
340 */
341 function sync_changeover_dependencies() {
342 var changeover_toggle;
343 var changeover_controls;
344 var changeover_fields;
345 var checkout_option;
346 var checkout_toggle;
347 var checkout_tooltip_wrapper;
348 var checkout_disabled_reason;
349 var recurrent_toggle;
350 var recurrent_option;
351 var behavior_section;
352 var changeover_is_active;
353 var checkout_is_supported;
354 var changeover_policy;
355 var recurrent_policy;
356 var checkout_policy;
357
358 if ( ! editor || ! editor.changeover ) {
359 return;
360 }
361
362 changeover_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_changeover_enabled"]' );
363 changeover_controls = editor.changeover.querySelector( '[data-wpbc-date-selection-changeover-controls]' );
364 changeover_fields = editor.changeover.querySelector( '[data-wpbc-date-selection-changeover-fields]' );
365 checkout_option = editor.node.querySelector( '[data-wpbc-date-selection-checkout-option]' );
366 checkout_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_checkout_available"]' );
367 checkout_tooltip_wrapper = checkout_toggle ? checkout_toggle.closest( '[data-wpbc-date-selection-disabled-tooltip]' ) : null;
368 recurrent_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_recurrent_time"]' );
369 recurrent_option = editor.node.querySelector( '[data-wpbc-date-selection-recurrent-option]' );
370 behavior_section = editor.node.querySelector( '[data-wpbc-date-selection-behavior]' );
371 changeover_policy = get_mode_policy_state( 'wpbcDateSelectionPolicyChangeover' );
372 recurrent_policy = get_mode_policy_state( 'wpbcDateSelectionPolicyRecurrentTime' );
373 checkout_policy = get_mode_policy_state( 'wpbcDateSelectionPolicyCheckoutAvailable' );
374 checkout_is_supported = 'optional' === checkout_policy;
375
376 if ( changeover_toggle ) {
377 changeover_toggle.dataset.wpbcDateSelectionPolicyState = changeover_policy;
378 if ( 'required_on' === changeover_policy ) {
379 changeover_toggle.checked = true;
380 changeover_toggle.disabled = true;
381 } else if ( 'hidden_off' === changeover_policy ) {
382 changeover_toggle.checked = false;
383 } else {
384 changeover_toggle.disabled = '1' === changeover_toggle.dataset.wpbcDateSelectionEditionLocked;
385 }
386 }
387 changeover_is_active = Boolean( changeover_toggle && changeover_toggle.checked && 'hidden_off' !== changeover_policy );
388
389 editor.changeover.hidden = -1 === [ 'optional', 'required_on' ].indexOf( changeover_policy );
390
391 if ( changeover_controls ) {
392 changeover_controls.hidden = ! changeover_is_active;
393 }
394 if ( changeover_fields ) {
395 changeover_fields.hidden = ! changeover_is_active;
396 }
397 if ( recurrent_option ) {
398 recurrent_option.hidden = -1 === [ 'optional', 'optional_default_on', 'required_on' ].indexOf( recurrent_policy );
399 }
400 if ( recurrent_toggle ) {
401 recurrent_toggle.dataset.wpbcDateSelectionPolicyState = recurrent_policy;
402 recurrent_toggle.disabled = 'required_on' === recurrent_policy;
403 if ( 'hidden_on' === recurrent_policy || 'required_on' === recurrent_policy ) {
404 recurrent_toggle.checked = true;
405 } else if ( -1 === [ 'optional', 'optional_default_on' ].indexOf( recurrent_policy ) || changeover_is_active ) {
406 recurrent_toggle.checked = false;
407 }
408 }
409 if ( checkout_option ) {
410 checkout_option.hidden = ! checkout_is_supported;
411 checkout_option.dataset.wpbcDateSelectionPolicyState = checkout_policy;
412 }
413 if ( checkout_toggle ) {
414 checkout_toggle.dataset.wpbcDateSelectionPolicyState = checkout_policy;
415 checkout_toggle.disabled = '1' === checkout_toggle.dataset.wpbcDateSelectionEditionLocked || changeover_is_active || ! checkout_is_supported;
416 checkout_disabled_reason = '';
417 if ( checkout_tooltip_wrapper && '1' === checkout_toggle.dataset.wpbcDateSelectionEditionLocked ) {
418 checkout_disabled_reason = checkout_tooltip_wrapper.dataset.wpbcDateSelectionCheckoutReasonEdition || '';
419 } else if ( checkout_tooltip_wrapper && changeover_is_active ) {
420 checkout_disabled_reason = checkout_tooltip_wrapper.dataset.wpbcDateSelectionCheckoutReasonChangeover || '';
421 } else if ( checkout_tooltip_wrapper && ! checkout_is_supported ) {
422 checkout_disabled_reason = checkout_tooltip_wrapper.dataset.wpbcDateSelectionCheckoutReasonMode || '';
423 }
424 checkout_toggle.dataset.wpbcDateSelectionDisabledReason = checkout_disabled_reason;
425 if ( changeover_is_active || ! checkout_is_supported ) {
426 checkout_toggle.checked = false;
427 }
428 }
429 if ( behavior_section ) {
430 behavior_section.hidden = -1 === [ 'optional', 'optional_default_on', 'required_on' ].indexOf( recurrent_policy ) && ! checkout_is_supported;
431 }
432 }
433
434 /**
435 * Let the most recently activated mutually exclusive option win.
436 *
437 * @param {HTMLElement|null} toggle Changed Date Selection toggle.
438 * @return {void}
439 */
440 function apply_exclusive_toggle_action( toggle ) {
441 var field_name;
442 var conflicting_toggle;
443
444 if ( ! editor || ! toggle || ! toggle.checked ) {
445 return;
446 }
447
448 field_name = toggle.dataset.wpbcDateSelectionToggle || '';
449 if ( 'date_selection_changeover_enabled' === field_name ) {
450 conflicting_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_recurrent_time"]' );
451 } else if ( 'date_selection_recurrent_time' === field_name ) {
452 conflicting_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_changeover_enabled"]' );
453 }
454
455 if ( conflicting_toggle ) {
456 conflicting_toggle.checked = false;
457 }
458 }
459
460 /**
461 * Synchronize the non-mutating calendar-legend configuration preview.
462 *
463 * All preview nodes are rendered by PHP. This method changes only text,
464 * visibility, and layout classes; it never constructs or evaluates markup.
465 *
466 * @return {void}
467 */
468 function sync_legend_preview() {
469 var legend_toggle;
470 var legend_settings;
471 var legend_preview;
472 var show_numbers_toggle;
473 var vertical_toggle;
474 var day_number;
475
476 if ( ! editor || ! editor.legend ) {
477 return;
478 }
479
480 legend_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_legend_enabled"]' );
481 legend_settings = editor.legend.querySelector( '[data-wpbc-date-selection-legend-settings]' );
482 legend_preview = editor.node.querySelector( '[data-wpbc-date-selection-legend-preview]' );
483 show_numbers_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_legend_show_numbers"]' );
484 vertical_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_legend_vertical"]' );
485 day_number = legend_preview ? String( legend_preview.dataset.wpbcDateSelectionLegendDayNumber || '' ) : '';
486
487 if ( legend_settings ) {
488 legend_settings.hidden = ! legend_toggle || ! legend_toggle.checked;
489 }
490 if ( ! legend_preview ) {
491 return;
492 }
493
494 legend_preview.hidden = ! legend_toggle || ! legend_toggle.checked;
495 legend_preview.classList.toggle( 'is-vertical', Boolean( vertical_toggle && vertical_toggle.checked ) );
496 legend_preview.querySelectorAll( '[data-wpbc-date-selection-legend-preview-item]' ).forEach( function ( preview_item ) {
497 var legend_id = preview_item.dataset.wpbcDateSelectionLegendPreviewItem;
498 var item_toggle = editor.node.querySelector( '[data-wpbc-date-selection-toggle="date_selection_legend_item_' + legend_id + '"]' );
499 var title_control = editor.node.querySelector( '[data-wpbc-date-selection-legend-title="' + legend_id + '"]' );
500 var title_node = preview_item.querySelector( '.wpdev_hint_with_text .block_text:last-child' );
501 var title = title_control ? String( title_control.value || '' ).trim() : '';
502
503 preview_item.hidden = ! item_toggle || ! item_toggle.checked;
504 if ( title_node && title_control ) {
505 title_node.textContent = title || title_control.placeholder || '';
506 }
507 preview_item.querySelectorAll( '.wpbc_calendar_legend_day_cell_height .date-cell-content > a, .wpbc_calendar_legend_day_cell_height .date-cell-content > span' ).forEach( function ( date_node ) {
508 date_node.textContent = show_numbers_toggle && show_numbers_toggle.checked ? day_number : '';
509 } );
510 } );
511 }
512
513 /**
514 * Synchronize canonical changeover classes in the calendar and legend.
515 *
516 * Booking Calendar owns the underlying day status and SVG markup. This
517 * adapter changes only the same ancestor and day-state classes used by the
518 * released calendar renderer, so triangle and split-day presentation stay
519 * consistent without constructing or evaluating HTML.
520 *
521 * @return {void}
522 */
523 function sync_changeover_preview_classes() {
524 var changeover_transport;
525 var triangles_transport;
526 var changeover_is_active;
527 var triangles_are_active;
528 var partially_item;
529 var partially_day;
530 var legend_table;
531 var legend_wrapper;
532 var changeover_classes;
533
534 if ( ! editor ) {
535 return;
536 }
537
538 changeover_transport = editor.node.querySelector( '[name="date_selection_changeover_enabled"]' );
539 triangles_transport = editor.node.querySelector( '[name="date_selection_triangles"]' );
540 changeover_is_active = Boolean( changeover_transport && 'On' === changeover_transport.value );
541 triangles_are_active = Boolean( changeover_is_active && triangles_transport && 'On' === triangles_transport.value );
542
543 if ( editor.calendar ) {
544 editor.calendar.classList.toggle( 'wpbc_change_over_triangle', triangles_are_active );
545 editor.calendar.querySelectorAll( '.wpbc_calendar_wraper' ).forEach( function ( calendar_wrapper ) {
546 calendar_wrapper.classList.toggle( 'wpbc_change_over_triangle', triangles_are_active );
547 } );
548 }
549
550 partially_item = editor.node.querySelector( '[data-wpbc-date-selection-legend-preview-item="partially"]' );
551 if ( ! partially_item ) {
552 return;
553 }
554
555 partially_day = partially_item.querySelector( '.wpbc_calendar_legend_day_cell_height' );
556 if ( partially_day ) {
557 changeover_classes = [
558 'date_approved',
559 'check_in_time',
560 'check_out_time',
561 'check_in_time_date_approved',
562 'check_out_time_date2approve'
563 ];
564 changeover_classes.forEach( function ( class_name ) {
565 partially_day.classList.toggle( class_name, changeover_is_active );
566 } );
567 partially_day.classList.toggle( 'date2approve', ! changeover_is_active );
568 partially_day.classList.toggle( 'times_clock', ! changeover_is_active );
569 }
570
571 legend_table = partially_item.querySelector( '.wpbc_calendar_legend_table_width_height' );
572 legend_wrapper = legend_table ? legend_table.parentElement : null;
573 if ( legend_wrapper ) {
574 legend_wrapper.classList.toggle( 'wpbc_change_over_triangle', triangles_are_active );
575 }
576 }
577
578 /**
579 * Reload canonical day-status data with authorized draft overrides.
580 *
581 * The shared calendar endpoint calculates booking boundaries. A dedicated
582 * Setup Wizard nonce and capability check authorize these request-local
583 * values; no setting or booking is written by this request.
584 *
585 * @return {void}
586 */
587 function refresh_changeover_preview_data() {
588 var changeover_transport;
589 var triangles_transport;
590 var recurrent_transport;
591 var checkout_transport;
592
593 if (
594 ! editor
595 || ! editor.calendar
596 || ! config.preview_nonce
597 || 'function' !== typeof window.wpbc_calendar__load_data__ajx
598 ) {
599 return;
600 }
601
602 changeover_transport = editor.node.querySelector( '[name="date_selection_changeover_enabled"]' );
603 triangles_transport = editor.node.querySelector( '[name="date_selection_triangles"]' );
604 recurrent_transport = editor.node.querySelector( '[name="date_selection_recurrent_time"]' );
605 checkout_transport = editor.node.querySelector( '[name="date_selection_checkout_available"]' );
606
607 window.wpbc_calendar__load_data__ajx( {
608 resource_id: editor.resource_id,
609 booking_hash: '',
610 request_uri: '',
611 custom_form: 'standard',
612 aggregate_resource_id_str: '',
613 aggregate_type: 'all',
614 skip_general_availability: 0,
615 classic_booking_context_token: '',
616 wpbc_settings_calendar_preview: 1,
617 wpbc_setup_wizard_date_selection_preview: 1,
618 wpbc_setup_wizard_date_selection_preview_nonce: String( config.preview_nonce ),
619 wpbc_settings_calendar_preview_changeover: changeover_transport && 'On' === changeover_transport.value ? 'On' : 'Off',
620 wpbc_settings_calendar_preview_triangles: triangles_transport && 'On' === triangles_transport.value ? 'On' : 'Off',
621 wpbc_settings_calendar_preview_recurrent_time: recurrent_transport && 'On' === recurrent_transport.value ? 'On' : 'Off',
622 wpbc_settings_calendar_preview_last_checkout: checkout_transport && 'On' === checkout_transport.value ? 'On' : 'Off',
623 wpbc_settings_calendar_preview_show_legend: 'Off'
624 } );
625 }
626
627 /**
628 * Debounce request-local day-status refreshes after related toggles change.
629 *
630 * @return {void}
631 */
632 function schedule_changeover_data_refresh() {
633 if ( preview_data_timer ) {
634 window.clearTimeout( preview_data_timer );
635 }
636 preview_data_timer = window.setTimeout( refresh_changeover_preview_data, 180 );
637 }
638
639 /**
640 * Return the authoritative number field paired with one presentation slider.
641 *
642 * @param {string} field_name Date Selection draft field name.
643 * @return {HTMLInputElement|null} Matching number field, or null.
644 */
645 function get_number_control( field_name ) {
646 return editor ? editor.node.querySelector( '[data-wpbc-date-selection-number="' + field_name + '"]' ) : null;
647 }
648
649 /**
650 * Copy one valid authoritative number value into its presentation slider.
651 *
652 * Invalid exact values remain visible for native and wizard validation; the
653 * slider must not silently coerce them into an apparently valid draft.
654 *
655 * @param {HTMLInputElement} number_control Authoritative number field.
656 * @return {void}
657 */
658 function sync_range_from_number( number_control ) {
659 var field_name = number_control ? number_control.dataset.wpbcDateSelectionNumber : '';
660 var range_control = field_name && editor ? editor.node.querySelector( '[data-wpbc-date-selection-range="' + field_name + '"]' ) : null;
661
662 if ( ! range_control || '' === number_control.value || ! number_control.checkValidity() ) {
663 return;
664 }
665
666 range_control.value = number_control.value;
667 }
668
669 /**
670 * Copy one presentation slider value into its authoritative number field.
671 *
672 * @param {HTMLInputElement} range_control Presentation range control.
673 * @return {void}
674 */
675 function sync_number_from_range( range_control ) {
676 var field_name = range_control ? range_control.dataset.wpbcDateSelectionRange : '';
677 var number_control = get_number_control( field_name );
678
679 if ( ! number_control || number_control.disabled || range_control.disabled ) {
680 return;
681 }
682
683 number_control.value = range_control.value;
684 }
685
686 /**
687 * Align every presentation slider with its authoritative number field.
688 *
689 * @return {void}
690 */
691 function sync_number_ranges() {
692 if ( ! editor ) {
693 return;
694 }
695
696 editor.node.querySelectorAll( '[data-wpbc-date-selection-number]' ).forEach( sync_range_from_number );
697 }
698
699 /**
700 * Apply one trusted preset value through the authoritative number control.
701 *
702 * @param {string} field_name Draft field name.
703 * @param {string} raw_value Server-defined integer value.
704 * @return {void}
705 */
706 function apply_number_preset( field_name, raw_value ) {
707 var number_control = get_number_control( field_name );
708
709 if ( ! number_control || number_control.disabled || ! /^\d+$/.test( String( raw_value ) ) ) {
710 return;
711 }
712
713 number_control.value = String( raw_value );
714 sync_range_from_number( number_control );
715 }
716
717 /**
718 * Apply a server-defined specific-length preset as one coherent draft.
719 *
720 * Clear intentionally omits the range and weekday data attributes, so it
721 * removes only the specific-length restriction and preserves manual rules.
722 *
723 * @param {HTMLElement} preset_button Activated preset button.
724 * @param {HTMLInputElement} text_control Specific-length text field.
725 * @return {void}
726 */
727 function apply_specific_day_preset( preset_button, text_control ) {
728 text_control.value = preset_button.dataset.wpbcDateSelectionSpecificPreset || '';
729
730 if ( preset_button.hasAttribute( 'data-wpbc-date-selection-preset-minimum' ) ) {
731 apply_number_preset( 'date_selection_dynamic_min', preset_button.dataset.wpbcDateSelectionPresetMinimum );
732 }
733 if ( preset_button.hasAttribute( 'data-wpbc-date-selection-preset-maximum' ) ) {
734 apply_number_preset( 'date_selection_dynamic_max', preset_button.dataset.wpbcDateSelectionPresetMaximum );
735 }
736 if ( preset_button.hasAttribute( 'data-wpbc-date-selection-preset-weekdays' ) ) {
737 apply_weekday_preset( 'dynamic', preset_button.dataset.wpbcDateSelectionPresetWeekdays );
738 }
739
740 shell_api.clear_field_error( 'date_selection_dynamic_min' );
741 shell_api.clear_field_error( 'date_selection_dynamic_max' );
742 shell_api.clear_field_error( 'date_selection_dynamic_specific' );
743 shell_api.clear_field_error( 'date_selection_dynamic_weekdays' );
744 }
745
746 /**
747 * Update selected cards, settings panels, and changeover visibility.
748 *
749 * @return {void}
750 */
751 function render_editor_state() {
752 var mode = get_selected_mode();
753
754 if ( ! editor ) {
755 return;
756 }
757
758 editor.node.querySelectorAll( '.wpbc_setup_wizard__date-selection-mode-card' ).forEach( function ( card ) {
759 var radio = card.querySelector( '[data-wpbc-date-selection-mode]' );
760 card.classList.toggle( 'is-selected', Boolean( radio && radio.checked ) );
761 } );
762 editor.node.querySelectorAll( '[data-wpbc-date-selection-panel]' ).forEach( function ( panel ) {
763 panel.hidden = panel.dataset.wpbcDateSelectionPanel !== mode;
764 } );
765
766 sync_changeover_dependencies();
767 }
768
769 /**
770 * Set one canonical request-local calendar parameter.
771 *
772 * @param {string} key Calendar parameter name.
773 * @param {*} value Parameter value.
774 * @return {void}
775 */
776 function set_calendar_parameter( key, value ) {
777 if ( editor && window._wpbc && 'function' === typeof window._wpbc.calendar__set_param_value ) {
778 window._wpbc.calendar__set_param_value( editor.resource_id, key, value );
779 }
780 }
781
782 /**
783 * Normalize the legacy calendar scroll target before a released mode helper runs.
784 *
785 * The core calendar accepts either false or a two-value month/year array. The
786 * request-local preview can initially expose null, which the legacy helper
787 * otherwise attempts to index while reinitializing the calendar.
788 *
789 * @return {void}
790 */
791 function normalize_calendar_scroll_target() {
792 var scroll_target;
793
794 if (
795 ! editor
796 || ! window._wpbc
797 || 'function' !== typeof window._wpbc.calendar__get_param_value
798 || 'function' !== typeof window._wpbc.calendar__set_param_value
799 ) {
800 return;
801 }
802
803 scroll_target = window._wpbc.calendar__get_param_value( editor.resource_id, 'calendar_scroll_to' );
804 if (
805 false !== scroll_target
806 && (
807 ! Array.isArray( scroll_target )
808 || scroll_target.length < 2
809 || null === scroll_target[ 0 ]
810 || null === scroll_target[ 1 ]
811 )
812 ) {
813 set_calendar_parameter( 'calendar_scroll_to', false );
814 }
815 }
816
817 /**
818 * Apply the current draft through the real Booking Calendar mode helpers.
819 *
820 * @return {boolean} True when the calendar runtime was ready.
821 */
822 function apply_preview_policy() {
823 var mode;
824 var changeover;
825 var recurrent_time;
826 var triangles;
827 var calendar_node;
828 var fixed_days;
829 var fixed_weekdays;
830 var dynamic_min;
831 var dynamic_max;
832 var dynamic_specific;
833 var dynamic_weekdays;
834
835 if ( ! editor ) {
836 return false;
837 }
838
839 sync_changeover_preview_classes();
840
841 if ( ! window._wpbc || 'function' !== typeof window._wpbc.calendar__set_param_value ) {
842 return false;
843 }
844
845 calendar_node = document.getElementById( 'calendar_booking' + editor.resource_id );
846 if ( ! calendar_node ) {
847 return false;
848 }
849
850 mode = get_selected_mode();
851 changeover = editor.node.querySelector( '[name="date_selection_changeover_enabled"]' );
852 recurrent_time = editor.node.querySelector( '[name="date_selection_recurrent_time"]' );
853 triangles = editor.node.querySelector( '[name="date_selection_triangles"]' );
854 fixed_days = get_integer( '[name="date_selection_fixed_days"]', 3 );
855 fixed_weekdays = get_weekdays( 'fixed' );
856 dynamic_min = get_integer( '[name="date_selection_dynamic_min"]', 1 );
857 dynamic_max = get_integer( '[name="date_selection_dynamic_max"]', 1 );
858 dynamic_specific = get_specific_days();
859 dynamic_weekdays = get_weekdays( 'dynamic' );
860 dynamic_specific = null === dynamic_specific ? [] : dynamic_specific;
861
862 set_calendar_parameter( 'is_enabled_change_over', Boolean( changeover && 'On' === changeover.value ) );
863 set_calendar_parameter( 'booking_recurrent_time', recurrent_time && 'On' === recurrent_time.value ? 'On' : 'Off' );
864 set_calendar_parameter( 'days_select_mode', mode );
865 set_calendar_parameter( 'fixed__days_num', fixed_days );
866 set_calendar_parameter( 'fixed__week_days__start', fixed_weekdays );
867 set_calendar_parameter( 'dynamic__days_min', dynamic_min );
868 set_calendar_parameter( 'dynamic__days_max', dynamic_max );
869 set_calendar_parameter( 'dynamic__days_specific', dynamic_specific );
870 set_calendar_parameter( 'dynamic__week_days__start', dynamic_weekdays );
871 normalize_calendar_scroll_target();
872 if ( 'function' === typeof window._wpbc.set_other_param ) {
873 window._wpbc.set_other_param( 'is_enabled_change_over', Boolean( changeover && 'On' === changeover.value ) );
874 }
875 if ( 'function' === typeof window.wpbc__conditions__SAVE_INITIAL__days_selection_params__bm ) {
876 window.wpbc__conditions__SAVE_INITIAL__days_selection_params__bm( editor.resource_id );
877 }
878
879 if ( 'single' === mode && 'function' === typeof window.wpbc_cal_days_select__single ) {
880 window.wpbc_cal_days_select__single( editor.resource_id );
881 return true;
882 }
883 if ( 'multiple' === mode && 'function' === typeof window.wpbc_cal_days_select__multiple ) {
884 window.wpbc_cal_days_select__multiple( editor.resource_id );
885 return true;
886 }
887 if ( 'fixed' === mode && 'function' === typeof window.wpbc_cal_days_select__fixed ) {
888 window.wpbc_cal_days_select__fixed(
889 editor.resource_id,
890 fixed_days,
891 fixed_weekdays
892 );
893 return true;
894 }
895 if ( 'dynamic' === mode && 'function' === typeof window.wpbc_cal_days_select__range ) {
896 window.wpbc_cal_days_select__range(
897 editor.resource_id,
898 dynamic_min,
899 dynamic_max,
900 dynamic_specific,
901 dynamic_weekdays
902 );
903 return true;
904 }
905
906 return false;
907 }
908
909 /**
910 * Reapply policy after immediate interaction and delayed calendar bootstrap.
911 *
912 * @return {void}
913 */
914 function schedule_preview_update() {
915 apply_timers.forEach( function ( timer_id ) {
916 window.clearTimeout( timer_id );
917 } );
918 apply_timers = [ 0, 120, 420, 900, 1400 ].map( function ( delay ) {
919 return window.setTimeout( apply_preview_policy, delay );
920 } );
921 }
922
923 /**
924 * Synchronize all presentation controls before draft collection.
925 *
926 * @return {void}
927 */
928 function sync() {
929 sync_weekday_transport( 'fixed' );
930 sync_weekday_transport( 'dynamic' );
931 sync_changeover_dependencies();
932 sync_disabled_toggle_tooltips();
933 sync_toggle_transports();
934 sync_legend_preview();
935 sync_changeover_preview_classes();
936 sync_number_ranges();
937 }
938
939 /**
940 * Validate coupled date-selection values before navigation.
941 *
942 * Server-side field allow-lists remain authoritative. This prevents an
943 * internally inconsistent flexible range from being sent as a draft.
944 *
945 * @return {HTMLElement|null} First invalid control, or null.
946 */
947 function validate() {
948 var mode = get_selected_mode();
949 var fixed_days = get_integer( '[name="date_selection_fixed_days"]', 0 );
950 var dynamic_min = get_integer( '[name="date_selection_dynamic_min"]', 0 );
951 var dynamic_max = get_integer( '[name="date_selection_dynamic_max"]', 0 );
952 var dynamic_specific = get_specific_days();
953 var fixed_control = editor.node.querySelector( '[name="date_selection_fixed_days"]' );
954 var dynamic_min_control = editor.node.querySelector( '[name="date_selection_dynamic_min"]' );
955 var dynamic_max_control = editor.node.querySelector( '[name="date_selection_dynamic_max"]' );
956 var dynamic_specific_control = editor.node.querySelector( '[data-wpbc-date-selection-specific]' );
957
958 if ( ! mode ) {
959 shell_api.set_field_error( 'date_selection_mode', get_message( 'mode_required', 'Choose how customers select dates.' ) );
960 return editor.node.querySelector( '[data-wpbc-date-selection-mode]' );
961 }
962
963 if ( fixed_days < 1 || fixed_days > parseInt( config.max_fixed_days || 180, 10 ) ) {
964 shell_api.set_field_error( 'date_selection_fixed_days', get_message( 'fixed_days_invalid', 'Enter a valid fixed range.' ) );
965 return fixed_control;
966 }
967 shell_api.clear_field_error( 'date_selection_fixed_days' );
968
969 if ( dynamic_min < 1 || dynamic_max < dynamic_min || dynamic_max > parseInt( config.max_dynamic_days || 1095, 10 ) ) {
970 shell_api.set_field_error( 'date_selection_dynamic_min', get_message( 'dynamic_days_invalid', 'Enter a valid flexible range.' ) );
971 shell_api.set_field_error( 'date_selection_dynamic_max', get_message( 'dynamic_days_invalid', 'Enter a valid flexible range.' ) );
972 return dynamic_min < 1 ? dynamic_min_control : dynamic_max_control;
973 }
974 shell_api.clear_field_error( 'date_selection_dynamic_min' );
975 shell_api.clear_field_error( 'date_selection_dynamic_max' );
976
977 if (
978 null === dynamic_specific
979 || dynamic_specific.some( function ( specific_day ) {
980 return specific_day < dynamic_min || specific_day > dynamic_max;
981 } )
982 ) {
983 shell_api.set_field_error( 'date_selection_dynamic_specific', get_message( 'specific_days_invalid', 'Enter comma-separated day lengths within the flexible range, for example 7,14,21,28.' ) );
984 return dynamic_specific_control;
985 }
986 shell_api.clear_field_error( 'date_selection_dynamic_specific' );
987
988 return null;
989 }
990
991 /**
992 * Handle editor controls without constructing dynamic HTML.
993 *
994 * @param {Event} event Browser input or click event.
995 * @return {void}
996 */
997 function handle_interaction( event ) {
998 var weekday_button = event.target.closest( '[data-wpbc-date-selection-weekdays] [data-weekday]' );
999 var number_control = event.target.closest( '[data-wpbc-date-selection-number]' );
1000 var range_control = event.target.closest( '[data-wpbc-date-selection-range]' );
1001 var specific_preset = event.target.closest( '[data-wpbc-date-selection-specific-preset]' );
1002 var specific_control = editor ? editor.node.querySelector( '[data-wpbc-date-selection-specific]' ) : null;
1003 var changed_mode = event.target.closest( '[data-wpbc-date-selection-mode]' );
1004 var changed_toggle = event.target.closest( '[data-wpbc-date-selection-toggle]' );
1005 var changed_toggle_name = changed_toggle ? String( changed_toggle.dataset.wpbcDateSelectionToggle || '' ) : '';
1006 var editor_control = event.target.closest( '[data-wpbc-date-selection-mode], [data-wpbc-date-selection-number], [data-wpbc-date-selection-range], [data-wpbc-date-selection-specific], [data-wpbc-date-selection-specific-preset], [data-wpbc-date-selection-toggle], [data-wpbc-date-selection-legend-title], [data-wpbc-date-selection-weekdays] [data-weekday], [name="date_selection_check_in_time"], [name="date_selection_check_out_time"]' );
1007
1008 if ( ! editor_control || event.target.closest( '[data-wpbc-date-selection-calendar]' ) ) {
1009 return;
1010 }
1011
1012 if ( weekday_button && ! weekday_button.disabled ) {
1013 event.preventDefault();
1014 weekday_button.setAttribute( 'aria-pressed', 'true' === weekday_button.getAttribute( 'aria-pressed' ) ? 'false' : 'true' );
1015 weekday_button.classList.toggle( 'is-selected', 'true' === weekday_button.getAttribute( 'aria-pressed' ) );
1016 sync_weekday_transport( weekday_button.closest( '[data-wpbc-date-selection-weekdays]' ).dataset.wpbcDateSelectionWeekdays );
1017 }
1018 if ( specific_preset && specific_control && ! specific_preset.disabled && ! specific_control.disabled ) {
1019 event.preventDefault();
1020 apply_specific_day_preset( specific_preset, specific_control );
1021 }
1022 if ( range_control ) {
1023 sync_number_from_range( range_control );
1024 } else if ( number_control ) {
1025 sync_range_from_number( number_control );
1026 }
1027 apply_exclusive_toggle_action( changed_toggle );
1028 apply_selected_mode_defaults( changed_mode );
1029
1030 sync();
1031 render_editor_state();
1032 schedule_preview_update();
1033 if (
1034 changed_mode
1035 || 'date_selection_changeover_enabled' === changed_toggle_name
1036 || 'date_selection_recurrent_time' === changed_toggle_name
1037 || 'date_selection_checkout_available' === changed_toggle_name
1038 ) {
1039 schedule_changeover_data_refresh();
1040 }
1041 }
1042
1043 return {
1044 /**
1045 * Initialize the editor below the authorized wizard shell.
1046 *
1047 * @param {Object} api Shared shell services.
1048 * @return {void}
1049 */
1050 initialize: function ( api ) {
1051 var node = api.root.querySelector( '[data-wpbc-date-selection-editor]' );
1052
1053 if ( ! node ) {
1054 return;
1055 }
1056
1057 shell_api = api;
1058 editor = {
1059 node: node,
1060 resource_id: parseInt( node.dataset.resourceId, 10 ) || 1,
1061 calendar: node.querySelector( '[data-wpbc-date-selection-calendar]' ),
1062 changeover: node.querySelector( '[data-wpbc-date-selection-changeover]' ),
1063 legend: node.querySelector( '[data-wpbc-date-selection-legend]' )
1064 };
1065 last_selected_mode = get_selected_mode();
1066 node.addEventListener( 'change', handle_interaction );
1067 node.addEventListener( 'input', handle_interaction );
1068 node.addEventListener( 'click', handle_interaction );
1069 sync();
1070 render_editor_state();
1071 schedule_preview_update();
1072 },
1073 sync: sync,
1074 validate: validate
1075 };
1076 }
1077
1078 /**
1079 * Register after the shared shell exposes its module adapter API.
1080 *
1081 * @return {void}
1082 */
1083 function register_adapter() {
1084 if ( window.wpbc_setup_wizard_api && 'function' === typeof window.wpbc_setup_wizard_api.register_step_adapter ) {
1085 window.wpbc_setup_wizard_api.register_step_adapter( create_date_selection_adapter( module_config ) );
1086 }
1087 }
1088
1089 window.wpbc_setup_editor_modules = window.wpbc_setup_editor_modules || {};
1090 window.wpbc_setup_editor_modules.date_selection = {
1091 create: create_date_selection_adapter
1092 };
1093
1094 if ( 'loading' === document.readyState ) {
1095 document.addEventListener( 'DOMContentLoaded', register_adapter );
1096 } else {
1097 register_adapter();
1098 }
1099 }( window, document ) );
1100