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-booking-form-template / _src / step-booking-form-template.js

step-booking-form-template.js in Booking Calendar 11.9, at includes/page-setup-wizard/step-booking-form-template/_src/step-booking-form-template.js

904 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Provide the reusable Booking Form template catalog and interactive preview.
3 *
4 * Catalog cards remain server-rendered. Direct templates use request-local
5 * inline HTML, while Appointment Flow and full-site previews use signed URLs.
6 * The browser never receives raw template definitions and never applies a form.
7 *
8 * @package Booking Calendar
9 */
10 ( function ( window, document ) {
11 'use strict';
12
13 var module_config = window.wpbc_setup_wizard_booking_form_template || { i18n: {} };
14
15 /**
16 * Create one isolated Booking Form template selector adapter.
17 *
18 * @param {Object} config Module endpoint and translation settings.
19 * @return {Object} Step adapter accepted by the shared wizard shell.
20 */
21 function create_booking_form_template_adapter( config ) {
22 var appointment_flow_dialog_action = 'booking-form-appointment-flow';
23 var template_guidance_dialog_action = 'booking-form-template-guidance';
24 var shell_api = null;
25 var root = null;
26 var editor = null;
27 var confirmed_booking_form_usage = '';
28 var preview_request_sequence = 0;
29 var preview_abort_controller = null;
30 var open_request_sequence = 0;
31 var open_abort_controller = null;
32 var open_request_is_loading = false;
33 var inline_preview_renderer = null;
34 var current_preview = {
35 kind: '',
36 url: '',
37 template_slug: '',
38 booking_form_usage: ''
39 };
40
41 /**
42 * Return one translated string with a safe fallback.
43 *
44 * @param {string} key Translation key.
45 * @param {string} fallback Fallback copy.
46 * @return {string} Translation or fallback.
47 */
48 function get_message( key, fallback ) {
49 return config.i18n && config.i18n[ key ] ? config.i18n[ key ] : fallback;
50 }
51
52 /**
53 * Return normalized text for case-insensitive catalog searching.
54 *
55 * @param {string} text Source text.
56 * @return {string} Lowercase normalized text.
57 */
58 function normalize_search_text( text ) {
59 return String( text || '' ).toLocaleLowerCase();
60 }
61
62 /**
63 * Return one template card's searchable text.
64 *
65 * @param {HTMLElement} card Template card.
66 * @return {string} Normalized searchable text.
67 */
68 function get_card_search_text( card ) {
69 var title = card.querySelector( '[data-wpbc-template-title]' );
70 var description = card.querySelector( '[data-wpbc-template-description]' );
71
72 return normalize_search_text(
73 ( title ? title.textContent : '' ) + ' ' +
74 ( description ? description.textContent : '' ) + ' ' +
75 ( card.dataset.templateSlug || '' )
76 );
77 }
78
79 /**
80 * Install a graceful fallback for one catalog thumbnail.
81 *
82 * @param {HTMLImageElement|null} image Image element.
83 * @param {HTMLElement|null} placeholder Placeholder element.
84 * @return {void}
85 */
86 function initialize_image_fallback( image, placeholder ) {
87 if ( ! image ) {
88 return;
89 }
90
91 image.addEventListener( 'load', function () {
92 image.hidden = false;
93 if ( placeholder ) {
94 placeholder.hidden = true;
95 }
96 } );
97 image.addEventListener( 'error', function () {
98 image.hidden = true;
99 if ( placeholder ) {
100 placeholder.hidden = false;
101 }
102 } );
103
104 if ( image.complete && image.src && 0 === image.naturalWidth ) {
105 image.hidden = true;
106 if ( placeholder ) {
107 placeholder.hidden = false;
108 }
109 }
110 }
111
112 /**
113 * Return the currently selected server-rendered card.
114 *
115 * @return {HTMLElement|null} Selected card or null.
116 */
117 function get_selected_card() {
118 var selected_radio = editor && editor.node.querySelector( '[data-wpbc-template-radio]:checked' );
119
120 return selected_radio ? selected_radio.closest( '[data-wpbc-template-card]' ) : null;
121 }
122
123 /**
124 * Return the selected server-rendered Booking Form entry point.
125 *
126 * @return {string} Stable usage identifier, or an empty string.
127 */
128 function get_selected_booking_form_usage() {
129 var selected_radio = editor && editor.node.querySelector( '[data-wpbc-booking-form-usage-radio]:checked' );
130
131 return selected_radio ? String( selected_radio.value || '' ) : '';
132 }
133
134 /**
135 * Build the informational shortcode for the current allow-listed choices.
136 *
137 * The value is inserted with textContent only and is never evaluated by the
138 * browser. The preview endpoint independently validates both identifiers.
139 *
140 * @param {string} booking_form_usage Selected entry-point identifier.
141 * @return {string} Informational shortcode.
142 */
143 function get_preview_shortcode( booking_form_usage ) {
144 return 'appointment_flow' === booking_form_usage
145 ? '[booking_appointment form_type="standard"]'
146 : '[booking form_type="standard"]';
147 }
148
149 /**
150 * Synchronize the selected template metadata below the preview.
151 *
152 * @param {HTMLElement|null} card Selected template card.
153 * @return {void}
154 */
155 function render_preview_metadata( card ) {
156 var title_node = editor.node.querySelector( '[data-wpbc-template-preview-title]' );
157 var slug_node = editor.node.querySelector( '[data-wpbc-template-preview-slug]' );
158 var shortcode_node = editor.node.querySelector( '[data-wpbc-template-preview-shortcode]' );
159 var description_node = editor.node.querySelector( '[data-wpbc-template-preview-description]' );
160 var title = card && card.querySelector( '[data-wpbc-template-title]' );
161 var description = card && card.querySelector( '[data-wpbc-template-description]' );
162 var template_slug = card ? String( card.dataset.templateSlug || '' ) : '';
163
164 if ( title_node ) {
165 title_node.textContent = title ? title.textContent.trim() : '';
166 }
167 if ( slug_node ) {
168 slug_node.textContent = template_slug;
169 }
170 if ( shortcode_node ) {
171 shortcode_node.textContent = get_preview_shortcode( get_selected_booking_form_usage() );
172 }
173 if ( description_node ) {
174 description_node.textContent = description ? description.textContent.trim() : '';
175 }
176 }
177
178 /**
179 * Update preview loading, error, and action states.
180 *
181 * @param {boolean} is_loading Whether a preview request/frame is loading.
182 * @param {string} error_text Plain error text, or an empty string.
183 * @return {void}
184 */
185 function set_preview_state( is_loading, error_text ) {
186 var has_error = '' !== String( error_text || '' );
187
188 editor.preview_stage.classList.toggle( 'is-loading', is_loading );
189 editor.preview_stage.classList.toggle( 'has-error', has_error );
190 editor.preview_stage.setAttribute( 'aria-busy', is_loading ? 'true' : 'false' );
191 editor.preview_loader.hidden = ! is_loading;
192 editor.preview_error.hidden = ! has_error;
193 if ( has_error ) {
194 editor.preview_error.querySelector( 'span' ).textContent = error_text;
195 }
196 editor.refresh_button.disabled = is_loading || ! get_selected_card();
197 editor.open_button.disabled = is_loading || open_request_is_loading || ! get_selected_card();
198 }
199
200 /**
201 * Clear the iframe's association with an earlier template preview.
202 *
203 * The attribute is diagnostic as well as functional: it lets browser tests
204 * verify that the visible iframe belongs to the currently selected card.
205 *
206 * @return {void}
207 */
208 function clear_preview_frame() {
209 editor.preview_frame.removeAttribute( 'data-preview-template-slug' );
210 editor.preview_frame.removeAttribute( 'data-preview-booking-form-usage' );
211 editor.preview_frame.src = 'about:blank';
212 editor.preview_frame.hidden = true;
213 }
214
215 /**
216 * Record one successfully rendered preview.
217 *
218 * @param {string} preview_kind Inline or URL preview kind.
219 * @param {string} preview_url Signed URL when applicable.
220 * @param {string} template_slug Validated template slug.
221 * @param {string} booking_form_usage Validated usage identifier.
222 * @return {void}
223 */
224 function set_current_preview( preview_kind, preview_url, template_slug, booking_form_usage ) {
225 current_preview = {
226 kind: preview_kind,
227 url: preview_url,
228 template_slug: template_slug,
229 booking_form_usage: booking_form_usage
230 };
231 }
232
233 /**
234 * Determine whether the current preview matches the selected controls.
235 *
236 * @param {string} template_slug Selected template slug.
237 * @param {string} booking_form_usage Selected usage identifier.
238 * @return {boolean} Whether the active preview belongs to the selection.
239 */
240 function current_preview_matches( template_slug, booking_form_usage ) {
241 return current_preview.template_slug === template_slug && current_preview.booking_form_usage === booking_form_usage;
242 }
243
244 /**
245 * Read a normalized WordPress AJAX error message.
246 *
247 * @param {Object|null} response Parsed JSON response.
248 * @return {string} Plain error message.
249 */
250 function get_ajax_error_message( response ) {
251 if ( response && response.data && response.data.message ) {
252 return String( response.data.message );
253 }
254
255 return get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' );
256 }
257
258 /**
259 * Build the authenticated request body for one server-owned preview.
260 *
261 * @param {string} template_slug Selected template slug.
262 * @param {string} booking_form_usage Selected usage identifier.
263 * @param {string} preview_target Embedded or window target.
264 * @return {FormData} Request body.
265 */
266 function build_preview_request_body( template_slug, booking_form_usage, preview_target ) {
267 var request_body = new window.FormData();
268
269 request_body.append( 'action', config.preview_action );
270 request_body.append( 'nonce', config.nonce || '' );
271 request_body.append( 'template_slug', template_slug );
272 request_body.append( 'booking_form_usage', booking_form_usage );
273 request_body.append( 'preview_target', preview_target );
274
275 return request_body;
276 }
277
278 /**
279 * Confirm that a response belongs to the current server-validated selection.
280 *
281 * @param {Object} response WordPress AJAX response.
282 * @param {string} template_slug Requested template slug.
283 * @param {string} booking_form_usage Requested usage identifier.
284 * @param {string} preview_target Requested preview target.
285 * @return {Object} Validated response data.
286 * @throws {Error} When response ownership or shape is invalid.
287 */
288 function validate_preview_response( response, template_slug, booking_form_usage, preview_target ) {
289 var response_data;
290 var selected_card = get_selected_card();
291
292 if ( ! response || ! response.success || ! response.data ) {
293 throw new Error( get_ajax_error_message( response ) );
294 }
295
296 response_data = response.data;
297 if (
298 String( response_data.template_slug || '' ) !== template_slug ||
299 String( response_data.booking_form_usage || '' ) !== booking_form_usage ||
300 String( response_data.preview_target || '' ) !== preview_target ||
301 ! selected_card ||
302 String( selected_card.dataset.templateSlug || '' ) !== template_slug ||
303 get_selected_booking_form_usage() !== booking_form_usage
304 ) {
305 throw new Error( get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' ) );
306 }
307
308 return response_data;
309 }
310
311 /**
312 * Request and render the selected allow-listed template.
313 *
314 * @param {HTMLElement|null} card Selected template card.
315 * @return {void}
316 */
317 function load_preview( card ) {
318 var template_slug = card ? String( card.dataset.templateSlug || '' ) : '';
319 var booking_form_usage = get_selected_booking_form_usage();
320 var request_sequence;
321 var request_body;
322
323 if ( ! template_slug || ! booking_form_usage || ! config.ajax_url || ! config.preview_action ) {
324 set_preview_state( false, get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' ) );
325 return;
326 }
327
328 if ( preview_abort_controller && 'function' === typeof preview_abort_controller.abort ) {
329 preview_abort_controller.abort();
330 }
331 preview_abort_controller = 'function' === typeof window.AbortController ? new window.AbortController() : null;
332 request_sequence = ++preview_request_sequence;
333 request_body = build_preview_request_body( template_slug, booking_form_usage, 'embedded' );
334
335 set_preview_state( true, '' );
336
337 window.fetch( config.ajax_url, {
338 method: 'POST',
339 body: request_body,
340 credentials: 'same-origin',
341 signal: preview_abort_controller ? preview_abort_controller.signal : undefined
342 } ).then( function ( response ) {
343 return response.json();
344 } ).then( function ( response ) {
345 var response_data;
346 var preview_kind;
347 var preview_url;
348
349 if ( request_sequence !== preview_request_sequence ) {
350 return;
351 }
352
353 response_data = validate_preview_response( response, template_slug, booking_form_usage, 'embedded' );
354 preview_kind = String( response_data.preview_kind || '' );
355
356 if (
357 'inline' === preview_kind &&
358 response_data.html &&
359 response_data.bootstrap &&
360 'object' === typeof response_data.bootstrap &&
361 inline_preview_renderer
362 ) {
363 return inline_preview_renderer.replace( String( response_data.html ), response_data.bootstrap, function () {
364 return request_sequence === preview_request_sequence;
365 } ).then( function () {
366 if ( request_sequence !== preview_request_sequence ) {
367 return;
368 }
369 clear_preview_frame();
370 editor.inline_preview.hidden = false;
371 set_current_preview( 'inline', '', template_slug, booking_form_usage );
372 set_preview_state( false, '' );
373 } );
374 }
375
376 if ( 'url' !== preview_kind || ! response_data.preview_url ) {
377 throw new Error( get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' ) );
378 }
379
380 preview_url = String( response_data.preview_url );
381 editor.preview_frame.onload = function () {
382 if ( request_sequence !== preview_request_sequence ) {
383 return;
384 }
385 editor.inline_preview.hidden = true;
386 editor.preview_frame.hidden = false;
387 set_current_preview( 'url', preview_url, template_slug, booking_form_usage );
388 set_preview_state( false, '' );
389 };
390 editor.preview_frame.setAttribute( 'data-preview-template-slug', template_slug );
391 editor.preview_frame.setAttribute( 'data-preview-booking-form-usage', booking_form_usage );
392 editor.preview_frame.src = preview_url;
393 } ).catch( function ( error ) {
394 if ( error && 'AbortError' === error.name ) {
395 return;
396 }
397 if ( request_sequence !== preview_request_sequence ) {
398 return;
399 }
400 set_preview_state( false, error && error.message ? error.message : get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' ) );
401 } );
402 }
403
404 /**
405 * Open the full-site preview, lazily creating a signed session when needed.
406 *
407 * @param {HTMLElement|null} card Selected template card.
408 * @return {void}
409 */
410 function open_preview( card ) {
411 var template_slug = card ? String( card.dataset.templateSlug || '' ) : '';
412 var booking_form_usage = get_selected_booking_form_usage();
413 var preview_window;
414 var request_sequence;
415
416 if ( ! template_slug || ! booking_form_usage || editor.open_button.disabled ) {
417 return;
418 }
419
420 if ( current_preview_matches( template_slug, booking_form_usage ) && current_preview.url ) {
421 window.open( current_preview.url, '_blank', 'noopener,noreferrer' );
422 return;
423 }
424
425 preview_window = window.open( '', '_blank' );
426 if ( ! preview_window ) {
427 set_preview_state( false, get_message( 'preview_open_error', 'The full-site preview could not be opened. Try again.' ) );
428 return;
429 }
430 preview_window.opener = null;
431
432 if ( open_abort_controller && 'function' === typeof open_abort_controller.abort ) {
433 open_abort_controller.abort();
434 }
435 open_abort_controller = 'function' === typeof window.AbortController ? new window.AbortController() : null;
436 request_sequence = ++open_request_sequence;
437 open_request_is_loading = true;
438 editor.open_button.disabled = true;
439
440 window.fetch( config.ajax_url, {
441 method: 'POST',
442 body: build_preview_request_body( template_slug, booking_form_usage, 'window' ),
443 credentials: 'same-origin',
444 signal: open_abort_controller ? open_abort_controller.signal : undefined
445 } ).then( function ( response ) {
446 return response.json();
447 } ).then( function ( response ) {
448 var response_data;
449
450 if ( request_sequence !== open_request_sequence ) {
451 return;
452 }
453 response_data = validate_preview_response( response, template_slug, booking_form_usage, 'window' );
454 if ( 'url' !== String( response_data.preview_kind || '' ) || ! response_data.preview_url ) {
455 throw new Error( get_message( 'preview_open_error', 'The full-site preview could not be opened. Try again.' ) );
456 }
457
458 preview_window.location.replace( String( response_data.preview_url ) );
459 } ).catch( function ( error ) {
460 if ( error && 'AbortError' === error.name ) {
461 preview_window.close();
462 return;
463 }
464 if ( request_sequence !== open_request_sequence ) {
465 return;
466 }
467 preview_window.close();
468 set_preview_state( false, error && error.message ? error.message : get_message( 'preview_open_error', 'The full-site preview could not be opened. Try again.' ) );
469 } ).then( function () {
470 if ( request_sequence === open_request_sequence ) {
471 open_request_is_loading = false;
472 editor.open_button.disabled = ! get_selected_card();
473 }
474 } );
475 }
476
477 /**
478 * Select one card and load its real preview.
479 *
480 * @param {HTMLElement} card Template card.
481 * @param {boolean} load_selected Whether to request a replacement preview.
482 * @return {void}
483 */
484 function select_card( card, load_selected ) {
485 var radio = card && card.querySelector( '[data-wpbc-template-radio]' );
486
487 if ( ! radio ) {
488 return;
489 }
490
491 radio.checked = true;
492 editor.cards.forEach( function ( template_card ) {
493 template_card.classList.toggle( 'is-selected', template_card === card );
494 } );
495 shell_api.clear_field_error( 'booking_form_template' );
496 render_preview_metadata( card );
497 if ( false !== load_selected ) {
498 load_preview( card );
499 }
500 }
501
502 /**
503 * Return cards matching the current search and category.
504 *
505 * @return {HTMLElement[]} Matching cards in server order.
506 */
507 function get_matching_cards() {
508 var query = normalize_search_text( editor.search ? editor.search.value.trim() : '' );
509
510 return editor.cards.filter( function ( card ) {
511 var matches_filter = 'all' === editor.active_filter ||
512 ( 'recommended' === editor.active_filter && 'true' === card.dataset.templateRecommended ) ||
513 editor.active_filter === card.dataset.templateCategory;
514 var matches_search = ! query || -1 !== get_card_search_text( card ).indexOf( query );
515
516 return matches_filter && matches_search;
517 } );
518 }
519
520 /**
521 * Render every card matching the current search and category.
522 *
523 * @return {void}
524 */
525 function render_catalog() {
526 var matching_cards = get_matching_cards();
527
528 editor.cards.forEach( function ( card ) {
529 card.hidden = true;
530 } );
531 matching_cards.forEach( function ( card ) {
532 card.hidden = false;
533 } );
534
535 editor.empty.hidden = 0 !== matching_cards.length;
536 editor.empty.textContent = get_message( 'no_matching_templates', 'No templates match this search and filter.' );
537 editor.grid.scrollLeft = 0;
538 }
539
540 /**
541 * Activate one category tab and refresh the visible cards.
542 *
543 * @param {HTMLElement} tab Filter tab button.
544 * @return {void}
545 */
546 function activate_filter( tab ) {
547 if ( ! tab ) {
548 return;
549 }
550
551 editor.active_filter = tab.dataset.wpbcTemplateFilter || 'all';
552 editor.tabs.forEach( function ( filter_tab ) {
553 var is_active = filter_tab === tab;
554 filter_tab.classList.toggle( 'is-active', is_active );
555 filter_tab.setAttribute( 'aria-selected', is_active ? 'true' : 'false' );
556 filter_tab.tabIndex = is_active ? 0 : -1;
557 } );
558 editor.grid.setAttribute( 'aria-labelledby', tab.id );
559 render_catalog();
560 }
561
562 /**
563 * Return the filter tab with the requested stable ID.
564 *
565 * @param {string} filter_id Stable filter ID.
566 * @return {HTMLElement|null} Matching tab, or null.
567 */
568 function get_filter_tab( filter_id ) {
569 return editor.tabs.find( function ( tab ) {
570 return filter_id === tab.dataset.wpbcTemplateFilter;
571 } ) || null;
572 }
573
574 /**
575 * Apply text-search changes immediately.
576 *
577 * Text search intentionally switches to All templates. Otherwise a valid
578 * match such as a Full Days or contact template can remain hidden merely
579 * because the Recommended tab was active before the user started typing.
580 *
581 * @return {void}
582 */
583 function handle_search_change() {
584 var query = normalize_search_text( editor.search ? editor.search.value.trim() : '' );
585 var all_templates_tab = get_filter_tab( 'all' );
586
587 if ( query && all_templates_tab && 'all' !== editor.active_filter ) {
588 activate_filter( all_templates_tab );
589 return;
590 }
591
592 render_catalog();
593 }
594
595 /**
596 * Handle selector and preview actions.
597 *
598 * @param {MouseEvent} event Click event.
599 * @return {void}
600 */
601 function handle_click( event ) {
602 var filter = event.target.closest( '[data-wpbc-template-filter]' );
603 var refresh_button = event.target.closest( '[data-wpbc-template-refresh]' );
604 var open_link = event.target.closest( '[data-wpbc-template-open-preview]' );
605 var template_card = event.target.closest( '[data-wpbc-template-card]' );
606 var template_radio;
607
608 if ( filter ) {
609 activate_filter( filter );
610 return;
611 }
612 if ( refresh_button ) {
613 load_preview( get_selected_card() );
614 return;
615 }
616 if ( open_link ) {
617 open_preview( get_selected_card() );
618 return;
619 }
620 if ( template_card ) {
621 template_radio = template_card.querySelector( '[data-wpbc-template-radio]' );
622 if ( template_radio && ! template_radio.checked ) {
623 event.preventDefault();
624 select_card( template_card, true );
625 template_radio.focus();
626 }
627 return;
628 }
629 }
630
631 /**
632 * Synchronize one entry-point choice with its native radio and card.
633 *
634 * @param {string} booking_form_usage Stable entry-point identifier.
635 * @return {HTMLInputElement|null} Matching radio control, or null.
636 */
637 function select_booking_form_usage( booking_form_usage ) {
638 var selected_radio = null;
639
640 Array.prototype.slice.call( editor.node.querySelectorAll( '[data-wpbc-booking-form-usage-radio]' ) ).forEach( function ( usage_radio ) {
641 var is_selected = booking_form_usage === String( usage_radio.value || '' );
642
643 usage_radio.checked = is_selected;
644 usage_radio.closest( '.wpbc_setup_wizard__booking-form-usage-option' ).classList.toggle( 'is-selected', is_selected );
645 if ( is_selected ) {
646 selected_radio = usage_radio;
647 }
648 } );
649
650 return selected_radio;
651 }
652
653 /**
654 * Commit an explained entry-point choice and refresh its preview.
655 *
656 * @param {string} booking_form_usage Stable entry-point identifier.
657 * @return {void}
658 */
659 function commit_booking_form_usage( booking_form_usage ) {
660 if ( ! select_booking_form_usage( booking_form_usage ) ) {
661 return;
662 }
663
664 confirmed_booking_form_usage = booking_form_usage;
665 shell_api.clear_field_error( 'booking_form_usage' );
666 render_preview_metadata( get_selected_card() );
667 load_preview( get_selected_card() );
668 }
669
670 /**
671 * Commit or roll back the provisional Appointment flow radio selection.
672 *
673 * @param {CustomEvent} event Shared dialog lifecycle event.
674 * @return {void}
675 */
676 function handle_dialog_closed( event ) {
677 var dialog_state = event.detail || {};
678
679 if ( appointment_flow_dialog_action !== dialog_state.action ) {
680 return;
681 }
682
683 if ( 'confirm' === dialog_state.outcome ) {
684 commit_booking_form_usage( 'appointment_flow' );
685 return;
686 }
687
688 select_booking_form_usage( confirmed_booking_form_usage );
689 }
690
691 /**
692 * Handle native template radio changes.
693 *
694 * @param {Event} event Change event.
695 * @return {void}
696 */
697 function handle_change( event ) {
698 if ( event.target.matches( '[data-wpbc-template-radio]' ) ) {
699 select_card( event.target.closest( '[data-wpbc-template-card]' ), true );
700 return;
701 }
702
703 if ( event.target.matches( '[data-wpbc-booking-form-usage-radio]' ) ) {
704 select_booking_form_usage( String( event.target.value || '' ) );
705 shell_api.clear_field_error( 'booking_form_usage' );
706
707 if (
708 'appointment_flow' === event.target.value &&
709 'appointment_flow' !== confirmed_booking_form_usage &&
710 'function' === typeof shell_api.open_dialog
711 ) {
712 if ( shell_api.open_dialog( appointment_flow_dialog_action, event.target ) ) {
713 return;
714 }
715 }
716
717 commit_booking_form_usage( String( event.target.value || '' ) );
718 }
719 }
720
721 /**
722 * Provide arrow-key navigation across horizontally scrollable filter tabs.
723 *
724 * @param {KeyboardEvent} event Keyboard event.
725 * @return {void}
726 */
727 function handle_keydown( event ) {
728 var tab = event.target.closest( '[data-wpbc-template-filter]' );
729 var current_index;
730 var target_index;
731
732 if ( ! tab || -1 === [ 'ArrowLeft', 'ArrowRight', 'Home', 'End' ].indexOf( event.key ) ) {
733 return;
734 }
735
736 event.preventDefault();
737 current_index = editor.tabs.indexOf( tab );
738 target_index = current_index;
739 if ( 'ArrowLeft' === event.key ) {
740 target_index = ( current_index - 1 + editor.tabs.length ) % editor.tabs.length;
741 } else if ( 'ArrowRight' === event.key ) {
742 target_index = ( current_index + 1 ) % editor.tabs.length;
743 } else if ( 'Home' === event.key ) {
744 target_index = 0;
745 } else if ( 'End' === event.key ) {
746 target_index = editor.tabs.length - 1;
747 }
748
749 activate_filter( editor.tabs[ target_index ] );
750 editor.tabs[ target_index ].focus();
751 editor.tabs[ target_index ].scrollIntoView( { block: 'nearest', inline: 'nearest' } );
752 }
753
754 /**
755 * Initialize this module against shell-provided APIs.
756 *
757 * @param {Object} registered_shell_api Shared wizard adapter services.
758 * @return {void}
759 */
760 function initialize( registered_shell_api ) {
761 var editor_node;
762 var selected_card;
763 var initial_preview_kind;
764 var initial_template_slug;
765 var initial_booking_form_usage;
766 var initial_preview_bootstrap;
767 var initial_preview_html;
768
769 shell_api = registered_shell_api;
770 root = shell_api.root;
771 editor_node = root.querySelector( '[data-wpbc-booking-form-template-editor]' );
772 if ( ! editor_node ) {
773 return;
774 }
775
776 editor = {
777 node: editor_node,
778 cards: Array.prototype.slice.call( editor_node.querySelectorAll( '[data-wpbc-template-card]' ) ),
779 tabs: Array.prototype.slice.call( editor_node.querySelectorAll( '[data-wpbc-template-filter]' ) ),
780 search: editor_node.querySelector( '[data-wpbc-template-search]' ),
781 grid: editor_node.querySelector( '[data-wpbc-template-grid]' ),
782 empty: editor_node.querySelector( '[data-wpbc-template-empty]' ),
783 preview_stage: editor_node.querySelector( '[data-wpbc-template-preview-stage]' ),
784 preview_loader: editor_node.querySelector( '[data-wpbc-template-preview-loader]' ),
785 preview_error: editor_node.querySelector( '[data-wpbc-template-preview-error]' ),
786 inline_preview: editor_node.querySelector( '[data-wpbc-template-inline-preview]' ),
787 inline_preview_content: editor_node.querySelector( '[data-wpbc-template-inline-preview-content]' ),
788 initial_preview_bootstrap: editor_node.querySelector( '[data-wpbc-template-initial-preview-bootstrap]' ),
789 preview_frame: editor_node.querySelector( '[data-wpbc-template-preview-frame]' ),
790 refresh_button: editor_node.querySelector( '[data-wpbc-template-refresh]' ),
791 open_button: editor_node.querySelector( '[data-wpbc-template-open-preview]' ),
792 active_filter: editor_node.dataset.initialFilter || 'all'
793 };
794
795 if (
796 window.wpbc_bfb_inline_preview &&
797 'function' === typeof window.wpbc_bfb_inline_preview.create
798 ) {
799 inline_preview_renderer = window.wpbc_bfb_inline_preview.create( editor.inline_preview_content );
800 }
801
802 editor.node.addEventListener( 'click', handle_click );
803 editor.node.addEventListener( 'change', handle_change );
804 editor.node.addEventListener( 'keydown', handle_keydown );
805 root.addEventListener( 'wpbc:setup-wizard-dialog-closed', handle_dialog_closed );
806 confirmed_booking_form_usage = get_selected_booking_form_usage();
807 if ( editor.search ) {
808 editor.search.addEventListener( 'input', handle_search_change );
809 editor.search.addEventListener( 'search', handle_search_change );
810 editor.search.addEventListener( 'change', handle_search_change );
811 }
812
813 editor.cards.forEach( function ( card ) {
814 initialize_image_fallback(
815 card.querySelector( '[data-wpbc-template-card-image]' ),
816 card.querySelector( '[data-wpbc-template-image-placeholder]' )
817 );
818 } );
819
820 render_catalog();
821 selected_card = get_selected_card();
822 if ( selected_card ) {
823 initial_preview_kind = String( editor.preview_stage.dataset.initialPreviewKind || '' );
824 initial_template_slug = String( editor.preview_stage.dataset.initialPreviewTemplateSlug || '' );
825 initial_booking_form_usage = String( editor.preview_stage.dataset.initialPreviewBookingFormUsage || '' );
826 select_card( selected_card, false );
827
828 if (
829 'inline' === initial_preview_kind &&
830 initial_template_slug === String( selected_card.dataset.templateSlug || '' ) &&
831 initial_booking_form_usage === get_selected_booking_form_usage() &&
832 inline_preview_renderer &&
833 editor.initial_preview_bootstrap
834 ) {
835 try {
836 initial_preview_bootstrap = JSON.parse( editor.initial_preview_bootstrap.textContent || '{}' );
837 } catch ( error ) {
838 initial_preview_bootstrap = null;
839 }
840 initial_preview_html = editor.inline_preview_content.innerHTML;
841 if ( initial_preview_bootstrap && 'object' === typeof initial_preview_bootstrap ) {
842 inline_preview_renderer.replace( initial_preview_html, initial_preview_bootstrap )
843 .done( function () {
844 set_current_preview( 'inline', '', initial_template_slug, initial_booking_form_usage );
845 set_preview_state( false, '' );
846 } )
847 .fail( function () {
848 load_preview( selected_card );
849 } );
850 } else {
851 load_preview( selected_card );
852 }
853 } else {
854 load_preview( selected_card );
855 }
856 } else {
857 render_preview_metadata( null );
858 set_preview_state( false, get_message( 'preview_error', 'The interactive preview could not be loaded. Try refreshing it.' ) );
859 }
860
861 if ( 'function' === typeof shell_api.open_dialog ) {
862 window.setTimeout( function () {
863 shell_api.open_dialog( template_guidance_dialog_action, editor.search || editor.node );
864 }, 0 );
865 }
866 }
867
868 /**
869 * Validate the required selection before forward navigation.
870 *
871 * @return {HTMLElement|null} First invalid radio, or null.
872 */
873 function validate() {
874 var selected_radio = editor && editor.node.querySelector( '[data-wpbc-template-radio]:checked' );
875 var selected_usage = editor && editor.node.querySelector( '[data-wpbc-booking-form-usage-radio]:checked' );
876
877 if ( ! selected_radio ) {
878 return shell_api.set_field_error( 'booking_form_template', get_message( 'template_required', 'Choose a booking form template.' ) );
879 }
880
881 if ( ! selected_usage ) {
882 return shell_api.set_field_error( 'booking_form_usage', get_message( 'usage_required', 'Choose how customers will open the booking form.' ) );
883 }
884
885 return null;
886 }
887
888 return {
889 initialize: initialize,
890 sync: function () {},
891 validate: validate
892 };
893 }
894
895 window.wpbc_setup_editor_modules = window.wpbc_setup_editor_modules || {};
896 window.wpbc_setup_editor_modules.booking_form_template = {
897 create: create_booking_form_template_adapter
898 };
899
900 if ( window.wpbc_setup_wizard_api && 'function' === typeof window.wpbc_setup_wizard_api.register_step_adapter ) {
901 window.wpbc_setup_wizard_api.register_step_adapter( create_booking_form_template_adapter( module_config ) );
902 }
903 }( window, document ) );
904