PluginProbe
Booking Calendar / 11.5
Booking Calendar v11.5
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 / booking-resource-selector / _src / booking-resource-selector.js

booking-resource-selector.js in Booking Calendar 11.5, at includes/booking-resource-selector/_src/booking-resource-selector.js

390 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function ( window, $ ) {
2 'use strict';
3
4 var config = window.wpbc_booking_resource_selector_config || {};
5 var active_native_contexts = {};
6 var loaded_script_urls = {};
7
8 $( 'script[src]' ).each( function () {
9 loaded_script_urls[ String( this.src || '' ) ] = true;
10 } );
11
12 /** Return a normalized Booking Resource ID from a selection form. */
13 function get_selected_resource_id( $form ) {
14 return Number( $form.find( '[name="wpbc_resource_selector_resource"]:checked' ).first().val() || 0 );
15 }
16
17 /** Toggle one selector loading state without clearing its current stage. */
18 function set_loading( $root, is_loading ) {
19 $root.toggleClass( 'is-loading', is_loading ).attr( 'aria-busy', is_loading ? 'true' : 'false' );
20 $root.find( '> .wpbc_booking_resource_selector__stage' ).attr( 'aria-busy', is_loading ? 'true' : 'false' );
21 $root.find( '> .wpbc_booking_resource_selector__loading' ).prop( 'hidden', ! is_loading ).attr( 'aria-hidden', is_loading ? 'false' : 'true' );
22 $root.find( '.wpbc_booking_resource_selector__selection_form :input' ).prop( 'disabled', is_loading );
23 }
24
25 /** Display and focus one controlled AJAX or initialization error. */
26 function show_error( $root, message ) {
27 var $notice = $root.find( '> .wpbc_booking_resource_selector__ajax_notice' );
28 $notice.empty().append( $( '<span>' ).text( message || config.error || 'Unable to load the booking form.' ) ).prop( 'hidden', false );
29 if ( $notice.get( 0 ) && typeof $notice.get( 0 ).focus === 'function' ) {
30 $notice.trigger( 'focus' );
31 }
32 }
33
34 /** Clear the selector AJAX error. */
35 function clear_error( $root ) {
36 $root.find( '> .wpbc_booking_resource_selector__ajax_notice' ).empty().prop( 'hidden', true );
37 }
38
39 /** Return a registered context only while its native form remains live. */
40 function get_native_context( resource_id ) {
41 resource_id = Number( resource_id || 0 );
42 var context = active_native_contexts[ resource_id ];
43 if ( ! context || ! context.element || ! document.documentElement.contains( context.element ) ) {
44 delete active_native_contexts[ resource_id ];
45 return null;
46 }
47 return context;
48 }
49
50 /** Detect any other live native Booking Calendar form for the same resource. */
51 function has_duplicate_resource_form( $root, resource_id ) {
52 resource_id = Number( resource_id || 0 );
53 if ( ! resource_id ) {
54 return false;
55 }
56
57 var context = get_native_context( resource_id );
58 if ( context && ! $.contains( $root.get( 0 ), context.element ) ) {
59 return true;
60 }
61
62 return $( '[id="booking_form' + resource_id + '"]' ).filter( function () {
63 return ! $.contains( $root.get( 0 ), this );
64 } ).length > 0;
65 }
66
67 /** Register the signed resource context used by final booking submission. */
68 function register_native_form( $native ) {
69 var resource_id = Number( $native.data( 'resource-id' ) || 0 );
70 var context_token = String( $native.attr( 'data-resource-selector-context-token' ) || '' );
71 var allow_past = '1' === String( $native.attr( 'data-allow-past' ) || '0' ) ? 1 : 0;
72 var existing = get_native_context( resource_id );
73
74 if ( ! resource_id || ! context_token ) {
75 return false;
76 }
77 if ( existing && existing.element !== $native.get( 0 ) ) {
78 return false;
79 }
80
81 active_native_contexts[ resource_id ] = {
82 element: $native.get( 0 ),
83 resource_id: resource_id,
84 context_token: context_token,
85 allow_past: allow_past
86 };
87 return true;
88 }
89
90 /** Remove one native form from the local submission registry. */
91 function unregister_native_form( $native ) {
92 var resource_id = Number( $native.data( 'resource-id' ) || 0 );
93 var context = get_native_context( resource_id );
94 if ( context && context.element === $native.get( 0 ) ) {
95 delete active_native_contexts[ resource_id ];
96 }
97 }
98
99 /** Prepare a newly inserted native form for resource-bound submission. */
100 function prepare_native_form( $scope ) {
101 var $native = $scope.find( '.wpbc_booking_resource_selector__native_form' ).first();
102 if ( ! $native.length ) {
103 return true;
104 }
105
106 return register_native_form( $native );
107 }
108
109 /** Convert a script URL to the same absolute form used by script elements. */
110 function get_absolute_script_url( url ) {
111 var anchor = document.createElement( 'a' );
112 anchor.href = String( url || '' );
113 return anchor.href;
114 }
115
116 /** Return a rejected promise carrying one controlled message. */
117 function rejected_stage( message ) {
118 var deferred = $.Deferred();
119 deferred.reject( { wpbc_message: message } );
120 return deferred.promise();
121 }
122
123 /** Execute renderer scripts sequentially while the request owns the stage. */
124 function execute_scripts( scripts, owns_stage ) {
125 var sequence = $.Deferred().resolve().promise();
126
127 $.each( scripts, function ( index, script ) {
128 sequence = sequence.then( function () {
129 if ( ! owns_stage() ) {
130 return rejected_stage( '' );
131 }
132 if ( script.src ) {
133 var absolute_url = get_absolute_script_url( script.src );
134 if ( loaded_script_urls[ absolute_url ] ) {
135 return undefined;
136 }
137 return $.ajax( { url: absolute_url, dataType: 'script', cache: true } ).then( function () {
138 loaded_script_urls[ absolute_url ] = true;
139 } );
140 }
141 if ( script.code ) {
142 $.globalEval( script.code );
143 }
144 return undefined;
145 } );
146 } );
147
148 return sequence;
149 }
150
151 /** Initialize native controls whose core handlers bind on document ready. */
152 function initialize_ajax_form_controls() {
153 if ( typeof window.wpbc_hook__init_booking_form_wizard_buttons === 'function' ) {
154 window.wpbc_hook__init_booking_form_wizard_buttons();
155 }
156 }
157
158 /** Destroy native calendars and unregister context before stage removal. */
159 function cleanup_native_form( $root ) {
160 $root.find( '.wpbc_booking_resource_selector__native_form' ).each( function () {
161 var $native = $( this );
162 var resource_id = Number( $native.data( 'resource-id' ) || 0 );
163 var $calendar = $native.find( '#calendar_booking' + resource_id );
164
165 unregister_native_form( $native );
166 if ( ! resource_id || ! $calendar.length || ! $.datepick || typeof $calendar.datepick !== 'function' ) {
167 return;
168 }
169
170 try {
171 var instance = typeof $.datepick._getInst === 'function' ? $.datepick._getInst( $calendar.get( 0 ) ) : null;
172 if ( instance ) {
173 $calendar.datepick( 'destroy' );
174 }
175 } catch ( error ) {
176 $calendar.removeClass( 'hasDatepick' );
177 }
178 } );
179 }
180
181 /** Restore the configured initial Resource choice after Start over. */
182 function restore_resource_selection( $root ) {
183 var resource_id = Number( $root.attr( 'data-selected-resource-id' ) || 0 );
184 if ( ! resource_id ) {
185 return;
186 }
187 var $input = $root.find( '[name="wpbc_resource_selector_resource"][value="' + resource_id + '"]' ).first();
188 if ( $input.length ) {
189 $input.closest( '.wpbc_booking_resource_selector__choices' ).find( '.wpbc_booking_resource_selector__choice' ).removeClass( 'is-selected' );
190 $input.prop( 'checked', true ).closest( '.wpbc_booking_resource_selector__choice' ).addClass( 'is-selected' );
191 }
192 }
193
194 /** Focus the new stage heading and keep the component near the viewport. */
195 function focus_stage( $root ) {
196 var $target = $root.find( '> .wpbc_booking_resource_selector__stage .wpbc_booking_resource_selector__heading h3, > .wpbc_booking_resource_selector__stage .wpbc_booking_resource_selector__notice' ).first();
197 if ( $target.length ) {
198 $target.attr( 'tabindex', '-1' );
199 try {
200 $target.get( 0 ).focus( { preventScroll: true } );
201 } catch ( error ) {
202 $target.trigger( 'focus' );
203 }
204 }
205
206 if ( $root.get( 0 ) && typeof $root.get( 0 ).scrollIntoView === 'function' ) {
207 var reduce_motion = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
208 $root.get( 0 ).scrollIntoView( { behavior: reduce_motion ? 'auto' : 'smooth', block: 'nearest' } );
209 }
210 }
211
212 /** Determine whether an AJAX callback still owns the component state. */
213 function is_current_request( $root, request_id ) {
214 return Number( $root.data( 'wpbc-resource-selector-request-id' ) || 0 ) === Number( request_id );
215 }
216
217 /** Finish only the current request so stale callbacks cannot alter the UI. */
218 function finish_request( $root, request_id ) {
219 if ( ! is_current_request( $root, request_id ) ) {
220 return;
221 }
222 $root.removeData( 'wpbc-resource-selector-request' );
223 set_loading( $root, false );
224 }
225
226 /** Replace a complete stage with DOM-before-script initialization ordering. */
227 function replace_stage( $root, html, stage, resource_id, request_id ) {
228 if ( ! is_current_request( $root, request_id ) ) {
229 return rejected_stage( '' );
230 }
231 if ( 'booking' === stage && has_duplicate_resource_form( $root, resource_id ) ) {
232 return rejected_stage( config.duplicate_resource );
233 }
234
235 var parsed = $.parseHTML( String( html || '' ), document, true ) || [];
236 var scripts = [];
237 var $container = $( '<div>' ).append( parsed );
238
239 $container.find( 'script' ).addBack( 'script' ).each( function () {
240 scripts.push( { src: this.src || '', code: this.src ? '' : ( this.text || this.textContent || '' ) } );
241 $( this ).remove();
242 } );
243
244 cleanup_native_form( $root );
245 $root.attr( 'data-resource-selector-stage', stage );
246 $root.find( '> .wpbc_booking_resource_selector__stage' ).empty().append( $container.contents() );
247
248 if ( ! prepare_native_form( $root ) ) {
249 cleanup_native_form( $root );
250 $root.find( '.wpbc_booking_resource_selector__native_form :input' ).prop( 'disabled', true );
251 return rejected_stage( config.initialization_error || config.error );
252 }
253
254 return execute_scripts( scripts, function () {
255 return is_current_request( $root, request_id );
256 } ).then( function () {
257 if ( ! is_current_request( $root, request_id ) ) {
258 return rejected_stage( '' );
259 }
260 initialize_ajax_form_controls();
261 if ( 'resource' === stage ) {
262 restore_resource_selection( $root );
263 }
264 } );
265 }
266
267 /** Request and render the next Booking Resource selector stage. */
268 function resolve_stage( $root, resource_id ) {
269 if ( ! $root || ! $root.length ) {
270 return;
271 }
272
273 resource_id = Number( resource_id || 0 );
274 if ( resource_id ) {
275 $root.attr( 'data-selected-resource-id', resource_id );
276 }
277
278 var previous_request = $root.data( 'wpbc-resource-selector-request' );
279 var request_id = Number( $root.data( 'wpbc-resource-selector-request-id' ) || 0 ) + 1;
280 $root.data( 'wpbc-resource-selector-request-id', request_id );
281 if ( previous_request && previous_request.readyState !== 4 ) {
282 previous_request.abort();
283 }
284
285 clear_error( $root );
286 set_loading( $root, true );
287 var request = $.post( config.ajax_url, {
288 action: config.action,
289 nonce: config.nonce,
290 config_token: $root.attr( 'data-config-token' ) || '',
291 resource_id: resource_id
292 } );
293 $root.data( 'wpbc-resource-selector-request', request );
294
295 request.done( function ( response ) {
296 if ( ! is_current_request( $root, request_id ) ) {
297 return;
298 }
299 if ( ! response || ! response.success || ! response.data ) {
300 show_error( $root, response && response.data && response.data.message ? response.data.message : config.error );
301 finish_request( $root, request_id );
302 return;
303 }
304
305 var stage = response.data.stage || '';
306 var replacement = replace_stage( $root, response.data.html, stage, response.data.resource_id, request_id );
307 replacement.done( function () {
308 if ( ! is_current_request( $root, request_id ) ) {
309 return;
310 }
311 if ( Number( response.data.resource_id || 0 ) ) {
312 $root.attr( 'data-selected-resource-id', Number( response.data.resource_id ) );
313 }
314 finish_request( $root, request_id );
315 focus_stage( $root );
316 } ).fail( function ( error ) {
317 if ( ! is_current_request( $root, request_id ) ) {
318 return;
319 }
320 show_error( $root, error && error.wpbc_message ? error.wpbc_message : ( config.initialization_error || config.error ) );
321 finish_request( $root, request_id );
322 } );
323 } ).fail( function ( xhr, status ) {
324 if ( 'abort' === status || ! is_current_request( $root, request_id ) ) {
325 return;
326 }
327 var response = xhr.responseJSON;
328 show_error( $root, response && response.data && response.data.message ? response.data.message : config.error );
329 finish_request( $root, request_id );
330 } );
331 }
332
333 /** Resolve the selected Booking Resource through AJAX. */
334 $( document ).on( 'submit', '.wpbc_booking_resource_selector__selection_form', function ( event ) {
335 if ( ! config.ajax_url || ! config.action ) {
336 return;
337 }
338 event.preventDefault();
339 var $form = $( this );
340 resolve_stage( $form.closest( '.wpbc_booking_resource_selector' ), get_selected_resource_id( $form ) );
341 } );
342
343 /** Keep selected card styling independent from CSS :has() support. */
344 $( document ).on( 'change', '.wpbc_booking_resource_selector__choice > input', function () {
345 var $input = $( this );
346 $input.closest( '.wpbc_booking_resource_selector__choices' ).find( '.wpbc_booking_resource_selector__choice' ).removeClass( 'is-selected' );
347 $input.closest( '.wpbc_booking_resource_selector__choice' ).addClass( 'is-selected' );
348 } );
349
350 /** Return to Resource selection without reloading the public page. */
351 $( document ).on( 'click', '.wpbc_booking_resource_selector [data-wpbc-resource-selector-action="start-over"]', function ( event ) {
352 if ( ! config.ajax_url || ! config.action ) {
353 return;
354 }
355 event.preventDefault();
356 var $root = $( this ).closest( '.wpbc_booking_resource_selector' );
357 if ( $root.hasClass( 'is-loading' ) ) {
358 return;
359 }
360 resolve_stage( $root, 0 );
361 } );
362
363 /** Add the signed resource context to the core booking-create request. */
364 $( 'body' ).on( 'wpbc_before_booking_create.wpbc_booking_resource_selector', function ( event, resource_id, params ) {
365 var context = get_native_context( resource_id );
366 if ( ! context || ! params ) {
367 return;
368 }
369 params.resource_selector_required = 1;
370 params.resource_selector_context_token = context.context_token;
371 params.allow_past = context.allow_past;
372 } );
373
374 $( function () {
375 $( '.wpbc_booking_resource_selector' ).each( function () {
376 var $root = $( this );
377 var $native = $root.find( '.wpbc_booking_resource_selector__native_form' ).first();
378 if ( $native.length ) {
379 $root.attr( 'data-selected-resource-id', Number( $native.data( 'resource-id' ) || 0 ) );
380 }
381 var duplicate = $native.length && has_duplicate_resource_form( $root, Number( $native.data( 'resource-id' ) || 0 ) );
382 if ( duplicate || ! prepare_native_form( $root ) ) {
383 cleanup_native_form( $root );
384 $root.find( '.wpbc_booking_resource_selector__native_form :input' ).prop( 'disabled', true );
385 show_error( $root, duplicate ? config.duplicate_resource : config.initialization_error );
386 }
387 } );
388 } );
389 } )( window, jQuery );
390