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-availability-timeslots / _src / availability_timeslots_page.js

availability_timeslots_page.js in Booking Calendar 11.9, at includes/page-availability-timeslots/_src/availability_timeslots_page.js

1,861 lines 57.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Time Slots Availability UI.
3 */
4 ( function ( $ ) {
5 'use strict';
6
7 var zoomSteps = [ 60, 30, 15, 10, 5 ];
8 var currentMode = 'block';
9 var selectionRanges = [];
10 var activeSelectionId = '';
11 var rowTemplates = [];
12 var nextSelectionId = 1;
13 var nextTooltipScopeId = 1;
14 var activeLoadRequest = null;
15 var activeLoadRequestId = 0;
16 var activeSaveRequest = null;
17
18 function pad_2( value ) {
19 return ( value < 10 ? '0' : '' ) + value;
20 }
21
22 function trim_text( value ) {
23 return String( value || '' ).replace( /^\s+|\s+$/g, '' );
24 }
25
26 function minutes_to_time( minutes ) {
27 var hours = Math.floor( minutes / 60 );
28 var mins = minutes % 60;
29 return pad_2( hours ) + ':' + pad_2( mins );
30 }
31
32 function clamp( value, min, max ) {
33 return Math.max( min, Math.min( max, value ) );
34 }
35
36 function snap_minute( minute, step ) {
37 return Math.round( minute / step ) * step;
38 }
39
40 function get_grid_config( $grid ) {
41 return {
42 start: parseInt( $grid.attr( 'data-wpbc-ts-start' ), 10 ) || 0,
43 end: parseInt( $grid.attr( 'data-wpbc-ts-end' ), 10 ) || 1440,
44 step: parseInt( $grid.attr( 'data-wpbc-ts-step' ), 10 ) || 15
45 };
46 }
47
48 function get_control( $page, name ) {
49 var $control = $page.find( '[data-wpbc-ts-control="' + name + '"]' ).first();
50
51 if ( ! $control.length ) {
52 $control = $page.find( '#wpbc_ts_' + name ).first();
53 }
54
55 return $control;
56 }
57
58 function percent_for_minute( minute, config ) {
59 return ( ( minute - config.start ) / ( config.end - config.start ) ) * 100;
60 }
61
62 function normalize_selection_range( start, end, config ) {
63 start = clamp( snap_minute( start, config.step ), config.start, config.end );
64 end = clamp( snap_minute( end, config.step ), config.start, config.end );
65
66 if ( start === end ) {
67 end = clamp( start + config.step, config.start, config.end );
68 if ( start === end ) {
69 start = clamp( end - config.step, config.start, config.end );
70 }
71 }
72
73 return {
74 start: Math.min( start, end ),
75 end: Math.max( start, end )
76 };
77 }
78
79 function get_selection_by_id( selectionId ) {
80 var found = null;
81 $.each( selectionRanges, function ( index, item ) {
82 if ( item.id === selectionId ) {
83 found = item;
84 return false;
85 }
86 return true;
87 } );
88 return found;
89 }
90
91 function render_axis( $grid ) {
92 var config = get_grid_config( $grid );
93 var $axis = $grid.find( '.wpbc_ts_time_axis' );
94 var html = '';
95 var minute;
96 var slotCount = Math.max( 1, ( config.end - config.start ) / config.step );
97 var visibleHours = Math.max( 1, ( config.end - config.start ) / 60 );
98 var axisFontSize = clamp( Math.round( 16 - ( visibleHours * 0.25 ) ), 10, 13 );
99 var axisFontWeight = visibleHours <= 10 ? 550 : 400;
100 var firstHour = Math.ceil( config.start / 60 ) * 60;
101
102 $grid.css( '--wpbc-ts-slot-count', String( slotCount ) );
103 $grid.css( '--wpbc-ts-axis-label-size', axisFontSize + 'px' );
104 $grid.css( '--wpbc-ts-axis-label-weight', String( axisFontWeight ) );
105
106 for ( minute = firstHour; minute <= config.end; minute += 60 ) {
107 html += '<span class="wpbc_ts_axis_label" style="left:' + percent_for_minute( minute, config ) + '%;">' + minutes_to_time( minute ) + '</span>';
108 html += '<span class="wpbc_ts_axis_tick" style="left:' + percent_for_minute( minute, config ) + '%;"></span>';
109 if ( minute + 30 < config.end ) {
110 html += '<span class="wpbc_ts_axis_dot" style="left:' + percent_for_minute( minute + 30, config ) + '%;"></span>';
111 }
112 }
113
114 for ( minute = config.start + config.step; minute < config.end; minute += config.step ) {
115 if ( 0 === minute % 60 || 30 === minute % 60 ) {
116 continue;
117 }
118 html += '<span class="wpbc_ts_axis_minor" style="left:' + percent_for_minute( minute, config ) + '%;"></span>';
119 }
120
121 $axis.html( html );
122 refresh_floating_header( $grid.closest( '.wpbc_ts_page' ) );
123 }
124
125 function position_bars( $grid ) {
126 var config = get_grid_config( $grid );
127
128 $grid.find( '.wpbc_ts_bar' ).each( function () {
129 var $bar = $( this );
130 var start = parseInt( $bar.attr( 'data-wpbc-ts-start' ), 10 );
131 var end = parseInt( $bar.attr( 'data-wpbc-ts-end' ), 10 );
132 var visibleStart = clamp( start, config.start, config.end );
133 var visibleEnd = clamp( end, config.start, config.end );
134 var left;
135 var width;
136
137 if ( visibleEnd <= config.start || visibleStart >= config.end || visibleStart >= visibleEnd ) {
138 $bar.hide();
139 return;
140 }
141
142 left = percent_for_minute( visibleStart, config );
143 width = percent_for_minute( visibleEnd, config ) - left;
144 $bar.show().css( { left: left + '%', width: width + '%' } );
145 } );
146 }
147
148 function render_timeline_bars( $page, bars ) {
149 var settings = window.wpbc_availability_timeslots_page || {};
150 var labels = settings.i18n || {};
151
152 $page.find( '.wpbc_ts_bar[data-wpbc-ts-source="server"]' ).remove();
153
154 $.each( bars || {}, function ( date, resources ) {
155 var $row = $page.find( '.wpbc_ts_row[data-wpbc-ts-date="' + date + '"]' );
156
157 if ( ! $row.length || ! resources ) {
158 return;
159 }
160
161 $.each( resources, function ( resourceId, resourceBars ) {
162 $.each( resourceBars || [], function ( index, interval ) {
163 var type = 'blocked' === interval.type ? 'blocked' : ( 'unavailable_day' === interval.type ? 'unavailable_day' : ( 'working_time' === interval.type ? 'working_time' : 'booked' ) );
164 var icon = 'booked' === type ? 'wpbc_icn_lock' : ( 'unavailable_day' === type ? 'wpbc_icn_event_busy' : ( 'working_time' === type ? 'wpbc-bi-clock-history' : 'wpbc_icn_do_not_disturb_on' ) );
165 var tooltip = interval.tooltip || '';
166 var $bar = $( '<div class="wpbc_ts_bar" data-wpbc-ts-source="server"></div>' );
167 var $iconWrap;
168
169 if ( 'booked' === type && interval.booking_url ) {
170 $iconWrap = $( '<a class="wpbc_ts_booking_link tooltip_top" rel="noopener noreferrer"><span></span></a>' );
171 $iconWrap
172 .attr( 'href', interval.booking_url )
173 .attr( 'data-wpbc-ts-booking-id', interval.booking_id || '' )
174 .attr( 'aria-label', ( labels.open_booking || 'Open booking in Booking Listing' ) + ( interval.booking_id ? ': ' + interval.booking_id : '' ) )
175 .attr( 'data-original-title', tooltip || '' );
176 } else if ( ( 'unavailable_day' === type || 'working_time' === type ) && interval.rule_url ) {
177 $iconWrap = $( '<a class="wpbc_ts_rule_link tooltip_top"><span></span></a>' );
178 $iconWrap
179 .attr( 'href', interval.rule_url )
180 .attr( 'data-wpbc-ts-rule-source', interval.rule_source || interval.source_type || '' )
181 .attr( 'aria-label', ( labels.open_availability_rule || 'Open availability settings' ) + ( interval.status_title ? ': ' + interval.status_title : '' ) )
182 .attr( 'data-original-title', tooltip || '' );
183 } else {
184 $iconWrap = $( '<span class="wpbc_ts_bar_icon tooltip_top"><span></span></span>' )
185 .attr( 'data-original-title', tooltip || '' );
186 }
187
188 $bar
189 .addClass( 'wpbc_ts_bar_' + type )
190 .attr( 'data-wpbc-ts-start', parseInt( interval.start_minute, 10 ) )
191 .attr( 'data-wpbc-ts-end', parseInt( interval.end_minute, 10 ) )
192 .attr( 'data-wpbc-ts-resource-id', resourceId )
193 .attr( 'data-wpbc-ts-booking-id', interval.booking_id || '' )
194 .attr( 'data-wpbc-ts-booking-url', interval.booking_url || '' )
195 .attr( 'data-wpbc-ts-rule-url', interval.rule_url || '' )
196 .attr( 'data-wpbc-ts-unavailable-id', interval.unavailable_timeslot_id || '' )
197 .attr( 'data-wpbc-ts-source-type', interval.source_type || '' )
198 .attr( 'data-wpbc-ts-editable', false === interval.editable ? '0' : '1' );
199 if ( tooltip ) {
200 $bar.attr( 'data-original-title', tooltip ).addClass( 'tooltip_top' );
201 }
202 $iconWrap.find( 'span' ).addClass( icon );
203 $bar.append( $iconWrap );
204 $row.find( '.wpbc_ts_lane' ).append( $bar );
205 } );
206 } );
207 } );
208
209 position_bars( $page.find( '.wpbc_ts_grid' ) );
210 refresh_bar_tooltips( $page );
211 }
212
213 function load_blocked_intervals( $page ) {
214 var settings = window.wpbc_availability_timeslots_page || {};
215 var $dateRange = get_control( $page, 'date_range' );
216 var labels = settings.i18n || {};
217 var requestId;
218
219 if ( ! settings.ajax_url ) {
220 return;
221 }
222
223 if ( activeLoadRequest && activeLoadRequest.readyState !== 4 ) {
224 activeLoadRequest.abort();
225 }
226
227 requestId = ++activeLoadRequestId;
228 set_timeline_loading( $page, true, labels.loading || 'Loading' );
229
230 activeLoadRequest = $.post( settings.ajax_url, {
231 action: 'WPBC_AJX_AVAILABILITY_TIMESLOTS_READ',
232 resource_id: get_control( $page, 'resource' ).val(),
233 date_start: $dateRange.attr( 'data-wpbc-ts-start' ),
234 date_end: $dateRange.attr( 'data-wpbc-ts-end' )
235 } ).done( function ( response ) {
236 if ( requestId !== activeLoadRequestId ) {
237 return;
238 }
239 if ( response && response.success && response.data ) {
240 render_timeline_bars( $page, response.data.bars );
241 }
242 } ).fail( function ( xhr, textStatus ) {
243 if ( 'abort' !== textStatus && window.wpbc_admin_show_message ) {
244 wpbc_admin_show_message( labels.save_error || 'Unable to save time-slot availability.', 'error', 5000 );
245 }
246 } ).always( function () {
247 if ( requestId === activeLoadRequestId ) {
248 set_timeline_loading( $page, false );
249 }
250 } );
251 }
252
253 function get_rows_between( $page, startRow, endRow ) {
254 var startIndex = parseInt( startRow, 10 );
255 var endIndex = parseInt( endRow, 10 );
256 var min = Math.min( startIndex, endIndex );
257 var max = Math.max( startIndex, endIndex );
258 var rows = [];
259 var i;
260
261 for ( i = min; i <= max; i++ ) {
262 if ( $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + i + '"]' ).length ) {
263 rows.push( String( i ) );
264 }
265 }
266
267 return rows;
268 }
269
270 function row_label( $page, rowId ) {
271 return trim_text( $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + rowId + '"] .wpbc_ts_row_label_text' ).text() );
272 }
273
274 function escape_html( value ) {
275 return $( '<div />' ).text( value ).html();
276 }
277
278 function position_loading_overlay( $page ) {
279 var $card = $page.find( '.wpbc_ts_timeline_card' ).first();
280 var card = $card.get( 0 );
281 var $overlay = $card.find( '.wpbc_ts_loading_overlay' ).first();
282
283 if ( ! card || ! $overlay.length ) {
284 return;
285 }
286
287 $overlay.css( {
288 width: card.clientWidth + 'px',
289 height: card.clientHeight + 'px',
290 transform: 'translate(' + card.scrollLeft + 'px,' + card.scrollTop + 'px)'
291 } );
292 }
293
294 function set_timeline_loading( $page, isLoading, label ) {
295 var $card = $page.find( '.wpbc_ts_timeline_card' );
296 var $overlay;
297
298 $card
299 .toggleClass( 'is-loading', !! isLoading )
300 .find( '.wpbc_ts_loading_overlay' )
301 .attr( 'aria-hidden', isLoading ? 'false' : 'true' );
302
303 if ( label ) {
304 $overlay = $page.find( '.wpbc_ts_loading_overlay' ).first();
305 $overlay.find( '.wpbc_spins_loading_container > span' ).text( label + '...' );
306 }
307
308 if ( isLoading ) {
309 position_loading_overlay( $page );
310 $card.off( 'scroll.wpbc_ts_loading_overlay' ).on( 'scroll.wpbc_ts_loading_overlay', function () {
311 position_loading_overlay( $page );
312 } );
313 } else {
314 $card.off( 'scroll.wpbc_ts_loading_overlay' );
315 $page.find( '.wpbc_ts_loading_overlay' ).css( {
316 width: '',
317 height: '',
318 transform: ''
319 } );
320 }
321 }
322
323 function set_action_buttons_busy( $page, isBusy ) {
324 var $scope = $page.add( $page.closest( '.modal' ) ).add( $( '.wpbc_ts_rightbar_panels' ) );
325
326 $scope
327 .find( '[data-wpbc-ts-command], .wpbc_ts_clear_selection' )
328 .toggleClass( 'disabled', !! isBusy )
329 .attr( 'aria-disabled', isBusy ? 'true' : 'false' );
330 }
331
332 function is_full_page_component( $page ) {
333 return (
334 ! $page.hasClass( 'wpbc_ts_popup' )
335 && $page.closest( '.wpbc_admin_page__tab__time_slots_availability' ).length > 0
336 );
337 }
338
339 function get_floating_header_namespace( $page ) {
340 return '.wpbc_ts_floating_' + String( $page.attr( 'data-wpbc-ts-id-prefix' ) || 'page' ).replace( /[^\w]/g, '_' );
341 }
342
343 function get_top_nav_bottom() {
344 var $topNav = $( '.wpbc_ui_el__top_nav:visible' ).first();
345 var rect;
346
347 if ( ! $topNav.length ) {
348 return 0;
349 }
350
351 rect = $topNav.get( 0 ).getBoundingClientRect();
352 return Math.max( 0, Math.round( rect.bottom ) );
353 }
354
355 function ensure_floating_header( $page ) {
356 var $floating = $page.children( '.wpbc_ts_floating_header' );
357 var $header;
358
359 if ( $floating.length ) {
360 return $floating;
361 }
362
363 $header = $page.find( '.wpbc_ts_grid > .wpbc_ts_header' ).first();
364 $floating = $( '<div class="wpbc_ts_floating_header" aria-hidden="true"></div>' ).appendTo( $page );
365
366 if ( $header.length ) {
367 $floating.append( $header.clone( false, false ) );
368 }
369
370 return $floating;
371 }
372
373 function sync_floating_header( $page ) {
374 var $floating;
375 var $card;
376 var $grid;
377 var $header;
378 var card;
379 var cardRect;
380 var headerRect;
381 var topOffset;
382 var shouldShow;
383
384 if ( ! is_full_page_component( $page ) ) {
385 return;
386 }
387
388 $floating = ensure_floating_header( $page );
389 $card = $page.find( '.wpbc_ts_timeline_card' ).first();
390 $grid = $page.find( '.wpbc_ts_grid' ).first();
391 $header = $grid.find( '> .wpbc_ts_header' ).first();
392 card = $card.get( 0 );
393
394 if ( ! card || ! $grid.length || ! $header.length ) {
395 $floating.removeClass( 'is-visible' );
396 return;
397 }
398
399 cardRect = card.getBoundingClientRect();
400 headerRect = $header.get( 0 ).getBoundingClientRect();
401 topOffset = get_top_nav_bottom();
402 shouldShow = ( headerRect.top < topOffset ) && ( cardRect.bottom > ( topOffset + headerRect.height ) );
403
404 if ( ! shouldShow ) {
405 $floating.removeClass( 'is-visible' );
406 return;
407 }
408
409 $floating
410 .css( {
411 top: topOffset + 'px',
412 left: Math.round( cardRect.left ) + 'px',
413 width: Math.round( card.clientWidth ) + 'px',
414 '--wpbc-ts-floating-scroll-left': ( -1 * card.scrollLeft ) + 'px',
415 '--wpbc-ts-floating-scroll-left-abs': card.scrollLeft + 'px'
416 } )
417 .addClass( 'is-visible' );
418
419 $floating.children( '.wpbc_ts_header' ).css( 'width', Math.round( $grid.outerWidth() ) + 'px' );
420 }
421
422 function refresh_floating_header( $page ) {
423 var $floating;
424 var $header;
425
426 if ( ! $page || ! $page.length || ! is_full_page_component( $page ) ) {
427 return;
428 }
429
430 $floating = $page.children( '.wpbc_ts_floating_header' );
431
432 if ( ! $floating.length ) {
433 return;
434 }
435
436 $header = $page.find( '.wpbc_ts_grid > .wpbc_ts_header' ).first();
437 $floating.empty();
438
439 if ( $header.length ) {
440 $floating.append( $header.clone( false, false ) );
441 }
442
443 sync_floating_header( $page );
444 }
445
446 function bind_floating_header( $page ) {
447 var namespace;
448
449 if ( ! is_full_page_component( $page ) ) {
450 return;
451 }
452
453 namespace = get_floating_header_namespace( $page );
454 ensure_floating_header( $page );
455
456 $( window )
457 .off( 'scroll' + namespace + ' resize' + namespace )
458 .on( 'scroll' + namespace + ' resize' + namespace, function () {
459 sync_floating_header( $page );
460 } );
461
462 $page.find( '.wpbc_ts_timeline_card' )
463 .off( 'scroll' + namespace )
464 .on( 'scroll' + namespace, function () {
465 sync_floating_header( $page );
466 } );
467
468 $( document )
469 .off( 'click' + namespace )
470 .on( 'click' + namespace, '.wpbc_ui__top_nav__btn_show_left_vertical_nav, .wpbc_ui__top_nav__btn_show_right_vertical_nav, .wpbc_ui__top_nav__btn_full_screen, .wpbc_ui__top_nav__btn_normal_screen', function () {
471 window.setTimeout( function () {
472 sync_floating_header( $page );
473 }, 80 );
474 } );
475
476 sync_floating_header( $page );
477 }
478
479 function refresh_bar_tooltips( $page ) {
480 var $tooltipScope = $page.find( '.wpbc_ts_timeline_card' ).first();
481 var tooltipScopeId;
482 var didInitializeTooltips = false;
483
484 $page.find( '.wpbc_ts_bar.tooltip_top, .wpbc_ts_bar_icon.tooltip_top, .wpbc_ts_booking_link.tooltip_top, .wpbc_ts_rule_link.tooltip_top' ).each( function () {
485 if ( this._tippy ) {
486 this._tippy.destroy();
487 }
488 } );
489
490 if ( 'function' === typeof window.wpbc_define_tippy_tooltips ) {
491 if ( $tooltipScope.length ) {
492 tooltipScopeId = $tooltipScope.attr( 'id' );
493
494 if ( ! tooltipScopeId ) {
495 tooltipScopeId = 'wpbc_ts_timeline_tooltip_scope_' + nextTooltipScopeId;
496 nextTooltipScopeId++;
497 $tooltipScope.attr( 'id', tooltipScopeId );
498 }
499
500 didInitializeTooltips = window.wpbc_define_tippy_tooltips( '#' + tooltipScopeId + ' ' );
501 }
502
503 if ( didInitializeTooltips ) {
504 return;
505 }
506 }
507
508 $page.find( '[data-original-title]' ).each( function () {
509 if ( ! $( this ).attr( 'title' ) ) {
510 $( this ).attr( 'title', $( this ).attr( 'data-original-title' ) );
511 }
512 } );
513 }
514
515 function is_booking_listing_search_available() {
516 return (
517 'function' === typeof window.wpbc_ajx_booking_send_search_request_with_params
518 && 'undefined' !== typeof window.wpbc_ajx_booking_listing
519 && $( '#wpbc_search_field' ).length
520 );
521 }
522
523 function close_time_slots_popup( $page, afterClose ) {
524 var $modal = $page.closest( '.wpbc_modal__availability_timeslots__section, .modal' );
525 var isCallbackDone = false;
526 var run_callback = function () {
527 if ( isCallbackDone ) {
528 return;
529 }
530 isCallbackDone = true;
531
532 if ( 'function' === typeof afterClose ) {
533 afterClose();
534 }
535 };
536
537 if ( ! $modal.length ) {
538 run_callback();
539 return;
540 }
541
542 if ( ! $modal.is( ':visible' ) ) {
543 run_callback();
544 return;
545 }
546
547 if ( 'function' === typeof afterClose ) {
548 $modal.one( 'hidden.wpbc.modal hidden.bs.modal', run_callback );
549 window.setTimeout( run_callback, 500 );
550 }
551
552 if ( 'function' === typeof $modal.wpbc_my_modal ) {
553 $modal.wpbc_my_modal( 'hide' );
554 return;
555 }
556
557 if ( 'function' === typeof $modal.modal ) {
558 $modal.modal( 'hide' );
559 return;
560 }
561
562 $modal.find( '[data-dismiss="modal"]' ).first().trigger( 'click' );
563 }
564
565 function search_booking_in_current_listing( $page, bookingId ) {
566 var keyword = 'id:' + parseInt( bookingId, 10 );
567
568 $( '#wpbc_search_field' ).val( keyword );
569 close_time_slots_popup( $page );
570 window.wpbc_ajx_booking_send_search_request_with_params( {
571 'keyword': keyword,
572 'page_num': 1
573 } );
574 }
575
576 function handle_booked_bar_click( $page, event ) {
577 var $bar = $( event.currentTarget );
578 var $link = $( event.target ).closest( '.wpbc_ts_booking_link' );
579 var bookingId = $bar.attr( 'data-wpbc-ts-booking-id' ) || $link.attr( 'data-wpbc-ts-booking-id' );
580 var bookingUrl = $bar.attr( 'data-wpbc-ts-booking-url' ) || $link.attr( 'href' );
581
582 if ( ! bookingId ) {
583 return;
584 }
585
586 if ( is_booking_listing_search_available() ) {
587 event.preventDefault();
588 event.stopPropagation();
589 search_booking_in_current_listing( $page, bookingId );
590 return;
591 }
592
593 if ( ! $link.length && bookingUrl ) {
594 event.preventDefault();
595 window.location.href = bookingUrl;
596 }
597 }
598
599 function unique_dates_from_selections( $page ) {
600 var dates = {};
601 $.each( selectionRanges, function ( index, item ) {
602 $.each( item.rows, function ( rowIndex, rowId ) {
603 var label = row_label( $page, rowId );
604 if ( label ) {
605 dates[ label ] = true;
606 }
607 } );
608 } );
609 return Object.keys( dates );
610 }
611
612 function get_selection_payload( $page ) {
613 var payload = [];
614
615 $.each( selectionRanges, function ( index, item ) {
616 $.each( item.rows, function ( rowIndex, rowId ) {
617 var $row = $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + rowId + '"]' );
618 var date = $row.attr( 'data-wpbc-ts-date' );
619
620 if ( ! date ) {
621 return;
622 }
623
624 payload.push( {
625 date: date,
626 start_second: item.start * 60,
627 end_second: item.end * 60
628 } );
629 } );
630 } );
631
632 payload.sort( function ( a, b ) {
633 if ( a.date !== b.date ) {
634 return a.date < b.date ? -1 : 1;
635 }
636 if ( a.start_second !== b.start_second ) {
637 return a.start_second - b.start_second;
638 }
639 return a.end_second - b.end_second;
640 } );
641
642 return payload;
643 }
644
645 function get_selection_dates( $page, rows ) {
646 var dates = [];
647 var seen = {};
648
649 $.each( rows || [], function ( rowIndex, rowId ) {
650 var $row = $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + rowId + '"]' );
651 var date = $row.attr( 'data-wpbc-ts-date' );
652
653 if ( date && ! seen[ date ] ) {
654 seen[ date ] = true;
655 dates.push( date );
656 }
657 } );
658
659 dates.sort();
660 return dates;
661 }
662
663 function get_active_booking_selection_context( $page ) {
664 var item = activeSelectionId ? get_selection_by_id( activeSelectionId ) : null;
665 var dates;
666
667 if ( ! item && 1 === selectionRanges.length ) {
668 item = selectionRanges[0];
669 }
670
671 if ( ! item || ! item.rows || ! item.rows.length ) {
672 return null;
673 }
674
675 dates = get_selection_dates( $page, item.rows );
676 if ( ! dates.length ) {
677 return null;
678 }
679
680 return {
681 resource_id: get_control( $page, 'resource' ).val() || '',
682 selected_date: dates[0],
683 selected_dates: dates.join( ',' ),
684 selected_time: minutes_to_time( item.start ) + ' - ' + minutes_to_time( item.end ),
685 start_second: item.start * 60,
686 end_second: item.end * 60
687 };
688 }
689
690 function create_booking_from_active_selection( $page ) {
691 var settings = window.wpbc_availability_timeslots_page || {};
692 var labels = settings.i18n || {};
693 var context = get_active_booking_selection_context( $page );
694 var $addBookingModal = $( '#wpbc_modal__add_booking__section' );
695
696 if ( ! context ) {
697 if ( window.wpbc_admin_show_message ) {
698 wpbc_admin_show_message( labels.select_one_slot_for_booking || 'Select one time range on one date first.', 'warning', 3500 );
699 }
700 return false;
701 }
702
703 if (
704 'function' !== typeof window.wpbc_boo_listing__click__add_booking_modal
705 || ! $addBookingModal.length
706 || 'function' !== typeof $addBookingModal.wpbc_my_modal
707 ) {
708 if ( window.wpbc_admin_show_message ) {
709 wpbc_admin_show_message( labels.add_booking_modal_missing || 'Add Booking popup is not available on this page.', 'warning', 5000 );
710 }
711 return false;
712 }
713
714 close_time_slots_popup( $page, function () {
715 window.wpbc_boo_listing__click__add_booking_modal( {
716 mode: 'add',
717 resource_id: context.resource_id,
718 booking_form: '',
719 selected_dates: context.selected_dates,
720 selected_date: context.selected_date,
721 selected_time: context.selected_time,
722 time_override_enabled: 1,
723 time_override_source: 'times_availability',
724 time_override_start: minutes_to_time( context.start_second / 60 ),
725 time_override_end: minutes_to_time( context.end_second / 60 )
726 } );
727 } );
728
729 return true;
730 }
731
732 function clone_date( date ) {
733 return new Date( date.getFullYear(), date.getMonth(), date.getDate() );
734 }
735
736 function add_days( date, days ) {
737 var next = clone_date( date );
738 next.setDate( next.getDate() + days );
739 return next;
740 }
741
742 function format_iso_date( date ) {
743 return date.getFullYear() + '-' + pad_2( date.getMonth() + 1 ) + '-' + pad_2( date.getDate() );
744 }
745
746 function format_row_date( date ) {
747 var days = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
748 var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
749 return days[ date.getDay() ] + ' ' + months[ date.getMonth() ] + ' ' + date.getDate();
750 }
751
752 function format_date_range_display( startDate, endDate ) {
753 return $.datepick.formatDate( 'M d, yy', startDate ) + ' - ' + $.datepick.formatDate( 'M d, yy', endDate );
754 }
755
756 function cache_row_templates( $page ) {
757 if ( rowTemplates.length ) {
758 return;
759 }
760 rowTemplates = $page.find( '.wpbc_ts_row' ).clone( false ).toArray();
761 $.each( rowTemplates, function ( index, row ) {
762 $( row ).find( '.wpbc_ts_selection:not(.wpbc_ts_selection_template)' ).remove();
763 $( row ).find( '.wpbc_ts_selection_template' ).removeClass( 'is-visible is-active' ).attr( 'hidden', 'hidden' );
764 } );
765 }
766
767 function ensure_row_count( $page, count ) {
768 var $rowsWrap = $page.find( '.wpbc_ts_rows' );
769 var $rows = $rowsWrap.find( '.wpbc_ts_row' );
770 var i;
771 var $clone;
772
773 count = Math.max( 1, count );
774 cache_row_templates( $page );
775
776 if ( $rows.length > count ) {
777 $rows.slice( count ).remove();
778 }
779
780 for ( i = $rowsWrap.find( '.wpbc_ts_row' ).length; i < count; i++ ) {
781 $clone = $( rowTemplates[ i % rowTemplates.length ] ).clone( false );
782 $clone.find( '.wpbc_ts_selection:not(.wpbc_ts_selection_template)' ).remove();
783 $clone.find( '.wpbc_ts_selection_template' ).removeClass( 'is-visible is-active' ).attr( 'hidden', 'hidden' );
784 $rowsWrap.append( $clone );
785 }
786
787 $rowsWrap.find( '.wpbc_ts_row' ).each( function ( index ) {
788 $( this ).attr( 'data-wpbc-ts-row', String( index ) );
789 } );
790 }
791
792 function update_rows_for_date_range( $page, startDate, endDate ) {
793 var dayMs = 24 * 60 * 60 * 1000;
794 var daysCount = Math.round( ( clone_date( endDate ).getTime() - clone_date( startDate ).getTime() ) / dayMs ) + 1;
795
796 daysCount = Math.max( 1, daysCount );
797 ensure_row_count( $page, daysCount );
798
799 $page.find( '.wpbc_ts_row' ).each( function ( index ) {
800 var rowDate = add_days( startDate, index );
801 $( this )
802 .attr( 'data-wpbc-ts-date', format_iso_date( rowDate ) )
803 .find( '.wpbc_ts_row_label_text' ).text( format_row_date( rowDate ) );
804 } );
805
806 selectionRanges = $.grep( selectionRanges, function ( item ) {
807 item.rows = $.grep( item.rows, function ( rowId ) {
808 return $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + rowId + '"]' ).length > 0;
809 } );
810 return item.rows.length > 0;
811 } );
812 canonicalize_selections( activeSelectionId );
813
814 position_bars( $page.find( '.wpbc_ts_grid' ) );
815 render_selections( $page );
816 sync_floating_header( $page );
817 }
818
819 function parse_datepick_selection( stringDates, jsDatesArr ) {
820 var dates = [];
821 var parts;
822 var start;
823 var end;
824
825 if ( jsDatesArr && Array.isArray( jsDatesArr.range ) ) {
826 dates = jsDatesArr.range;
827 } else if ( jsDatesArr && Array.isArray( jsDatesArr.multiple ) ) {
828 dates = jsDatesArr.multiple;
829 } else if ( Array.isArray( jsDatesArr ) ) {
830 dates = jsDatesArr;
831 }
832
833 dates = $.grep( dates, function ( date ) {
834 return date instanceof Date && ! isNaN( date.getTime() );
835 } );
836
837 if ( dates.length ) {
838 dates.sort( function ( a, b ) {
839 return clone_date( a ).getTime() - clone_date( b ).getTime();
840 } );
841
842 return {
843 start: clone_date( dates[0] ),
844 end: clone_date( dates[ dates.length - 1 ] )
845 };
846 }
847
848 parts = String( stringDates || '' ).split( ' - ' );
849 start = parse_date_text( parts[0] );
850 end = parse_date_text( parts[1] || parts[0] );
851
852 if ( start && end ) {
853 return {
854 start: start,
855 end: end
856 };
857 }
858
859 return null;
860 }
861
862 function get_datepick_selected_range( $date ) {
863 var inst;
864
865 if ( ! $.datepick || 'function' !== typeof $.datepick._getInst || ! $date.length ) {
866 return null;
867 }
868
869 inst = $.datepick._getInst( $date[0] );
870 if ( ! inst ) {
871 return null;
872 }
873
874 return parse_datepick_selection( $date.val(), $.datepick._getDate( inst ) );
875 }
876
877 function apply_date_range_selection( $page, range, options ) {
878 var $date = get_control( $page, 'date_range' );
879 var startDate;
880 var endDate;
881 var startIso;
882 var endIso;
883 var currentKey;
884 var nextKey;
885
886 options = options || {};
887
888 if ( ! range || ! range.start || ! range.end ) {
889 return false;
890 }
891
892 startDate = clone_date( range.start );
893 endDate = clone_date( range.end );
894
895 if ( startDate.getTime() > endDate.getTime() ) {
896 range = startDate;
897 startDate = endDate;
898 endDate = range;
899 }
900
901 startIso = format_iso_date( startDate );
902 endIso = format_iso_date( endDate );
903 currentKey = $date.attr( 'data-wpbc-ts-start' ) + ':' + $date.attr( 'data-wpbc-ts-end' );
904 nextKey = startIso + ':' + endIso;
905
906 if ( currentKey === nextKey ) {
907 $date.val( format_date_range_display( startDate, endDate ) );
908 if ( options.force_reload ) {
909 load_blocked_intervals( $page );
910 }
911 return false;
912 }
913
914 $date
915 .attr( 'data-wpbc-ts-start', startIso )
916 .attr( 'data-wpbc-ts-end', endIso )
917 .val( format_date_range_display( startDate, endDate ) );
918
919 update_rows_for_date_range( $page, startDate, endDate );
920 load_blocked_intervals( $page );
921
922 return true;
923 }
924
925 function sync_date_range_from_input( $page ) {
926 var $date = get_control( $page, 'date_range' );
927 var range = get_datepick_selected_range( $date ) || parse_datepick_selection( $date.val(), null );
928
929 apply_date_range_selection( $page, range );
930 }
931
932 function get_date_range_from_data_attrs( $page ) {
933 var $date = get_control( $page, 'date_range' );
934 var startDate = parse_date_text( $date.attr( 'data-wpbc-ts-start' ) );
935 var endDate = parse_date_text( $date.attr( 'data-wpbc-ts-end' ) );
936
937 if ( startDate && endDate ) {
938 return {
939 start: startDate,
940 end: endDate
941 };
942 }
943
944 return null;
945 }
946
947 function get_current_date_range( $page ) {
948 var $date = get_control( $page, 'date_range' );
949 var range = get_date_range_from_data_attrs( $page ) || parse_datepick_selection( $date.val(), null );
950
951 if ( range ) {
952 return range;
953 }
954
955 return {
956 start: parse_date_text( $date.attr( 'data-wpbc-ts-start' ) ),
957 end: parse_date_text( $date.attr( 'data-wpbc-ts-end' ) )
958 };
959 }
960
961 function shift_date_range( $page, direction ) {
962 var range = get_current_date_range( $page );
963 var dayMs = 24 * 60 * 60 * 1000;
964 var daysCount;
965 var shift;
966
967 if ( ! range || ! range.start || ! range.end ) {
968 return false;
969 }
970
971 daysCount = Math.round( ( clone_date( range.end ).getTime() - clone_date( range.start ).getTime() ) / dayMs ) + 1;
972 shift = Math.max( 1, daysCount ) * ( 'prev' === direction ? -1 : 1 );
973
974 clear_selection( $page );
975 return apply_date_range_selection( $page, {
976 start: add_days( range.start, shift ),
977 end: add_days( range.end, shift )
978 }, {
979 force_reload: true
980 } );
981 }
982
983 function parse_date_text( text ) {
984 var parsed;
985 var sqlMatch;
986
987 text = trim_text( text );
988 if ( ! text ) {
989 return null;
990 }
991
992 sqlMatch = text.match( /^(\d{4})-(\d{2})-(\d{2})/ );
993 if ( sqlMatch ) {
994 return new Date( parseInt( sqlMatch[1], 10 ), parseInt( sqlMatch[2], 10 ) - 1, parseInt( sqlMatch[3], 10 ) );
995 }
996
997 if ( $.datepick && 'function' === typeof $.datepick.parseDate ) {
998 try {
999 parsed = $.datepick.parseDate( 'M d, yy', text );
1000 } catch ( error ) {
1001 parsed = null;
1002 }
1003 }
1004
1005 if ( ! parsed ) {
1006 parsed = new Date( text );
1007 }
1008
1009 return parsed instanceof Date && ! isNaN( parsed.getTime() ) ? clone_date( parsed ) : null;
1010 }
1011
1012 function set_time_slots_context( context, options ) {
1013 var $context = context ? $( context ) : $( document );
1014 var $page = $context.is( '.wpbc_ts_page' ) ? $context : $context.find( '.wpbc_ts_page' ).first();
1015 var startDate;
1016 var endDate;
1017 var resourceChanged = false;
1018 var rangeChanged = false;
1019
1020 options = options || {};
1021
1022 if ( ! $page.length ) {
1023 return false;
1024 }
1025
1026 init_time_slots_page( $page, true );
1027
1028 if ( options.resource_id ) {
1029 resourceChanged = String( get_control( $page, 'resource' ).val() ) !== String( options.resource_id );
1030 get_control( $page, 'resource' ).val( String( options.resource_id ) );
1031 }
1032
1033 startDate = parse_date_text( options.date_start );
1034 endDate = parse_date_text( options.date_end ) || startDate;
1035
1036 if ( startDate && endDate ) {
1037 clear_selection( $page );
1038 rangeChanged = apply_date_range_selection( $page, {
1039 start: startDate,
1040 end: endDate
1041 } );
1042 }
1043
1044 if ( resourceChanged && rangeChanged ) {
1045 return true;
1046 }
1047
1048 if ( resourceChanged || ! rangeChanged ) {
1049 load_blocked_intervals( $page );
1050 }
1051
1052 return true;
1053 }
1054
1055 function row_from_pointer( $page, event ) {
1056 var original = event.originalEvent || event;
1057 var point = original.touches && original.touches.length ? original.touches[0] : original;
1058 var el = document.elementFromPoint( point.clientX, point.clientY );
1059 var $row = $( el ).closest( '.wpbc_ts_row' );
1060
1061 if ( ! $row.length || ! $.contains( $page[0], $row[0] ) ) {
1062 return null;
1063 }
1064
1065 return $row.attr( 'data-wpbc-ts-row' );
1066 }
1067
1068 function minute_from_pointer( event, $lane ) {
1069 var original = event.originalEvent || event;
1070 var point = original.touches && original.touches.length ? original.touches[0] : original;
1071 var $grid = $lane.closest( '.wpbc_ts_grid' );
1072 var config = get_grid_config( $grid );
1073 var rect = $lane[0].getBoundingClientRect();
1074 var ratio = ( point.clientX - rect.left ) / rect.width;
1075 var minute = config.start + ratio * ( config.end - config.start );
1076
1077 return clamp( snap_minute( minute, config.step ), config.start, config.end );
1078 }
1079
1080 function create_selection( $page, rows, start, end ) {
1081 var $grid = $page.find( '.wpbc_ts_grid' );
1082 var config = get_grid_config( $grid );
1083 var range = normalize_selection_range( start, end, config );
1084 var item = {
1085 id: 'selection_' + nextSelectionId++,
1086 start: range.start,
1087 end: range.end,
1088 rows: rows.slice( 0 )
1089 };
1090
1091 selectionRanges.push( item );
1092 activeSelectionId = item.id;
1093 canonicalize_selections( item.id );
1094 render_selections( $page );
1095 return get_selection_by_id( activeSelectionId );
1096 }
1097
1098 function update_selection( $page, selectionId, rows, start, end ) {
1099 var $grid = $page.find( '.wpbc_ts_grid' );
1100 var config = get_grid_config( $grid );
1101 var range = normalize_selection_range( start, end, config );
1102 var item = get_selection_by_id( selectionId );
1103
1104 if ( ! item ) {
1105 return;
1106 }
1107
1108 item.start = range.start;
1109 item.end = range.end;
1110 item.rows = rows.slice( 0 );
1111 canonicalize_selections( selectionId );
1112 render_selections( $page );
1113 }
1114
1115 function canonicalize_selections( preferredActiveId ) {
1116 var intervals = [];
1117 var merged = [];
1118 var groups = [];
1119 var newActiveId = '';
1120
1121 $.each( selectionRanges, function ( index, item ) {
1122 $.each( item.rows, function ( rowIndex, rowId ) {
1123 intervals.push( {
1124 row: parseInt( rowId, 10 ),
1125 start: item.start,
1126 end: item.end,
1127 active: item.id === preferredActiveId || item.id === activeSelectionId
1128 } );
1129 } );
1130 } );
1131
1132 intervals.sort( function ( a, b ) {
1133 if ( a.row !== b.row ) {
1134 return a.row - b.row;
1135 }
1136 if ( a.start !== b.start ) {
1137 return a.start - b.start;
1138 }
1139 return a.end - b.end;
1140 } );
1141
1142 $.each( intervals, function ( index, item ) {
1143 var last = merged[ merged.length - 1 ];
1144
1145 if ( last && last.row === item.row && item.start <= last.end ) {
1146 last.end = Math.max( last.end, item.end );
1147 last.active = last.active || item.active;
1148 return;
1149 }
1150
1151 merged.push( {
1152 row: item.row,
1153 start: item.start,
1154 end: item.end,
1155 active: item.active
1156 } );
1157 } );
1158
1159 merged.sort( function ( a, b ) {
1160 if ( a.start !== b.start ) {
1161 return a.start - b.start;
1162 }
1163 if ( a.end !== b.end ) {
1164 return a.end - b.end;
1165 }
1166 return a.row - b.row;
1167 } );
1168
1169 $.each( merged, function ( index, item ) {
1170 var last = groups[ groups.length - 1 ];
1171
1172 if ( last && last.start === item.start && last.end === item.end && last.lastRow + 1 === item.row ) {
1173 last.rows.push( String( item.row ) );
1174 last.lastRow = item.row;
1175 last.active = last.active || item.active;
1176 return;
1177 }
1178
1179 groups.push( {
1180 start: item.start,
1181 end: item.end,
1182 rows: [ String( item.row ) ],
1183 lastRow: item.row,
1184 active: item.active
1185 } );
1186 } );
1187
1188 selectionRanges = $.map( groups, function ( item ) {
1189 var id = 'selection_' + nextSelectionId++;
1190
1191 if ( item.active && ! newActiveId ) {
1192 newActiveId = id;
1193 }
1194
1195 return {
1196 id: id,
1197 start: item.start,
1198 end: item.end,
1199 rows: item.rows
1200 };
1201 } );
1202
1203 activeSelectionId = newActiveId || ( selectionRanges[0] ? selectionRanges[0].id : '' );
1204 }
1205
1206 function render_selections( $page ) {
1207 var $grid = $page.find( '.wpbc_ts_grid' );
1208 var config = get_grid_config( $grid );
1209
1210 $page.find( '.wpbc_ts_selection:not(.wpbc_ts_selection_template)' ).remove();
1211
1212 $.each( selectionRanges, function ( index, item ) {
1213 var range = normalize_selection_range( item.start, item.end, config );
1214 var left = percent_for_minute( range.start, config );
1215 var width = percent_for_minute( range.end, config ) - left;
1216
1217 item.start = range.start;
1218 item.end = range.end;
1219
1220 $.each( item.rows, function ( rowIndex, rowId ) {
1221 var $row = $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + rowId + '"]' );
1222 var $lane = $row.find( '.wpbc_ts_lane' );
1223 var $template = $lane.find( '.wpbc_ts_selection_template' ).first();
1224 var $selection;
1225
1226 if ( ! $lane.length || ! $template.length ) {
1227 return;
1228 }
1229
1230 $selection = $template.clone( false );
1231 if ( $selection[0] ) {
1232 $selection[0].hidden = false;
1233 $selection[0].removeAttribute( 'hidden' );
1234 }
1235
1236 $selection
1237 .removeClass( 'wpbc_ts_selection_template' )
1238 .addClass( 'is-visible' )
1239 .toggleClass( 'is-active', item.id === activeSelectionId )
1240 .attr( 'data-wpbc-ts-selection-id', item.id )
1241 .css( { left: left + '%', width: width + '%' } );
1242
1243 $selection.find( '.wpbc_ts_time_chip_start' ).text( minutes_to_time( range.start ) );
1244 $selection.find( '.wpbc_ts_time_chip_end' ).text( minutes_to_time( range.end ) );
1245 $lane.append( $selection );
1246 } );
1247 } );
1248
1249 update_summary( $page );
1250 }
1251
1252 function clear_selection( $page ) {
1253 selectionRanges = [];
1254 activeSelectionId = '';
1255 render_selections( $page );
1256 }
1257
1258 function update_summary( $page ) {
1259 var config = get_grid_config( $page.find( '.wpbc_ts_grid' ) );
1260 var slotCount = 0;
1261 var dates = {};
1262 var datesCount = 0;
1263 var dateText = '-';
1264 var timeText = '-';
1265 var details = [];
1266 var detailsHtml = '';
1267
1268 $.each( selectionRanges, function ( index, item ) {
1269 slotCount += Math.max( 0, Math.round( ( item.end - item.start ) / config.step ) * item.rows.length );
1270
1271 $.each( item.rows, function ( rowIndex, rowId ) {
1272 var label = row_label( $page, rowId );
1273
1274 if ( ! label ) {
1275 return;
1276 }
1277
1278 dates[ label ] = true;
1279 details.push( {
1280 row: parseInt( rowId, 10 ),
1281 start: item.start,
1282 end: item.end,
1283 label: label
1284 } );
1285 } );
1286 } );
1287
1288 details.sort( function ( a, b ) {
1289 if ( a.row !== b.row ) {
1290 return a.row - b.row;
1291 }
1292 if ( a.start !== b.start ) {
1293 return a.start - b.start;
1294 }
1295 return a.end - b.end;
1296 } );
1297
1298 $.each( details, function ( index, item ) {
1299 var timeLabel = minutes_to_time( item.start ) + ' - ' + minutes_to_time( item.end );
1300
1301 detailsHtml += '<div class="wpbc_ts_selection_detail_item">'
1302 + '<span class="wpbc_ts_selection_detail_date">' + escape_html( item.label ) + '</span>'
1303 + '<span class="wpbc_ts_selection_detail_time">' + escape_html( timeLabel ) + '</span>'
1304 + '</div>';
1305 } );
1306
1307 datesCount = Object.keys( dates ).length;
1308
1309 if ( selectionRanges.length ) {
1310 dateText = datesCount + ( 1 === datesCount ? ' date' : ' dates' );
1311 timeText = selectionRanges.length + ( 1 === selectionRanges.length ? ' interval' : ' intervals' );
1312 }
1313
1314 if ( ! detailsHtml ) {
1315 detailsHtml = '<div class="wpbc_ts_selection_details_empty">No time slots selected.</div>';
1316 }
1317
1318 $( document ).find( '[data-wpbc-ts-detail="slots"]' ).text( slotCount );
1319 $( document ).find( '[data-wpbc-ts-detail="dates"]' ).text( dateText );
1320 $( document ).find( '[data-wpbc-ts-detail="time"]' ).text( timeText );
1321 $( document ).find( '[data-wpbc-ts-detail="selection_list"]' ).html( detailsHtml );
1322 }
1323
1324 function show_live_tip( $page, event ) {
1325 var original = event.originalEvent || event;
1326 var point = original.touches && original.touches.length ? original.touches[0] : original;
1327 var $tip = $page.find( '.wpbc_ts_live_tip' );
1328 var active = get_selection_by_id( activeSelectionId );
1329
1330 if ( ! active ) {
1331 return;
1332 }
1333
1334 if ( ! $tip.length ) {
1335 $tip = $( '<div class="wpbc_ts_live_tip"></div>' ).appendTo( $page );
1336 }
1337
1338 $tip
1339 .text( minutes_to_time( active.start ) + ' - ' + minutes_to_time( active.end ) )
1340 .css( {
1341 left: point.pageX + 12 + 'px',
1342 top: point.pageY - 38 + 'px'
1343 } )
1344 .addClass( 'is-visible' );
1345 }
1346
1347 function hide_live_tip( $page ) {
1348 $page.find( '.wpbc_ts_live_tip' ).removeClass( 'is-visible' );
1349 }
1350
1351 function sync_slot_step_controls( $page, step ) {
1352 get_control( $page, 'slot_step' ).val( String( step ) );
1353 $( '#wpbc_ts_side_slot_step' ).val( String( step ) );
1354 }
1355
1356 function sync_zoom_controls( $page, step ) {
1357 var index = zoomSteps.indexOf( step );
1358 if ( -1 === index ) {
1359 index = 2;
1360 }
1361 get_control( $page, 'zoom' ).val( String( index ) );
1362 $( '#wpbc_ts_side_zoom' ).val( String( index ) );
1363 }
1364
1365 function set_step( $page, step ) {
1366 var $grid = $page.find( '.wpbc_ts_grid' );
1367 $grid.attr( 'data-wpbc-ts-step', step );
1368 sync_slot_step_controls( $page, step );
1369 sync_zoom_controls( $page, step );
1370 render_axis( $grid );
1371 position_bars( $grid );
1372 render_selections( $page );
1373 }
1374
1375 function sync_visible_time_controls( $page, start, end ) {
1376 get_control( $page, 'day_start' ).val( String( start ) );
1377 get_control( $page, 'day_end' ).val( String( end ) );
1378 get_control( $page, 'day_start_slider' ).val( String( start ) );
1379 get_control( $page, 'day_end_slider' ).val( String( end ) );
1380 $( '#wpbc_ts_side_start' ).val( String( start ) );
1381 $( '#wpbc_ts_side_end' ).val( String( end ) );
1382 $( '#wpbc_ts_side_start_slider' ).val( String( start ) );
1383 $( '#wpbc_ts_side_end_slider' ).val( String( end ) );
1384 }
1385
1386 function set_visible_time_range( $page, start, end ) {
1387 var $grid = $page.find( '.wpbc_ts_grid' );
1388
1389 start = parseInt( start, 10 );
1390 end = parseInt( end, 10 );
1391
1392 if ( end <= start ) {
1393 if ( $( document.activeElement ).is( '[data-wpbc-ts-control="day_start"], [data-wpbc-ts-control="day_start_slider"], #wpbc_ts_day_start, #wpbc_ts_side_start, #wpbc_ts_day_start_slider, #wpbc_ts_side_start_slider' ) ) {
1394 start = Math.max( 0, end - 60 );
1395 } else {
1396 end = Math.min( 1440, start + 60 );
1397 }
1398 }
1399
1400 $grid.attr( 'data-wpbc-ts-start', start );
1401 $grid.attr( 'data-wpbc-ts-end', end );
1402 sync_visible_time_controls( $page, start, end );
1403
1404 render_axis( $grid );
1405 position_bars( $grid );
1406 render_selections( $page );
1407 }
1408
1409 function set_mode( mode ) {
1410 currentMode = mode;
1411 }
1412
1413 function is_bar_selectable_for_time_action( $bar ) {
1414 if ( ! $bar.length ) {
1415 return true;
1416 }
1417
1418 return (
1419 $bar.hasClass( 'wpbc_ts_bar_blocked' )
1420 && '0' !== $bar.attr( 'data-wpbc-ts-editable' )
1421 );
1422 }
1423
1424 function run_command( $page, mode ) {
1425 var settings = window.wpbc_availability_timeslots_page || {};
1426 var labels = settings.i18n || {};
1427 var payload;
1428
1429 if ( activeSaveRequest && activeSaveRequest.readyState !== 4 ) {
1430 return;
1431 }
1432
1433 set_mode( mode );
1434 if ( ! selectionRanges.length ) {
1435 if ( window.wpbc_admin_show_message ) {
1436 wpbc_admin_show_message( labels.select_slots_first || 'Select one or more time ranges first.', 'warning', 3500 );
1437 }
1438 return;
1439 }
1440
1441 payload = get_selection_payload( $page );
1442 if ( ! settings.ajax_url || ! settings.nonce || ! payload.length ) {
1443 return;
1444 }
1445
1446 set_timeline_loading( $page, true, labels.saving || 'Saving' );
1447 set_action_buttons_busy( $page, true );
1448
1449 activeSaveRequest = $.post( settings.ajax_url, {
1450 action: 'WPBC_AJX_AVAILABILITY_TIMESLOTS_SAVE',
1451 nonce: settings.nonce,
1452 resource_id: get_control( $page, 'resource' ).val(),
1453 mode: mode,
1454 intervals: JSON.stringify( payload )
1455 } ).done( function ( response ) {
1456 if ( response && response.success ) {
1457 if ( window.wpbc_admin_show_message ) {
1458 wpbc_admin_show_message( 'block' === mode ? ( labels.block_success || 'Selected time ranges have been blocked.' ) : ( labels.unblock_success || 'Selected time ranges have been unblocked.' ), 'success', 5000 );
1459 }
1460 clear_selection( $page );
1461 load_blocked_intervals( $page );
1462 } else if ( window.wpbc_admin_show_message ) {
1463 wpbc_admin_show_message( labels.save_error || 'Unable to save time-slot availability.', 'error', 5000 );
1464 }
1465 } ).fail( function () {
1466 if ( window.wpbc_admin_show_message ) {
1467 wpbc_admin_show_message( labels.save_error || 'Unable to save time-slot availability.', 'error', 5000 );
1468 }
1469 } ).always( function () {
1470 activeSaveRequest = null;
1471 set_action_buttons_busy( $page, false );
1472
1473 if ( ! activeLoadRequest || activeLoadRequest.readyState === 4 ) {
1474 set_timeline_loading( $page, false, labels.loading || 'Loading' );
1475 }
1476 } );
1477 }
1478
1479 function init_date_range_picker( $page ) {
1480 var $date = get_control( $page, 'date_range' );
1481 var $dateWrap = $date.closest( '.wpbc_ts_input_icon' );
1482 var firstDay = 0;
1483
1484 if ( ! $.fn.datepick ) {
1485 if ( window.console ) {
1486 console.log( 'WPBC Error. JavaScript library "datepick" was not defined.' );
1487 }
1488 return;
1489 }
1490
1491 if ( window._wpbc && 'function' === typeof window._wpbc.get_other_param ) {
1492 firstDay = parseInt( window._wpbc.get_other_param( 'calendars__first_day' ), 10 ) || 0;
1493 }
1494
1495 if ( $date.hasClass( $.datepick.markerClassName ) ) {
1496 try {
1497 $date.datepick( 'destroy' );
1498 } catch ( error ) {}
1499 }
1500
1501 $date.datepick( {
1502 beforeShowDay: function () {
1503 return [ true, 'date_available' ];
1504 },
1505 onSelect: function ( stringDates, jsDatesArr ) {
1506 apply_date_range_selection( $page, parse_datepick_selection( stringDates, jsDatesArr ) );
1507 },
1508 onClose: function () {
1509 window.setTimeout( function () {
1510 sync_date_range_from_input( $page );
1511 }, 0 );
1512 },
1513 showOn: 'none',
1514 showAnim: 'show',
1515 duration: '',
1516 rangeSelect: true,
1517 multiSelect: 0,
1518 numberOfMonths: 1,
1519 stepMonths: 1,
1520 prevText: '&lsaquo;',
1521 nextText: '&rsaquo;',
1522 dateFormat: 'M d, yy',
1523 changeMonth: false,
1524 changeYear: false,
1525 minDate: null,
1526 maxDate: null,
1527 showStatus: false,
1528 multiSeparator: ', ',
1529 closeAtTop: null,
1530 firstDay: firstDay,
1531 gotoCurrent: false,
1532 hideIfNoPrevNext: true,
1533 useThemeRoller: false,
1534 mandatory: true
1535 } );
1536
1537 function show_date_range_picker() {
1538 var input = $date.get( 0 );
1539
1540 if ( ! input || ! $.datepick || ! $.datepick._showDatepick || ! $date.hasClass( $.datepick.markerClassName ) ) {
1541 return;
1542 }
1543
1544 if ( ( $.datepick._lastInput === input ) && ! $.datepick._datepickerShowing ) {
1545 $.datepick._lastInput = null;
1546 }
1547
1548 if ( ( $.datepick._lastInput === input ) && $.datepick._datepickerShowing ) {
1549 return;
1550 }
1551
1552 $.datepick._showDatepick( input );
1553 $( '#datepick-div, .datepick-popup' ).css( 'z-index', 1000010 );
1554 }
1555
1556 $date.off( 'click.wpbc_ts_date_range_open focus.wpbc_ts_date_range_open keydown.wpbc_ts_date_range_open' ).on( 'click.wpbc_ts_date_range_open focus.wpbc_ts_date_range_open keydown.wpbc_ts_date_range_open', function ( event ) {
1557 if ( 'keydown' === event.type && ( 13 !== event.which ) && ( 32 !== event.which ) ) {
1558 return;
1559 }
1560
1561 if ( 'keydown' === event.type ) {
1562 event.preventDefault();
1563 }
1564
1565 show_date_range_picker();
1566 } );
1567
1568 $dateWrap.off( 'mousedown.wpbc_ts_date_range_open click.wpbc_ts_date_range_open' )
1569 .on( 'mousedown.wpbc_ts_date_range_open', function ( event ) {
1570 event.stopPropagation();
1571 } )
1572 .on( 'click.wpbc_ts_date_range_open', function ( event ) {
1573 if ( event.target !== $date.get( 0 ) ) {
1574 event.preventDefault();
1575 }
1576
1577 $date.trigger( 'focus' );
1578 show_date_range_picker();
1579 } );
1580
1581 $date.on( 'change input', function () {
1582 sync_date_range_from_input( $page );
1583 } );
1584 }
1585
1586 function init_rightbar_tabs() {
1587 $( document ).on( 'click', '.wpbc_ts_rightbar_tabs [role="tab"]', function ( event ) {
1588 var $tab = $( this );
1589 var panelId = $tab.attr( 'aria-controls' );
1590 var $panel = $( '#' + panelId );
1591 var $tablist = $tab.closest( '[role="tablist"]' );
1592
1593 if ( ! $panel.length ) {
1594 return;
1595 }
1596
1597 event.preventDefault();
1598 $tablist.find( '[role="tab"]' ).attr( 'aria-selected', 'false' );
1599 $tab.attr( 'aria-selected', 'true' );
1600 $( '.wpbc_ts_rightbar_panels .wpbc_bfb__palette_panel' ).attr( 'hidden', 'hidden' ).attr( 'aria-hidden', 'true' );
1601 $panel.each( function () {
1602 this.hidden = false;
1603 this.removeAttribute( 'hidden' );
1604 } ).attr( 'aria-hidden', 'false' );
1605 } );
1606 }
1607
1608 function init_time_slots_page( context, force ) {
1609 var $context = context ? $( context ) : $( document );
1610 var $pages = $context.is( '.wpbc_ts_page' ) ? $context : $context.find( '.wpbc_ts_page' );
1611
1612 if ( ! $pages.length ) {
1613 return;
1614 }
1615
1616 if ( ! force ) {
1617 $pages = $pages.filter( function () {
1618 return '0' !== $( this ).attr( 'data-wpbc-ts-auto-init' );
1619 } );
1620 }
1621
1622 $pages.each( function () {
1623 init_time_slots_component( $( this ) );
1624 } );
1625 }
1626
1627 function init_time_slots_component( $page ) {
1628 var $grid;
1629 var isDragging = false;
1630 var dragStartMinute = 0;
1631 var dragStartRow = null;
1632 var dragSelectionId = '';
1633 var resizeMode = '';
1634
1635 if ( $page.attr( 'data-wpbc-ts-initialized' ) ) {
1636 return;
1637 }
1638 $page.attr( 'data-wpbc-ts-initialized', '1' );
1639
1640 cache_row_templates( $page );
1641 $grid = $page.find( '.wpbc_ts_grid' );
1642 render_axis( $grid );
1643 position_bars( $grid );
1644 init_date_range_picker( $page );
1645 load_blocked_intervals( $page );
1646 set_mode( currentMode );
1647 sync_slot_step_controls( $page, get_grid_config( $grid ).step );
1648 sync_zoom_controls( $page, get_grid_config( $grid ).step );
1649 sync_visible_time_controls( $page, get_grid_config( $grid ).start, get_grid_config( $grid ).end );
1650 bind_floating_header( $page );
1651
1652 $page.on( 'change', '[data-wpbc-ts-control="slot_step"], #wpbc_ts_slot_step', function () {
1653 set_step( $page, parseInt( $( this ).val(), 10 ) );
1654 } );
1655
1656 $( document ).on( 'change', '#wpbc_ts_side_slot_step', function () {
1657 set_step( $page, parseInt( $( this ).val(), 10 ) );
1658 } );
1659
1660 $page.on( 'change', '[data-wpbc-ts-control="resource"], #wpbc_ts_resource', function () {
1661 load_blocked_intervals( $page );
1662 } );
1663
1664 $page.on( 'input change', '[data-wpbc-ts-control="zoom"], #wpbc_ts_zoom', function () {
1665 var index = parseInt( $( this ).val(), 10 );
1666 set_step( $page, zoomSteps[ index ] || 15 );
1667 } );
1668
1669 $( document ).on( 'input change', '#wpbc_ts_side_zoom', function () {
1670 var index = parseInt( $( this ).val(), 10 );
1671 set_step( $page, zoomSteps[ index ] || 15 );
1672 } );
1673
1674 $page.on( 'click', '[data-wpbc-ts-zoom]', function () {
1675 var $zoom = get_control( $page, 'zoom' );
1676 var value = parseInt( $zoom.val(), 10 );
1677 value += 'in' === $( this ).attr( 'data-wpbc-ts-zoom' ) ? 1 : -1;
1678 value = clamp( value, 0, zoomSteps.length - 1 );
1679 $zoom.val( String( value ) ).trigger( 'change' );
1680 } );
1681
1682 $( document ).on( 'click', '.wpbc_ts_rightbar_panels [data-wpbc-ts-zoom]', function () {
1683 var $zoom = $( '#wpbc_ts_side_zoom' );
1684 var value = parseInt( $zoom.val(), 10 );
1685 value += 'in' === $( this ).attr( 'data-wpbc-ts-zoom' ) ? 1 : -1;
1686 value = clamp( value, 0, zoomSteps.length - 1 );
1687 $zoom.val( String( value ) ).trigger( 'change' );
1688 } );
1689
1690 $page.on( 'change', '[data-wpbc-ts-control="day_start"], [data-wpbc-ts-control="day_end"], #wpbc_ts_day_start, #wpbc_ts_day_end', function () {
1691 set_visible_time_range( $page, get_control( $page, 'day_start' ).val(), get_control( $page, 'day_end' ).val() );
1692 } );
1693
1694 $( document ).on( 'change', '#wpbc_ts_side_start, #wpbc_ts_side_end', function () {
1695 set_visible_time_range( $page, $( '#wpbc_ts_side_start' ).val(), $( '#wpbc_ts_side_end' ).val() );
1696 } );
1697
1698 $page.on( 'input change', '[data-wpbc-ts-control="day_start_slider"], [data-wpbc-ts-control="day_end_slider"], #wpbc_ts_day_start_slider, #wpbc_ts_day_end_slider', function () {
1699 set_visible_time_range( $page, get_control( $page, 'day_start_slider' ).val(), get_control( $page, 'day_end_slider' ).val() );
1700 } );
1701
1702 $( document ).on( 'input change', '#wpbc_ts_side_start_slider, #wpbc_ts_side_end_slider', function () {
1703 set_visible_time_range( $page, $( '#wpbc_ts_side_start_slider' ).val(), $( '#wpbc_ts_side_end_slider' ).val() );
1704 } );
1705
1706 $page.on( 'click', '[data-wpbc-ts-command]', function ( event ) {
1707 event.preventDefault();
1708 run_command( $page, $( this ).attr( 'data-wpbc-ts-command' ) );
1709 } );
1710
1711 $page.on( 'click', '[data-wpbc-ts-range-shift]', function ( event ) {
1712 event.preventDefault();
1713 shift_date_range( $page, $( this ).attr( 'data-wpbc-ts-range-shift' ) );
1714 } );
1715
1716 $page.on( 'click', '[data-wpbc-ts-create-booking]', function ( event ) {
1717 event.preventDefault();
1718 create_booking_from_active_selection( $page );
1719 } );
1720
1721 $page.closest( '.modal' ).off( 'click.wpbc_ts_create_booking' ).on( 'click.wpbc_ts_create_booking', '[data-wpbc-ts-create-booking]', function ( event ) {
1722 event.preventDefault();
1723 create_booking_from_active_selection( $page );
1724 } );
1725
1726 $page.closest( '.modal' ).off( 'click.wpbc_ts_footer_command' ).on( 'click.wpbc_ts_footer_command', '[data-wpbc-ts-command]', function ( event ) {
1727 event.preventDefault();
1728 run_command( $page, $( this ).attr( 'data-wpbc-ts-command' ) );
1729 } );
1730
1731 $page.closest( '.modal' ).off( 'click.wpbc_ts_footer_clear' ).on( 'click.wpbc_ts_footer_clear', '.wpbc_ts_clear_selection', function ( event ) {
1732 event.preventDefault();
1733 clear_selection( $page );
1734 } );
1735
1736 $page.on( 'click', '.wpbc_ts_bar_booked', function ( event ) {
1737 handle_booked_bar_click( $page, event );
1738 } );
1739
1740 $( document ).off( 'click.wpbc_ts_rightbar_command' ).on( 'click.wpbc_ts_rightbar_command', '.wpbc_ts_rightbar_panels [data-wpbc-ts-command]', function ( event ) {
1741 event.preventDefault();
1742 run_command( $page, $( this ).attr( 'data-wpbc-ts-command' ) );
1743 } );
1744
1745 $( document ).off( 'click.wpbc_ts_rightbar_range_shift' ).on( 'click.wpbc_ts_rightbar_range_shift', '.wpbc_ts_rightbar_panels [data-wpbc-ts-range-shift]', function ( event ) {
1746 event.preventDefault();
1747 shift_date_range( $page, $( this ).attr( 'data-wpbc-ts-range-shift' ) );
1748 } );
1749
1750 $page.on( 'mousedown touchstart', '.wpbc_ts_handle', function ( event ) {
1751 var $selection = $( this ).closest( '.wpbc_ts_selection' );
1752 var $lane = $selection.closest( '.wpbc_ts_lane' );
1753 isDragging = true;
1754 resizeMode = $( this ).hasClass( 'wpbc_ts_handle_start' ) ? 'start' : 'end';
1755 activeSelectionId = $selection.attr( 'data-wpbc-ts-selection-id' );
1756 dragSelectionId = activeSelectionId;
1757 dragStartRow = $lane.closest( '.wpbc_ts_row' ).attr( 'data-wpbc-ts-row' );
1758 dragStartMinute = minute_from_pointer( event, $lane );
1759 render_selections( $page );
1760 event.preventDefault();
1761 event.stopPropagation();
1762 } );
1763
1764 $page.on( 'mousedown touchstart', '.wpbc_ts_selection:not(.wpbc_ts_selection_template)', function ( event ) {
1765 if ( $( event.target ).closest( '.wpbc_ts_handle' ).length ) {
1766 return;
1767 }
1768 activeSelectionId = $( this ).attr( 'data-wpbc-ts-selection-id' );
1769 render_selections( $page );
1770 event.stopPropagation();
1771 } );
1772
1773 $page.on( 'mousedown touchstart', '.wpbc_ts_lane', function ( event ) {
1774 var $lane = $( this );
1775 var $barTarget = $( event.target ).closest( '.wpbc_ts_bar' );
1776 var step;
1777
1778 if ( $( event.target ).closest( '.wpbc_ts_selection:not(.wpbc_ts_selection_template)' ).length ) {
1779 return;
1780 }
1781 if ( $barTarget.length && ! is_bar_selectable_for_time_action( $barTarget ) ) {
1782 return;
1783 }
1784
1785 isDragging = true;
1786 resizeMode = '';
1787 dragStartRow = $lane.closest( '.wpbc_ts_row' ).attr( 'data-wpbc-ts-row' );
1788 dragStartMinute = minute_from_pointer( event, $lane );
1789 step = get_grid_config( $grid ).step;
1790 dragSelectionId = create_selection( $page, [ dragStartRow ], dragStartMinute, dragStartMinute + step ).id;
1791 show_live_tip( $page, event );
1792 event.preventDefault();
1793 } );
1794
1795 $( document ).on( 'mousemove.wpbc_ts touchmove.wpbc_ts', function ( event ) {
1796 var currentRow;
1797 var rows;
1798 var $lane;
1799 var minute;
1800 var item;
1801
1802 if ( ! isDragging ) {
1803 return;
1804 }
1805
1806 item = get_selection_by_id( dragSelectionId );
1807 if ( ! item ) {
1808 return;
1809 }
1810
1811 currentRow = row_from_pointer( $page, event ) || dragStartRow;
1812 rows = get_rows_between( $page, dragStartRow, currentRow );
1813 $lane = $page.find( '.wpbc_ts_row[data-wpbc-ts-row="' + dragStartRow + '"] .wpbc_ts_lane' );
1814 minute = minute_from_pointer( event, $lane );
1815
1816 if ( 'start' === resizeMode ) {
1817 update_selection( $page, item.id, item.rows, minute, item.end );
1818 } else if ( 'end' === resizeMode ) {
1819 update_selection( $page, item.id, item.rows, item.start, minute );
1820 } else {
1821 update_selection( $page, item.id, rows, dragStartMinute, minute );
1822 }
1823 dragSelectionId = activeSelectionId;
1824
1825 show_live_tip( $page, event );
1826 event.preventDefault();
1827 } );
1828
1829 $( document ).on( 'mouseup.wpbc_ts touchend.wpbc_ts', function () {
1830 isDragging = false;
1831 resizeMode = '';
1832 dragSelectionId = '';
1833 hide_live_tip( $page );
1834 } );
1835
1836 $page.on( 'click', '.wpbc_ts_clear_selection', function ( event ) {
1837 event.preventDefault();
1838 clear_selection( $page );
1839 } );
1840
1841 $( document ).off( 'click.wpbc_ts_rightbar_clear' ).on( 'click.wpbc_ts_rightbar_clear', '.wpbc_ts_rightbar_panels .wpbc_ts_clear_selection', function ( event ) {
1842 event.preventDefault();
1843 clear_selection( $page );
1844 } );
1845 }
1846
1847 window.wpbc_availability_timeslots_init = function ( context ) {
1848 init_time_slots_page( context || document, true );
1849 };
1850 window.wpbc_availability_timeslots_set_context = set_time_slots_context;
1851
1852 $( function () {
1853 init_rightbar_tabs();
1854 init_time_slots_page( document, false );
1855
1856 $( document ).on( 'wpbc_availability_timeslots_init', function ( event, context ) {
1857 init_time_slots_page( context || document, true );
1858 } );
1859 } );
1860 }( jQuery ) );
1861