PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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.7, at includes/booking-resource-selector/_src/booking-resource-selector.js

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