PluginProbe
Booking Calendar / 11.1
Booking Calendar v11.1
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.1, at includes/page-availability-timeslots/_src/availability_timeslots_page.js

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