PageSourceSearch

https://rhcl.org/js/am-reserve-room.v3.js?v=1480

js rhcl.org collected 2026-09-26 03:50:52 UTC 75,473 bytes, 1,476 lines download raw bytes

1;
2(function ($, window, document, undefined) {
3    "use strict";
4    var pluginName = "amReserveRoom",
5        defaults = {
6            apiServer: false,
7            client: false,
8            segmentWidth: 30,
9            segmentSize: 15,
10            blockSize: 1,
11            multiLocation: false,
12            room: {},
13            loggedIn: false,
14            mode: 0,
15            units: 'ft',
16            useMetric: 0,
17            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
18        };
19    // The actual plugin constructor
20    function Plugin(element, options) {
21        this.locations = [];
22        this.element = element;
23        this.$element = $(element);
24        this.settings = $.extend(true, {}, defaults, options);
25        this.clientSettings = this.settings.clientSettings;
26        this._defaults = defaults;
27        this._name = pluginName;
28        this.selected = { start: null, end: null }
29        this.selectedLayout = false;
30        this.barsAdded = false;
31        this.bookedAssets = [];
32        this.storage = window.sessionStorage || false;
33        this.init();
34
35    }
36    $.extend(true, Plugin.prototype, $.fn['amShared']);
37    $.extend(Plugin.prototype, {
38        init: function () {
39            var self = this;
40
41            self.settings.segmentSize = parseInt(self.settings.room.restrictions.segment_size, 10);
42            self.settings.blockSize = self.settings.segmentSize * parseInt(self.settings.room.restrictions.block_count, 10);
43
44            self.libraryOpenTime  = moment.tz(self.settings.date + ' ' + self.settings.hours.open,  'YYYY-MM-DD h:mma', self.settings.timezone);
45            self.libraryCloseTime = moment.tz(self.settings.date + ' ' + self.settings.hours.close, 'YYYY-MM-DD h:mma', self.settings.timezone);
46
47            self.roomOpenTime = self.libraryOpenTime.clone().add(parseInt(self.settings.room.restrictions.day_start_buffer, 10), 'minutes');
48            self.roomCloseTime = self.libraryCloseTime.clone().subtract(parseInt(self.settings.room.restrictions.day_end_buffer, 10), 'minutes');
49
50            self._buildInterface();
51            self._bindEvents();
52            self._update();
53            if (self.settings.selected) self._makeActive();
54
55
56        },
57        _buildInterface: function () {
58            this.currency = $('<span class="amnp-currency link fa-stack fa-lg"><i class="fa fa-circle fa-stack-2x"></i><i class="fa fa-usd fa-stack-1x fa-inverse"></i></span>');
59            this.segmentCount = 1440 / this.settings.segmentSize;
60
61            var locationId = this.settings.location ? this.settings.location.id : 'noloc';
62            var roomTitleId = 'amnp-room-title-' + this.settings.room.id + '-' + locationId;
63			this.holder = $('<div class="amnp-room-holder" data-mode="' + this.settings.mode + '"></div>').appendTo(this.element);
64			this.holder.attr({
65				tabindex: -1,
66				role: 'button',
67				'aria-expanded': 'false',
68				'aria-labelledby': roomTitleId
69			});
70			this.holder.attr('data-room-id', this.settings.room.id);
71            $('<h3/>').attr('id', roomTitleId).text(this.settings.room.title).appendTo(this.holder);
72			this.dayHolder = $('<div class="amnp-day-holder"></div>').attr('tabindex', '-1').appendTo(this.holder);
73            this.day = $('<div class="amnp-day"></div>').appendTo(this.dayHolder);
74
75            var activeCount = 0;
76			for (var i = 0; i < 1440; i += this.settings.segmentSize) {
77                var segmentTime = moment.tz(this.settings.date, this.settings.timezone).startOf('day').add(i, 'minutes');
78				var segmentLabel = segmentTime.format('h:mm a');
79				var segment = $('<div class="amnp-segment" data-time="' + segmentLabel + '"></div>')
80					.attr({
81						tabindex: '-1',
82						role: 'button',
83						'aria-pressed': 'false',
84						'aria-disabled': 'true',
85						'aria-label': segmentLabel
86					})
87					.data('segmentTime', segmentTime)
88					.css({ width: this.settings.segmentWidth + 'px' })
89					.appendTo(this.day);
90                if (segmentTime.format('m') == 0) {
91                    segment.addClass('odd');
92                }
93                if (this.settings.mode === 1) {
94                    activeCount++;
95                    if (segmentTime >= this.roomOpenTime && segmentTime < this.roomCloseTime) {
96                        segment.addClass('amnp-segment-open');
97                        segment.attr('aria-disabled', 'false');
98                    }
99                } else {
100                    if (segmentTime >= this.roomOpenTime && segmentTime < this.roomCloseTime) {
101                        segment.addClass('amnp-segment-open');
102                        segment.attr('aria-disabled', 'false');
103                        activeCount++;
104                    } else {
105                        segment.hide();
106                    }
107                }
108            }
109			this.day.css({ width: (this.settings.segmentWidth * activeCount) + 'px' });
110            var self = this;
111            if (this.settings.mode !== 1) {
112                var now = moment.tz(this.settings.timezone);
113                var allowActiveBlockReservation = (self.clientSettings || {}).rooms_allow_active_block_reservation === true;
114                var currentBlockStart = self._activeBlockStart({ now: now, segmentSize: self.settings.segmentSize });
115                $('.amnp-segment', self.day).each(function (idx, el) {
116                    var $segment = $(el);
117                    var segmentTime = $segment.data('segmentTime');
118                    var isAllowedBackfillStart =
119                        allowActiveBlockReservation &&
120                        segmentTime &&
121                        currentBlockStart &&
122                        segmentTime.isSame(currentBlockStart);
123                    if (segmentTime < now && !isAllowedBackfillStart) {
124                        $segment.addClass('amnp-segment-booked');
125                        $segment.attr('aria-disabled', 'true');
126                    } else if (isAllowedBackfillStart && $segment.hasClass('amnp-segment-open')) {
127                        $segment.removeClass('amnp-segment-booked');
128                        $segment.attr('aria-disabled', 'false');
129                    }
130                });
131            }
132
133			this.settings.room.bookings.forEach(function (booking) {
134                var start  = moment.tz(booking.start_time, self.settings.timezone);
135                var end    = moment.tz(booking.end_time,   self.settings.timezone);
136                var bookingStart = start.clone();
137                var bookingEnd = end.clone();
138                var hasParent = false;
139
140                if (self.settings.useSetupBreakdown && (booking.event_id || null) === null) {
141
142                    var useLayout = false;
143                    if (parseInt(booking.layout_id) > 0 && self.settings.room.layouts.length > 0) {
144                        self.settings.room.layouts.forEach(function (layout) {
145                            if (parseInt(layout.id) === parseInt(booking.layout_id)) {
146                                bookingEnd.add(layout.breakdown_time, 'minutes');
147                                bookingStart.add(layout.setup_time * -1, 'minutes');
148                                useLayout = true;
149                            }
150                        });
151                    }
152
153                    if (!useLayout) {
154                        bookingEnd.add(booking.breakdown_time, 'minutes');
155                        bookingStart.add(booking.setup_time * -1, 'minutes');
156                    }
157                }
158
159                self.day.find('.amnp-segment').each(function (idx, el) {
160                    var segmentTime = $(el).data('segmentTime');
161		    var segmentEndTime = moment($(el).data('segmentTime')).add(self.settings.segmentSize, 'minutes');
162                    if (segmentTime >= start && segmentTime < end) {
163						$(el).addClass('amnp-segment-booked').attr('data-show-booking', booking.id).attr('data-status', booking.status).data('booking', booking);
164						$(el).attr('aria-disabled', 'true');
165						if (booking.t_id) {
166							$(el).addClass('amnp-segment-basket');
167						}
168					} else if (self.settings.useSetupBreakdown && bookingStart.isBefore(segmentEndTime) && bookingEnd.isAfter(segmentTime)) {
169						$(el).addClass('amnp-segment-booked').attr('data-show-booking', booking.id).attr('data-status', booking.status).data('booking', booking);
170						$(el).attr('aria-disabled', 'true');
171						if (booking.t_id) {
172							$(el).addClass('amnp-segment-basket amnp-segment-basket-setup-breakdown');
173						}
174					}
175				});
176			});
177			this._refreshSegmentAria();
178			this._ensureSegmentFocusTarget();
179
180			this.resetButton = $('<button type="button" class="amnp-reset-button btn" aria-label="Reset selection"><i class="fa fa-undo"/></button>').prependTo(this.holder);
181            var keylist = $('<div class="amnp-key-list"></div>').appendTo(this.holder);
182
183            // this.closeButton = $('<div class="amnp-close-button btn"><i class="am-cancel"/></div>').prependTo(this.holder);
184            var blockMessage = "Select green time blocks...";
185            var blockColor = "#22A519";
186            if (typeof (self.clientSettings.rooms_select_time_block_message) !== "undefined") {
187                blockMessage = self.clientSettings.rooms_select_time_block_message;
188            }
189
190            var blockMessageEle = $('<div class="amnp-unselected-message">' + blockMessage + '</div>');
191            if (typeof (self.clientSettings.rooms_select_time_block_color) !== "undefined") {
192                blockColor = self.clientSettings.rooms_select_time_block_color;
193                blockMessageEle.css("color", blockColor + " !important");
194            }
195
196            blockMessageEle.appendTo(this.holder);
197			this.message = $('<div class="amnp-message" role="status" aria-live="polite"></div>').appendTo(this.holder);
198			this.selection = $('<div class="amnp-selection"><span class="amnp-title link">Time and date</span> </div>').appendTo(this.holder);
199			var detailsId = 'amnp-booking-details-' + this.settings.room.id;
200			this.bookingDetails = $('<span class="amnp-booking-details"></span>').attr('id', detailsId).appendTo(this.selection);
201            this.details = $('<div class="amnp-room-details"></div>').appendTo(this.holder)
202
203            this.footer = $('<div class="amnp-room-footer"></div>').appendTo(this.holder);
204            this.chargeTotal = $('<div class="amnp-charge-total"></div>').appendTo(this.footer);
205			this.checkoutNowButton = $('<button class="amnp-checkout-now btn btn-info">Reserve now</button>').attr('aria-describedby', detailsId).appendTo(this.footer).hide();
206			this.bookingButton = $('<button class="amnp-booking-button btn"></button>').text(this.settings.enabledCart ? 'Add to basket' : 'Choose').attr('aria-describedby', detailsId).appendTo(this.footer).hide();
207			this.cartMessage = $('<div class="amnp-cart-message" role="status" aria-live="polite"></div>').appendTo(this.holder);
208			this._registerRoomFocus();
209
210            $('<div><span class="amnp-segment amnp-segment-open"/>Available</div>').appendTo(keylist);
211            $('<div><span class="amnp-segment amnp-segment-booked"/>Unavailable</div>').appendTo(keylist);
212            $('<div><span class="amnp-segment amnp-segment-booked amnp-segment-selected"/>Clash</div>').appendTo(keylist);
213            if (this.settings.useSetupBreakdown) {
214                $('<div><span class="amnp-segment amnp-segment-open amnp-segment-setup-breakdown"/>Setup/Breakdown</div>').appendTo(keylist);
215            }
216            this.chargeDetails = $('<div class="amnp-charge-details"></div>').appendTo(keylist);
217
218            setTimeout(function () {
219                // self.dayHolder.scrollLeft(self.day.width()/2);
220                var s = $('.amnp-segment-open:not(.amnp-segment-booked)', self.day);
221                if (s.length > 0) {
222                    self.dayHolder.scrollLeft(s.first().position().left - 15);
223                }
224            }, 100);
225        },
226		_addToBasket: function (checkout) {
227			this._updateBookedAssets();
228			this.$element.trigger('room:addtocart', {
229				checkout: checkout, details: {
230					start: this.selected.start.format('YYYY-MM-DD HH:mm:ss'),
231                    end: this.selected.end.format('YYYY-MM-DD HH:mm:ss'),
232                    room: this.settings.room,
233                    location: this.settings.location,
234                    assets: this.bookedAssets,
235                    layout: this.selectedLayout,
236                    breakdown_time: this.selectedLayout ? this.selectedLayout.breakdown_time : this.settings.room.breakdown_time,
237                    setup_time: this.selectedLayout ? this.selectedLayout.setup_time : this.settings.room.setup_time,
238                    booking_class: this.settings.bookingClass
239				}
240			});
241			if (!checkout) {
242				var message = this.settings.room && this.settings.room.title ? this.settings.room.title + ' added to basket' : 'Room added to basket';
243				this.cartMessage.text(message);
244			}
245			if (!checkout) {
246				this._reset();
247			}
248		},
249        _bindEvents: function () {
250            var self = this;
251            this.checkoutNowButton.on('click', function (event) {
252                event.preventDefault();
253                event.stopPropagation();
254
255                if (self.selected.start.format('YYYY-MM-DD HH:mm:ss') === self.selected.end.format('YYYY-MM-DD HH:mm:ss')) {
256                    alert('Event must have a valid duration, start and end time cannot be the same.');
257                    return;
258                }
259
260                self._addToBasket(true);
261            });
262            this.bookingButton.on('click', function (event) {
263                event.preventDefault();
264                event.stopPropagation();
265
266                if (self.selected.start.format('YYYY-MM-DD HH:mm:ss') === self.selected.end.format('YYYY-MM-DD HH:mm:ss')) {
267                    alert('Event must have a valid duration, start and end time cannot be the same.');
268                    return;
269                }
270
271                self._addToBasket(!self.settings.enabledCart);
272            });
273			this.details.on('click', '.amnp-layouts li', function (event) {
274                event.preventDefault();
275                event.stopPropagation();
276                var li = $(this).closest('li');
277                var input = li.find('input');
278                if (input.is(':checked')) {
279                    $(this).closest('.amnp-layouts').find('input').removeProp('checked');
280                    $(this).closest('.amnp-layouts').children('li').attr('aria-checked', 'false');
281                    self.selectedLayout = false;
282                    var maxCap = Math.max(self.settings.room.capacity_standing, self.settings.room.capacity_chairs, self.settings.room.capacity_tables, 0);
283
284                    // CH 29929 to limit displaying these when the flag is off
285                    if (self.clientSettings.rooms_use_setup) {
286                        $("#breakdown_time").html("Breakdown Time: " + (self.settings.room.breakdown_time ? self.settings.room.breakdown_time : 0) + "<br />");
287                        $("#setup_time").html("Setup Time: " + (self.settings.room.setup_time ? self.settings.room.setup_time : 0) + "<br />");
288                    }
289
290                    $("#total_capacity").text("Total Capacity: " + maxCap);
291                } else {
292                    $(this).closest('.amnp-layouts').find('input').removeProp('checked');
293                    self.selectedLayout = li.data('layout');
294
295                    // CH 29929 to limit displaying these when the flag is off
296                    if (self.clientSettings.rooms_use_setup) {
297                        // Fix for CH#3432
298                        $("#breakdown_time").html("Breakdown Time: " + (self.selectedLayout.breakdown_time ? self.selectedLayout.breakdown_time : 0) + "<br />");
299                        $("#setup_time").html("Setup Time: " + (self.selectedLayout.setup_time ? self.selectedLayout.setup_time : 0) + "<br />");
300                    }
301
302                    if (self.selectedLayout.capacity > 0) {
303                        $("#total_capacity").text("Total Capacity: " + self.selectedLayout.capacity);
304                    }
305                    input.prop('checked', true);
306                    $(this).closest('.amnp-layouts').children('li').attr('aria-checked', 'false');
307                    li.attr('aria-checked', 'true');
308                }
309
310                self._update();
311            }).on('click', '.amnp-layouts li input', function (event) {
312                event.stopPropagation();
313                if ($(this).is(':checked')) {
314                    $(this).removeProp('checked');
315                } else {
316                    $(this).prop('checked', true);
317                }
318                $(this).closest('li').trigger('click');
319            }).on('keydown', '.amnp-layouts li', function (event) {
320                var key = event.key;
321                var keyCode = event.which || event.keyCode;
322                if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
323                    event.preventDefault();
324                    $(this).trigger('click');
325                }
326            }).on('change', '.amnp-bookable-assets-list select', function (event) {
327                event.preventDefault();
328                var li = $(this).closest('li');
329                li.find('input').prop('checked', true);
330                li.attr('aria-checked', 'true');
331                self._updateBookedAssets();
332                self._updateTotal();
333            }).on('change', '.amnp-bookable-assets-list input', function (event) {
334                event.preventDefault();
335                var li = $(this).closest('li');
336                li.attr('aria-checked', $(this).is(':checked') ? 'true' : 'false');
337                self._updateBookedAssets();
338                self._updateTotal();
339			}).on('keydown', '.amnp-bookable-assets-list li', function (event) {
340                var key = event.key;
341                var keyCode = event.which || event.keyCode;
342                if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
343                    event.preventDefault();
344                    var checkbox = $(this).find('input[type="checkbox"]').first();
345                    if (checkbox.length && !checkbox.is(':disabled')) {
346                        checkbox.prop('checked', !checkbox.prop('checked')).trigger('change');
347                    }
348                }
349            });
350		var focusableSegments = this.day.find('.amnp-segment');
351		focusableSegments.attr('tabindex', '-1');
352
353		this.holder.on('click', function () {
354			self._makeActive();
355		}).on('keydown', function (event) {
356			var key = event.key;
357			var keyCode = event.which || event.keyCode;
358
359			if (key === 'Tab') {
360				if (self._handleHolderTab(event, $(event.target))) {
361					return;
362				}
363			}
364			if (event.target !== self.holder[0]) {
365				return;
366			}
367
368			if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
369				event.preventDefault();
370				event.stopPropagation();
371				self._makeActive();
372				self._prepareTimelineFocus();
373				return;
374			}
375
376                if (key === 'ArrowDown' || keyCode === 40) {
377                    event.preventDefault();
378                    event.stopPropagation();
379                    self._focusAdjacentRoom('next');
380                    return;
381                }
382
383                if (key === 'ArrowUp' || keyCode === 38) {
384                    event.preventDefault();
385                    event.stopPropagation();
386                    self._focusAdjacentRoom('prev');
387                }
388            });
389			this.day.on('click', '.amnp-segment', function (event) {
390                if (self.settings.mode !== 1 && !$(this).hasClass('amnp-segment-open')) return;
391                self._makeActive();
392
393                var segmentTime = $(this).data('segmentTime');
394                self._setSegmentFocusTarget($(this));
395
396				self.holder.addClass('amnp-room-selection');
397                var selectionChange = {
398                    didSelect: !$(this).hasClass('amnp-segment-selected'),
399                    startChanged: false,
400                    endChanged: false
401                };
402				if (!selectionChange.didSelect) {
403                    var startDiff = segmentTime.diff(self.selected.start, 'minutes');
404                    var endDiff = (segmentTime.diff(self.selected.end, 'minutes') * -1);
405
406                    if (startDiff === 0) {
407                        self.selected.start = segmentTime.clone().add(self.settings.segmentSize, 'minutes');
408                        selectionChange.startChanged = true;
409                    } else if (endDiff === 0) {
410                        self.selected.end = segmentTime.clone().subtract(self.settings.segmentSize, 'minutes');
411                        selectionChange.endChanged = true;
412                    } else if (startDiff <= endDiff) {
413                        self.selected.start = segmentTime.clone();
414                        selectionChange.startChanged = true;
415                    } else {
416                        self.selected.end = segmentTime.clone();
417                        selectionChange.endChanged = true;
418                    }
419                } else if (!$(this).hasClass('amnp-segment-booked')) {
420                    if (!self.selected.start) {
421                        self.selected.start = segmentTime.clone();
422                        selectionChange.startChanged = true;
423                    }
424                    if (!self.selected.end) {
425                        self.selected.end = segmentTime.clone().add(self.settings.segmentSize, 'minutes');
426                        selectionChange.endChanged = true;
427                    }
428                    if (segmentTime >= self.selected.end) {
429                        self.selected.end = segmentTime.clone().add(self.settings.segmentSize, 'minutes');
430                        selectionChange.endChanged = true;
431                    } else if (segmentTime < self.selected.start) {
432                        self.selected.start = segmentTime.clone();
433                        selectionChange.startChanged = true;
434                    }
435                }
436                if (self.selected.start > self.selected.end || self.selected.start === self.selected.end) {
437                    self.selected = { start: null, end: null };
438                    self.bookingButton.hide();
439                    self.holder.removeClass('amnp-room-selection');
440                } else {
441                    // var duration = moment.duration(self.selected.end.clone().add(self.settings.segmentSize, 'minutes').diff(self.selected.start)).asMinutes();
442                    // if(duration % self.settings.blockSize !== 0){
443                    //     self.selected.end = self.selected.start.clone().add((Math.ceil(duration/self.settings.blockSize)*self.settings.blockSize)-self.settings.segmentSize, 'minutes');
444                    // }
445				}
446                self._update();
447                if (event.originalEvent && event.originalEvent.type === 'click') {
448                    self._focusSelectedSegments($(this), selectionChange);
449                } else if (event.isTrigger) {
450                    self._focusSegment($(this));
451                }
452            }).on('keydown', '.amnp-segment', function (event) {
453                var key = event.key;
454                var keyCode = event.which || event.keyCode;
455                var $segment = $(this);
456                var selectionKeys = ['Enter', ' ', 'Spacebar'];
457
458                if (selectionKeys.indexOf(key) > -1 || keyCode === 13 || keyCode === 32) {
459                    if ($segment.attr('aria-disabled') === 'true') {
460                        event.preventDefault();
461                        return;
462                    }
463
464                    event.preventDefault();
465                    event.stopPropagation();
466                    $segment.trigger('click');
467                    return;
468                }
469
470                var direction = null;
471                if (key === 'ArrowRight' || keyCode === 39) direction = '
471next';
472                if (key === 'ArrowLeft' || keyCode === 37) direction = 'prev';
473                if (key === 'Home' || keyCode === 36) direction = 'first';
474                if (key === 'End' || keyCode === 35) direction = 'last';
475
476                if (!direction) return;
477
478                event.preventDefault();
479                event.stopPropagation();
480
481                var $candidate = $();
482                if (direction === 'next' || direction === 'prev') {
483                    var $siblings = direction === 'next' ? $segment.nextAll('.amnp-segment:visible') : $segment.prevAll('.amnp-segment:visible');
484                    $siblings.each(function () {
485                        if ($(this).attr('aria-disabled') !== 'true') {
486                            $candidate = $(this);
487                            return false;
488                        }
489                        return true;
490                    });
491                } else if (direction === 'first') {
492                    $candidate = self.day.find('.amnp-segment:visible').filter(function () {
493                        return $(this).attr('aria-disabled') !== 'true';
494                    }).first();
495                } else if (direction === 'last') {
496                    $candidate = self.day.find('.amnp-segment:visible').filter(function () {
497                        return $(this).attr('aria-disabled') !== 'true';
498                    }).last();
499                }
500
501			if ($candidate && $candidate.length) {
502				self._focusSegment($candidate);
503			}
504            });
505			this.resetButton.on('click', function () {
506                self._reset();
507            }).on('keydown', function (event) {
508                var key = event.key;
509                var keyCode = event.which || event.keyCode;
510                if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
511                    event.preventDefault();
512                    self._reset();
513                }
514            });
515        },
516        _calculateRoomCharge: function () {
517            if (!this.selected || !this.selected.start) return 0;
518            var charge = parseFloat(this.settings.room.restrictions.charge_amount);
519            if (parseInt(this.settings.room.restrictions.charge_period) === 1) return charge;
520            var blockSize = parseInt(this.settings.room.restrictions.segment_size) * parseInt(this.settings.room.restrictions.block_count);
521            var rawDuration = moment.duration(this.selected.end.clone().diff(this.selected.start)).asMinutes();
522            var total = (rawDuration / blockSize) * charge;
523            return total;
524        },
525        _updateTotal: function () {
526            var total = this.bookedAssets.reduce(function (total, asset) {
527                return total + parseFloat(asset.cost) * asset.qty;
528            }, 0);
529            if (this.selectedLayout && this.selectedLayout.cost && parseFloat(this.selectedLayout.cost) > 0) {
530                total += parseFloat(this.selectedLayout.cost);
531            }
532            total += this._calculateRoomCharge();
533            this.chargeTotal.empty();
534            if (total > 0 && this.settings.chargingEnabled) {
535                this.chargeTotal.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(total));
536            }
537        },
538        _formatCurrency: function (amount) {
539            return amount.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
540        },
541        _updateAssets: function () {
542            var options = {
543                start: this.selectedBookingStart.clone().format('YYYY-MM-DD HH:mm:ss'),
544                end: this.selectedBookingEnd.clone().format('YYYY-MM-DD HH:mm:ss'),
545            }
546
547			if (this.settings.bookingType === 0 || this.settings.mode === 1) {
548                options.staff = 1;
549            }
550            if (parseInt(this.settings.bookingClass) > 0) {
551                options.class_id = this.settings.bookingClass;
552            }
553
554            var self = this;
555            this._getJSON(this.settings.apiServer + '/v2/' + this.settings.client + '/rooms/assets/' + this.settings.room.location_id + '/' + this.settings.room.id, options).then(function (json) {
556                var resourcesTitle = $('.resources-title', self.holder);
557                var col4 = $('.col4', self.holder).empty().show();
558                self.bookableAssets = {};
559                json.forEach(function (asset) {
560                    self.bookableAssets[asset.id] = asset;
561                });
562                if (Object.keys(self.bookableAssets).length > 0) {
563                    resourcesTitle.show();
564                    var assetList = $('<ul class="amnp-bookable-assets-list"></ul>').appendTo(col4);
565                    $.each(self.bookableAssets, function (index, val) {
566                        if (parseInt(val.quantity) > 0) {
567					var li = $('<li><label><input id="input-' + val.id + '" data-id="' + val.id + '" type="checkbox" tabindex="-1">&nbsp;&nbsp;' + val.name + ' </label></li>').attr({ tabindex: 0, role: 'checkbox', 'aria-checked': 'false' }).data('details', val).appendTo(assetList);
568                            $("#input-" + val.id).change(function () {
569                                if (parseInt($('#select-' + val.id + ' option:eq(0)').val(), 10) == 0) {
570                                    if (this.checked) {
571                                        $('#select-' + val.id + ' option:eq(1)').prop('selected', true);
572                                    } else {
573                                        $('#select-' + val.id + ' option:eq(0)').prop('selected', true);
574                                    }
575                                }
576                            });
577                            if (parseInt(val.quantity) < parseInt(val.booking_max)) val.booking_max = val.quantity;
578                            if (parseInt(val.booking_max) > 1) {
579                                var s = $('<select id="select-' + val.id + '" data-id="' + val.id + '"></select>
579').appendTo(li);
580                                for (var i = parseInt(val.booking_min); i <= parseInt(val.booking_max); i++) {
581                                    $('<option>' + i + '</option>').appendTo(s);
582                                }
583                            }
584                        } else {
585                            var li = $('<li class="amnp-none-available"><label><input type="checkbox" disabled>&nbsp;&nbsp;' + val.name + ' (none available at the chosen time)</label></li>').data('details', val).appendTo(assetList);
586                        }
587                        if (self.settings.chargingEnabled) {
588                            if (val.cost > 0) {
589                                if (parseInt(val.booking_max) > 1) {
590                                    $('<span> (' + self.settings.payment_currency_symbol + '' + val.cost + ' each)</span>').appendTo(li);
591                                } else {
592                                    $('<span> (' + self.settings.payment_currency_symbol + '' + val.cost + ')</span>').appendTo(li);
593                                }
594                            }
595                        }
596                    });
597                    assetList.children().detach().sort(function (a, b) {
598                        return $(a).text().localeCompare($(b).text());
599                    }).appendTo(assetList);
600
601                    self.bookedAssets.forEach(function (asset) {
602                        assetList.find('input[data-id=' + asset.id + ']').prop('checked', true);
603                        if (asset.qty > 1) {
604                            assetList.find('select[data-id=' + asset.id + ']').val(asset.qty);
605                        }
606                    });
607                } else {
608                    resourcesTitle.hide();
609                }
610            }).catch(function (err) {
611                console.error(err);
612            });
613
614        },
615        _getRoomDetails: function (room, location) {
616            var self = this;
617            var details = $('<div/>');
618            var row1 = $('<div class="row1"/>').addClass('row').appendTo(details);
619
620            if (room.layouts.length > 0) {
621                $('<div class="amnp-title link">Room layout options</div>').appendTo(details);
622                var layoutRow = $('<div class="layoutRow"/>').addClass('row').appendTo(details);
623                var layouts = $('<ul class="amnp-layouts"/>').appendTo(layoutRow);
624				room.layouts.forEach(function (layout) {
625					var l = $('<li></li>').attr({ tabindex: 0, role: 'checkbox', 'aria-checked': 'false' }).appendTo(layouts).data('layout', layout);
626					var label = $('<label class="amnp-layout-name"></label>').text(' ' + layout.name).appendTo(l);
627					if (parseFloat(layout.cost) > 0) {
628						label.append(' (' + self.settings.payment_currency_symbol + '' + self._formatCurrency(parseFloat(layout.cost)) + ')');
629					}
630					$('<input type="checkbox" tabindex="-1"/>').prependTo(label);
631                    $('<img>').attr('src', 'https://' + self.settings.client + '.libnet.info/images/roomlayouts/' + layout.image).attr('alt', 'Room Layout Image').appendTo(l);
632                    $('<span class="amnp-layout-capacity"></span>').text('capacity ' + layout.capacity).appendTo(l);
633                    // Fix for CH#3429
634                    if (layout.description !== '' && layout.description !== null) {
635                        l.qtip({
636                            content: {
637                                text: layout.description
638                            },
639                            style: {
640                                classes: 'qtip-bootstrap events-popup',
641                                def: false
642                            },
643                            position: {
644                                target: 'mouse',
645                                adjust: {
646                                    mouse: false
647                                }
648                            }
649                        });
650                    }
651                });
652            }
653
654            $('<div class="amnp-title link resources-title">Additional resources</div>').appendTo(details).hide();
655            var row2 = $('<div class="row2"/>').addClass('row').appendTo(details);
656            var col1 = $('<div class="col1"/>').addClass('col-md-6').appendTo(row1);
657            var col2 = $('<div class="col2"/>').addClass('col-md-6').appendTo(row1);
658
659            var col4 = $('<div class="col4"/>').addClass('col-md-12').appendTo(row2);
660            var row3 = $('<div class="row3"/>').addClass('row').appendTo(details);
661            var col5 = $('<div class="col1"/>').addClass('col-md-6').appendTo(row3);
662            var col6 = $('<div class="col2"/>').addClass('col-md-6').appendTo(row3);
663
664            if (room.image && room.image.length > 0) {
665                $('<img/>').attr('src', '//' + this.settings.client + '.libnet.info/images/roombooking/' + this.settings.client + '/' + room.image).attr('alt', room.title).appendTo(col1);
666            }
667
668            $('<div><b class="with-margin">Location</b></div>').appendTo(col1);
669            $('<div class=""><i class="am-locations"/> ' + location.name + '</div>').appendTo(col1);
670            if (parseInt(room.length, 10) > 0 || parseInt(room.width, 10) > 0 || parseInt(room.area, 10) > 0) {
671                $('<div><b class="with-margin">Room size</b></div>').appendTo(col1);
672            }
673
674            if (typeof (self.clientSettings.client_use_metric) !== 'undefined') {
675                self.settings.useMetric = self.clientSettings.client_use_metric;
676                if (self.settings.useMetric == 1) {
677                    self.settings.units = 'm';
678                }
679            }
680
681            if (parseInt(room.length, 10) > 0) $('<span class="room-size-descriptors">length:&nbsp;<span class="room-size-descriptors-value">' + room.length + '' + self.settings.units + ' </span></span>').appendTo(col1);
682            if (parseInt(room.width, 10) > 0) $('<span class="room-size-descriptors">width:&nbsp;<span class="room-size-descriptors-value">' + room.width + '' + self.settings.units + ' </span></span>').appendTo(col1);
683            if (parseInt(room.area, 10) > 0) $('<span class="room-size-descriptors">area:&nbsp;<span class="room-size-descriptors-value">' + room.area + ' sq ' + self.settings.units + '</span></span>').appendTo(col1);
684            if (parseInt(room.length, 10) > 0 && parseInt(room.width, 10) > 0 && parseInt(room.area, 10) <= 0) $('<span class="room-size-descriptors">area:&nbsp;<span class="room-size-descriptors-value">' + (room.length * room.width) + ' sq ' + self.settings.units + '</span></span>').appendTo(col1);
685            if (room.description && room.description.length > 0) {
686                $('<div><b>Room description</b></div>').appendTo(col2);
687                $('<div/>').html(room.description).appendTo(col2);
688                $('<div class="amnp-layout-details"/>').appendTo(col2);
689            }
690
691            // #4762 - CK
692            var maxCap = Math.max(room.capacity_standing, room.capacity_chairs, room.capacity_tables, 0);
693
694            $('<div><b class="with-margin">Room Capacity</b></div>').appendTo(col5);
695            $('<span id="total_capacity">Total Capacity: ' + maxCap + '</span><br>').appendTo(col5);
696
697            var breakdownTime = (room.breakdown_time ? room.breakdown_time : 0);
698            var setupTime = (room.setup_time ? room.setup_time : 0);
699
700            if (self.clientSettings.rooms_use_setup) {
701                $('<div><b class="with-margin">Setup Times</b></div>').appendTo(col6);
702                $('<span id="breakdown_time">Breakdown Time: ' + breakdownTime + '<br></span>').appendTo(col6);
703                $('<span id="setup_time">Setup Time: ' + setupTime + '</span>').appendTo(col6);
704            }
705
706            if (room.assets && room.assets.length > 0) {
707                $('<div><b class="with-margin">Included resources</b></div>').appendTo(col2);
708
709                $.each(room.assets, function (index, asset) {
710                    var a = $('<div class="amnp-resources-asset"></div>').appendTo(col2);
711                    $('<span class="amnp-resources-asset-name">' + asset.name + '</span>').appendTo(a);
712                    if (asset.qty > 1) {
713                        $('<span class="amnp-resources-asset-qty"> (' + asset.qty + ')</span>').appendTo(a);
714                    }
715                    if (asset.description) {
716                        $('<div class="amnp-resources-asset-description">' + asset.description + '</div>').appendTo(a);
717                    }
718                });
719            }
720            return details;
721        },
722        _getLayoutDetailView: function (layout) {
723            var view = $('<div class="amnp-room-layout-details"></div>');
724            $('<img>').attr('src', 'http://' + this.settings.client + '.libnet.info/images/roomlayouts/' + layout.image).attr('alt', 'Room Layout Image').appendTo(view);
725            $('<div class="amnp-room-layout-title"></div>').text(layout.name).appendTo(view);
726            $('<div class="amnp-room-layout-capacity"><span>Capacity</span> ' + layout.capacity + '</div>').appendTo(view);
727            if (parseInt(layout.setup_time, 10) > 0) $('<div class="amnp-room-layout-capacity"><span>Setup</span> ' + layout.setup_time + '</div>').appendTo(view);
728            if (parseInt(layout.breakdown_time, 10) > 0) $('<div class="amnp-room-layout-capacity"><span>Breakdown</span> ' + layout.breakdown_time + '</div>').appendTo(view);
729            $('<div class="amnp-room-layout-description"></div>').text(layout.description).appendTo(view);
730            return view;
731        },
732        _reset: function () {
733            this.selected = { start: null, end: null };
734            this.selectedLayout = false;
735            this.holder.removeClass('amnp-room-active amnp-room-selection amnp-has-error');
736            this.holder.attr('aria-expanded', 'false');
737            this.details.empty();
738            this.bookingButton.hide();
739            this._update();
740        },
741        _updateBookedAssets: function () {
742            var self = this;
743            self.bookedAssets = [];
744
745            $('.amnp-bookable-assets-list li', self.details).each(function (index, el) {
746                if ($(this).find('input').is(':checked')) {
747                    var details = $(this).data('details');
748                    var qty = parseInt(details.booking_max) > 1 ? $(this).find('select').val() : 1;
749
750                    self.bookedAssets.push({
751                        id: details.id,
752                        qty: parseInt(qty),
753                        cost: details.cost,
754                        name: details.name,
755                        assetData: details
756                    });
757                }
758            });
759        },
760        _scrollToSegment: function ($segment) {
761            if (!$segment || !$segment.length) return;
762
763            this.dayHolder.mCustomScrollbar('scrollTo', $segment, { scrollInertia: 400, timeout: this.scrollbarJustInitialized ? 100 : 0 });
764        },
765		_makeActive: function () {
766			if (!this.barsAdded) {
767				var currentScrollLeft = this.dayHolder.scrollLeft();
768
769				this.scrollbarJustInitialized = true;
770				var self = this;
771
772				this.dayHolder.mCustomScrollbar({
773					axis: 'x',
774					theme: 'rounded-dark',
775					mouseWheel: { enable: true, axis: 'x' },
776					snapAmount: this.settings.segmentWidth,
777					scrollbarPosition: 'outside',
778					setLeft: currentScrollLeft + 'px'
779				});
780
781				setTimeout(function() {
782					self.scrollbarJustInitialized = false;
783				}, 150);
784
785                this._scrollToSegment($('.amnp-segment-open:not(.amnp-segment-booked)', this.day));
786				this.barsAdded = true;
787			}
788			this.dayHolder.find('.mCustomScrollBox, .mCSB_container, .mCSB_scrollTools').attr('tabindex', '-1');
789			if (!this.holder.hasClass('amnp-room-active')) {
790				$('.amnp-room-active').each(function (index, el) {
791					$(el).parent().data('plugin_amReserveRoom')._reset();
792				});
793				this.holder.addClass('amnp-room-active');
794				this.holder.attr('aria-expanded', 'true');
795				this._setRoomTabTarget(this.holder);
796				this.details.empty().append(this._getRoomDetails(this.settings.room, this.settings.location));
797				this._prepareTimelineFocus();
798			}
799		},
800		_prepareTimelineFocus: function () {
801			this._ensureSegmentFocusTarget();
802		},
803		_handleHolderTab: function (event, $target) {
804			var isActive = this.holder.hasClass('amnp-room-active');
805			$target = $target || $(event.target);
806			var isHolderTarget = $target[0] === this.holder[0];
807
808			if (!isActive) {
809				return false;
810			}
811
812			var focusables = this._getRoomFocusables();
813			if (!focusables.length) {
814				return false;
815			}
816
817			var index = focusables.index($target);
818			if (isHolderTarget) {
819				index = -1;
820			}
821
822			if (event.shiftKey) {
823				if (isHolderTarget) {
824					event.preventDefault();
825					focusables.last().focus();
826					return true;
827				}
828				if (index <= 0) {
829					event.preventDefault();
830					this._setRoomTabTarget(this.holder);
831					this.holder.focus();
832					return true;
833				}
834				return false;
835			}
836
837			if (isHolderTarget) {
838				this._setRoomTabTarget(this.holder);
839				return false;
840			}
841
842			if (index === -1) {
843				event.preventDefault();
844				this._focusFirstAvailableSegment();
845				return true;
846			}
847
848			if (index >= focusables.length - 1) {
849				event.preventDefault();
850				this._setRoomTabTarget(this.holder);
851				this.holder.focus();
852				return true;
853			}
854
855			return false;
856		},
857		_getRoomFocusables: function () {
858			var focusables = $();
859			if (this.holder.hasClass('amnp-room-active')) {
860				var currentSegment = this.day.find('.amnp-segment[tabindex="0"]');
861				if (currentSegment.length) {
862					focusables = focusables.add(currentSegment);
863				}
864			}
865			var selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
866			var additional = this.holder.find(selector)
867				.filter(':visible')
868				.not(this.day.find('.amnp-segment'))
869				.not(this.holder)
870				.not(this.resetButton)
871				.not('.mCustomScrollBox, .mCSB_container, .mCSB_scrollTools');
872			focusables = focusables.add(additional);
873			if (this.resetButton && this.resetButton.length) {
874				focusables = focusables.add(this.resetButton);
875			}
876			return focusables;
877		},
878		_findNextPageFocusable: function ($start, forward) {
879			var selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
880			var allFocusable = $(selector).filter(':visible');
881			if (!allFocusable.length) return $();
882			var currentIndex = allFocusable.index($start);
883			if (currentIndex === -1) {
884				return forward ? allFocusable.first() : allFocusable.last();
885			}
886			var targetIndex = forward ? currentIndex + 1 : currentIndex - 1;
887			if (targetIndex >= allFocusable.length) {
888				targetIndex = allFocusable.length - 1;
889			}
890			if (targetIndex < 0) {
891				targetIndex = 0;
892			}
893			return allFocusable.eq(targetIndex);
894		},
895		_registerRoomFocus: function () {
896			var $scope = this.holder.closest('.amnp-stage');
897			if (!$scope.length) {
898				$scope = this.holder.closest('[data-locationid]');
899			}
900			var $rooms = $scope.find('.amnp-room-holder');
901			if (!$rooms.filter('[tabindex="0"]').length) {
902				this._setRoomTabTarget($rooms.filter(':visible').first());
903			} else {
904				this.holder.attr('tabindex', '-1');
905			}
906		},
907		_focusAdjacentRoom: function (direction) {
908			var $container = this.holder.closest('.amnp-stage');
909			if (!$container.length) {
910				$container = this.holder.closest('[data-locationid]');
911			}
912			var $rooms = $container.find('.amnp-room-holder:visible');
913			var count = $rooms.length;
914			if (!count) return;
915			var currentIndex = $rooms.index(this.holder);
916			if (currentIndex < 0) return;
917			var targetIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1;
918			if (targetIndex >= count) targetIndex = 0;
919			if (targetIndex < 0) targetIndex = count - 1;
920			var $target = $rooms.eq(targetIndex);
921			if ($target.length) {
922				this._setRoomTabTarget($target);
923				$target.focus();
924			}
925		},
926		_setRoomTabTarget: function ($room) {
927			if (!$room || !$room.length) return;
928			var $scope = $room.closest('.amnp-stage');
929			if (!$scope.length) {
930				$scope = $room.closest('[data-locationid]');
931			}
932			var $rooms = $scope.find('.amnp-room-holder');
933			$rooms.attr('tabindex', '-1');
934			$room.attr('tabindex', '0');
935		},
936		_setSegmentFocusTarget: function ($segment) {
937			var segments = this.day.find('.amnp-segment');
938			segments.attr('tabindex', '-1');
939			if ($segment && $segment.length) {
940				$segment.attr('tabindex', '0');
941			}
942		},
943		_ensureSegmentFocusTarget: function () {
944			if (!this.holder.hasClass('amnp-room-active')) {
945				this.day.find('.amnp-segment').attr('tabindex', '-1');
946				return;
947			}
948
949			var current = this.day.find('.amnp-segment[tabindex="0"]');
950			if (current.length && current.attr('aria-disabled') !== 'true' && current.is(':visible')) return;
951
952			var fallback = this.day.find('.amnp-segment-open:not(.amnp-segment-booked):visible').first();
953			if (!fallback.length) {
954				fallback = this.day.find('.amnp-segment:visible').first();
955			}
956
957			this._setSegmentFocusTarget(fallback);
958		},
959		focusHolder: function () {
960			this._setRoomTabTarget(this.holder);
961			this.holder.focus();
962		},
963		_focusSegment: function ($segment) {
964			if (!$segment || !$segment.length) return;
965
966			this._setSegmentFocusTarget($segment);
967			$segment.focus();
968
969			if (this.barsAdded && typeof this.dayHolder.mCustomScrollbar === 'function') {
970                this._scrollToSegment($segment);
971			} else {
972                this.dayHolder.scrollLeft($segment.position().left);
973			}
974		},
975		_updateSegmentAria: function ($segment) {
976			if (!$segment || !$segment.length) return;
977			var timeLabel = $segment.attr('data-time');
978			var status = '';
979			if ($segment.hasClass('amnp-segment-selected')) {
980				status = 'selected';
981			} else if ($segment.hasClass('amnp-segment-basket')) {
982				status = 'in basket';
983			} else if ($segment.hasClass('amnp-segment-setup-breakdown')) {
984				status = 'setup or breakdown';
985			} else if ($segment.hasClass('amnp-segment-booked') || $segment.attr('aria-disabled') === 'true') {
986				status = 'unavailable';
987			} else if ($segment.hasClass('amnp-segment-open')) {
988				status = 'available';
989			} else {
990				status = 'not available';
991			}
992			var labelParts = [];
993			if (timeLabel) {
994				labelParts.push('Time ' + timeLabel);
995			}
996			labelParts.push(status);
997			if ($segment.attr('aria-disabled') !== 'true') {
998				if (status === 'selected') {
999					labelParts.push('press enter to adjust selection');
1000				} else {
1001					labelParts.push('press enter to select');
1002				}
1003			}
1004			$segment.attr('aria-label', labelParts.join(', '));
1005		},
1006		_refreshSegmentAria: function () {
1007			var self = this;
1008			this.day.find('.amnp-segment').each(function () {
1009				self._updateSegmentAria($(this));
1010			});
1011		},
1012		_focusFirstAvailableSegment: function () {
1013			var focusTarget = this.day.find('.amnp-segment-open:not(.amnp-segment-booked):visible').first();
1014
1015			if (!focusTarget.length) {
1016				focusTarget = this.day.find('.amnp-segment:visible').first();
1017			}
1018
1019			this._focusSegment(focusTarget);
1020		},
1021        _focusSelectedSegments: function ($clickedSegment, selectionChange) {
1022            if (!$clickedSegment || !$clickedSegment.length || !selectionChange) return;
1023
1024			this._setSegmentFocusTarget($clickedSegment);
1025			$clickedSegment.focus();
1026
1027            if (selectionChange.startChanged && selectionChange.endChanged) {
1028                var $scrollToSegment = this.day.children().eq(Math.max(0, $clickedSegment.index() - 2));
1029                this._scrollToSegment($scrollToSegment);
1030                return;
1031            }
1032            if (!selectionChange.startChanged && !selectionChange.endChanged) {
1033                return;
1034            }
1035
1036            var highlightedSegments = this.day.find('.amnp-segment.amnp-segment-selected, .amnp-segment.amnp-segment-setup-breakdown');
1037            var $firstHighlightedSegment = highlightedSegments.first();
1038            var $lastHighlightedSegment = highlightedSegments.last(
1038);
1039
1040            var scrollContainer = this.dayHolder.find(".mCSB_container");
1041            var scrollX = Math.abs(
1042                parseInt(scrollContainer.css("left"), 10) || 0
1043            );
1044            var visibleLeft = scrollX;
1045            var visibleRight = scrollX + this.dayHolder.width();
1046
1047            var $firstVisibleSegment = null;
1048            var $lastVisibleSegment = null;
1049            var segmentWidth = this.settings.segmentWidth;
1050            this.day.find('.amnp-segment:visible').each(function() {
1051                var $seg = $(this);
1052                var segLeft = $seg.position().left;
1053                var segRight = segLeft + segmentWidth;
1054
1055                if (segRight > visibleLeft && !$firstVisibleSegment) {
1056                    $firstVisibleSegment = $seg;
1057                }
1058                if (segLeft + 0.5 * segmentWidth < visibleRight) {
1059                    $lastVisibleSegment = $seg;
1060                } else {
1061                    return false;
1062                }
1063            });
1064
1065            var numVisible = $lastVisibleSegment?.index() - $firstVisibleSegment?.index() + 1;
1066            var numHighlighted = $lastHighlightedSegment.index() - $firstHighlightedSegment.index() + 1;
1067
1068			if (this.barsAdded && typeof this.dayHolder.mCustomScrollbar === 'function') {
1069                if (selectionChange?.didSelect) {
1070                    if (selectionChange.startChanged) {
1071                        if ($firstVisibleSegment.index() >= $firstHighlightedSegment.index()) {
1072                            var scrollLeftIndex = Math.max(0, $firstHighlightedSegment.index() - 1);
1073                            var $scrollLeftSegment = this.day.children().eq(scrollLeftIndex);
1074                            this._scrollToSegment($scrollLeftSegment);
1075                        }
1076                    } else { // selectionChange.endChanged
1077                        if ($lastVisibleSegment.index() <= $lastHighlightedSegment.index()) {
1078                            var numSteps = $lastHighlightedSegment.index() - $lastVisibleSegment.index() + 1;
1079                            var scrollLeftIndex = $firstVisibleSegment.index() + numSteps;
1080                            var $scrollLeftSegment = this.day.children().eq(scrollLeftIndex);
1081                            this._scrollToSegment($scrollLeftSegment);
1082                        }
1083                    }
1084                } else if (!selectionChange?.didSelect) {
1085                    if (selectionChange.startChanged) {
1086                        if ($lastVisibleSegment.index() <= $lastHighlightedSegment.index()) {
1087                            var scrollLeftIndex = Math.max(0, Math.min($firstHighlightedSegment.index() - 1, $lastHighlightedSegment.index() + 1 - numHighlighted));
1088                            var $scrollToSegment = this.day.children().eq(scrollLeftIndex);
1089                            this._scrollToSegment($scrollToSegment);
1090                        }
1091                    } else { // selectionChange.endChanged
1092                        if ($firstHighlightedSegment.index() <= $firstVisibleSegment.index()) {
1093                            var scrollLeftIndex = Math.max(0, $firstHighlightedSegment.index() - 1,  $lastHighlightedSegment.index() - numVisible + 2);
1094                            var $scrollToSegment = this.day.children().eq(scrollLeftIndex);
1095                            this._scrollToSegment($scrollToSegment);
1096                        }
1097                    }
1098                }
1099			} else {
1100				this.dayHolder.scrollLeft($clickedSegment.position().left);
1101			}
1102        },
1103        _render: function () {
1104            this.day.find('.amnp-segment').removeClass('amnp-segment-selected amnp-segment-setup-breakdown');
1105            this.chargeDetails.empty();
1106			if (this.selected.start && this.selected.end) {
1107				this.selectedBookingStart = this.selected.start.clone();
1108				this.selectedBookingEnd = this.selected.end.clone();
1109
1110				if (this.settings.useSetupBreakdown) {
1111					this.selectedBookingStart = this.selectedBookingStart.add((this.selectedLayout ? this.selectedLayout.setup_time : this.settings.room.setup_time) * -1, 'minutes');
1112					this.selectedBookingEnd = this.selectedBookingEnd.add(this.selectedLayout ? this.selectedLayout.breakdown_time : this.settings.room.breakdown_time, 'minutes');
1113                    if (this.settings.adjustSetupBreakdown) {
1114                        if (this.selectedBookingEnd > this.libraryCloseTime) {
1115                            this.selectedBookingEnd = this.libraryCloseTime.clone();
1116                        }
1117                        if (this.selectedBookingStart < this.libraryOpenTime) {
1118                            this.selectedBookingStart = this.libraryOpenTime.clone();
1119                        }
1120                    }
1121                }
1122				this._updateAssets();
1123
1124                if (this.settings.chargingEnabled) {
1125                    var charge = this._calculateRoomCharge();
1126                    if (charge > 0) {
1127                        var non_refundable = typeof (this.settings.room.restrictions.non_refundable_amount) !== 'undefined' ? this._formatCurrency(parseFloat(this.settings.room.restrictions.non_refundable_amount)) : 0.00;
1128                        var $chargeMessage = $('<div />').addClass('amnp-charge-message');
1129                        var $chargeIcon = $('<div />').addClass('amnp-charge-icon').append($('<span class="amnp-currency link fa-stack fa-lg"><i class="fa fa-circle fa-stack-2x"></i><i class="fa fa-usd fa-stack-1x fa-inverse"></i></span>'));
1130                        var $chargeText = $('<div />').addClass('amnp-charge-text').html('Room charge <b>' + this.settings.payment_currency_symbol + '' + this._formatCurrency(charge) + '</b>');
1131
1132                        $chargeMessage.append($chargeIcon).append($chargeText);
1133                        if (non_refundable > 0) {
1134                            $chargeIcon.css('padding-left', '10%');
1135                            $chargeMessage.css('line-height', '28px');
1136                        } else {
1137                            $chargeIcon.css('padding-left', '10%');
1138                            $chargeMessage.css('line-height', '28px');
1139                        }
1140
1141                        $chargeMessage.appendTo(this.chargeDetails);
1142                    }
1143                }
1144
1145                var self = this;
1146                this.day.find('.amnp-segment').each(function (idx, el) {
1147                    var segmentStartTime = $(el).data('segmentTime');
1148                    var segmentTimeEnd = moment(segmentStartTime).add(self.settings.segmentSize, 'minutes');
1149
1150                    if (segmentStartTime >= self.selected.start && segmentStartTime < self.selected.end) {
1151                        $(el).addClass('amnp-segment-selected');
1152                    } else if(self.settings.useSetupBreakdown) {
1153                        var startInSegment = (segmentStartTime.isSameOrBefore(self.selectedBookingStart) && segmentTimeEnd.isAfter(self.selectedBookingStart));
1154                        var endInSegment = (segmentStartTime.isBefore(self.selectedBookingEnd) && segmentTimeEnd.isSameOrAfter(self.selectedBookingEnd));
1155                        if(startInSegment || endInSegment || (segmentStartTime >= self.selectedBookingStart && segmentStartTime < self.selectedBookingEnd))  {
1156                            $(el).addClass('amnp-segment-setup-breakdown');
1157                        }
1158                    }
1159                });
1160            }
1161            this.bookingDetails.empty();
1162            if (this.selected.start && this.selected.end) {
1163                this.bookingDetails.text(this.selected.start.format('h:mma') + ' - ' + this.selected.end.clone().format('h:mma, MMMM Do YYYY'));
1164            }
1165			this._updateTotal();
1166			var self = this;
1167			this.day.find('.amnp-segment').each(function () {
1168				var $segment = $(this);
1169				var isSelected = $segment.hasClass('amnp-segment-selected');
1170				var isDisabled = !$segment.hasClass('amnp-segment-open') || $segment.hasClass('amnp-segment-booked');
1171
1172				$segment.attr('aria-pressed', isSelected ? 'true' : 'false');
1173				$segment.attr('aria-disabled', isDisabled ? 'true' : 'false');
1174				self._updateSegmentAria($segment);
1175			});
1176			this._ensureSegmentFocusTarget();
1177		},
1178        _formatMinsAsString: function (mins) {
1179            var min = parseInt(mins, 10);
1180            if (min < 60) {
1181                return mins + " minutes";
1182            }
1183            var hours = Math.floor(min / 60);
1184            var minutes = min % 60;
1185            if (minutes > 0) {
1186                if (hours > 1) {
1187                    return hours + ' hours ' + minutes + ' minutes';
1188                } else {
1189                    return hours + ' hour ' + minutes + ' minutes';
1190                }
1191            } else {
1192                if (hours > 1) {
1193                    return hours + ' hours';
1194                } else {
1195                    return hours + ' hour';
1196                }
1197            }
1198        },
1199        _updateButton: function () {
1200            this.message.empty();
1201            this.holder.removeClass('amnp-has-error anmp-layout-required');
1202            var roomData = this.settings.room;
1203            var locationData = this.settings.location;
1204            this.checkoutNowButton.removeClass('btn-info btn-edit btn-primary').addClass('btn-info').removeProp('disabled').hide();
1205            //debugger;
1206            if (this.selected.start && this.selected.end) {
1207                try {
1208                    var roomData = this.settings.room;
1209                    var locationData = this.settings.location;
1210                    var selectedBlockStart = this.selected.start.clone();
1211                    var selectedBlockEnd = this.selected.end.clone();
1212                    var bookingStart = selectedBlockStart.clone();
1213                    var bookingEnd = selectedBlockEnd.clone();
1214                    var rawDuration = moment.duration(bookingEnd.diff(bookingStart)).asMinutes();
1215                    var roomOpens = this.roomOpenTime.clone();
1216                    var roomCloses = this.roomCloseTime.clone();
1217                    var self = this;
1218                    var min = parseInt(roomData.restrictions.minimum_booking_time, 10) || 0;
1219                    var max = parseInt(roomData.restrictions.maximum_booking_time, 10) || 0;
1220                    var near = parseInt(roomData.restrictions.nearest_booking_days, 10) || 0;
1221                    var far = parseInt(roomData.restrictions.furthest_booking_days, 10) || 0;
1222                    var breakdown = this.selectedLayout ? this.selectedLayout.breakdown_time : roomData.breakdown_time;
1223                    var setup = (this.selectedLayout ? this.selectedLayout.setup_time : roomData.setup_time) * -1;
1224                    var nowLibraryTz;
1225                    var allowActiveBlockReservation;
1226                    var currentBlockStart;
1227                    var bookingStillActive;
1228
1229                    if (self.settings.useSetupBreakdown) {
1230                        roomOpens.add(setup, 'minutes');
1231                        roomCloses.add(breakdown, 'minutes');
1232
1233                        bookingEnd.add(breakdown, 'minutes');
1234                        bookingStart.add(setup, 'minutes');
1235
1236                        if (self.settings.adjustSetupBreakdown) {
1237                            if (bookingStart < roomOpens) {
1238                                bookingStart = roomOpens.clone();
1239                            }
1240
1241                            if (bookingEnd > roomCloses) {
1242                                bookingEnd = roomCloses.clone();
1243                            }
1244
1245                            if (bookingStart < self.libraryOpenTime) {
1246                                bookingStart = self.libraryOpenTime.clone();
1247                            }
1248
1249                            if (bookingEnd > self.libraryCloseTime) {
1250                                bookingEnd = self.libraryCloseTime.clone();
1251                            }
1252                        }
1253                    }
1254
1255                    this.settings.room.bookings.forEach(function (booking) {
1256                        var lStart = moment.tz(booking.start_time, self.settings.timezone);
1257                        var lEnd   = moment.tz(booking.end_time,   self.settings.timezone);
1258                        if (self.settings.useSetupBreakdown && booking.event_id === null) {
1259                            // Fix for wrong setup/breakdown times being used
1260                            // #4575 - Cknapp
1261                            var useLayout = false;
1262                            if (parseInt(booking.layout_id) > 0 && self.settings.room.layouts.length > 0) {
1263                                self.settings.room.layouts.forEach(function (layout) {
1264                                    if (parseInt(layout.id) === parseInt(booking.layout_id)) {
1265                                        lEnd.add(layout.breakdown_time, 'minutes');
1266                                        lStart.add(layout.setup_time * -1, 'minutes');
1267                                        useLayout = true;
1268                                    }
1269                                });
1270                            }
1271
1272                            if (!useLayout) {
1273                                lEnd.add(booking.breakdown_time, 'minutes');
1274                                lStart.add(booking.setup_time * -1, 'minutes');
1275                            }
1276
1277                            if (self.settings.adjustSetupBreakdown) {
1278                                if (lStart < roomOpens) {
1279                                    lStart = roomOpens.clone();
1280                                }
1281
1282                                if (lEnd > roomCloses) {
1283                                    lEnd = roomCloses.clone();
1284                                }
1285
1286                                if (lStart < self.libraryOpenTime) {
1287                                    lStart = self.libraryOpenTime.clone();
1288                                }
1289
1290                                if (lEnd > self.libraryCloseTime) {
1291                                    lEnd = self.libraryCloseTime.clone();
1292                                }
1293                            }
1294                        }
1295
1296                        // #4033 - CK
1297                        // Moved this and the var declarations to the top, because booking clash is more important than layout.
1298                        if (lStart.isBefore(bookingEnd) && lEnd.isAfter(bookingStart)) {
1299                            self.bookingButton.removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1300                            throw ('There is a clash with an existing booking.');
1301                        }
1302                    });
1303
1304
1305
1306                    if (this.settings.enabledCart && this.storage && JSON.parse(this.storage.getItem('communicoReserveCart') || '[]').length === 0) {
1307                        this.checkoutNowButton.show();
1308                    }
1309                    this.bookingButton.removeClass('btn-info btn-edit btn-primary').addClass('btn-info').removeProp('disabled').show();
1310
1311                    if (this.settings.mode === 0 && !roomData.restrictions.bookable) {
1312                        this.checkoutNowButton.hide();
1313                        this.bookingButton.text('Please call').removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1314                        throw (this.settings.roomsNotBookableMessage.replace('{{branch_number}}', locationData.tel));
1315                    }
1316
1317                    if (this.settings.layoutRequired && !this.selectedLayout && this.settings.room.layouts && this.settings.room.layouts.length > 0) {
1318                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1319                        this.holder.addClass('anmp-layout-required');
1320                        throw ('A layout is required for this room.');
1321                    }
1322
1323                    if (bookingStart < roomOpens || bookingEnd > roomCloses) {
1324                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1325                        throw ('The room is closed at this time.');
1326                    }
1327
1328                    if (bookingStart < this.libraryOpenTime || bookingEnd > this.libraryCloseTime) {
1329                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1330                        throw ('The library is closed at this time.');
1331                    }
1332                    if (max > 0 && rawDuration > max) {
1333                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1334                        throw (this.settings.roomsMaxBookingMessage.replace('{{branch_number}}', locationData.tel).replace('{{amount}}', this._formatMinsAsString(max)));
1335                    }
1336                    if (min > 0 && rawDuration < min) {
1337                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1338                        throw (this.settings.roomsMinBookingMessage.replace('{{branch_number}}', locationData.tel).replace('{{amount}}', this._formatMinsAsString(min)));
1339                    }
1340                    var when = bookingStart.clone().startOf('day')
1341                        .diff(moment().tz(self.settings.timezone).startOf('day'), 'days');
1342
1343                    if (near > 0 && when < near) {
1344                        if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1345                        throw (this.settings.roomsNearBookingMessage.replace('{{branch_number}}', locationData.tel).replace('{{amount}}', near));
1346                    }
1347                    var args = {
1348                        libraryTimezone: this.settings.timezone,
1349                        nearestDays: near,
1350                        furthestDays: far,
1351                        calendarDayRestrictions: this.clientSettings.rooms_calendar_day_restrictions,
1352                        useBookingEndForFurthestRestrictions: this.clientSettings.rooms_use_end_time_for_furthest,
1353                        // moment objects in device timezone
1354                        bookingStartDeviceTz: bookingStart,
1355                        bookingEndDeviceTz: bookingEnd,
1356                        // input argument allows unit testing across timezones
1357                        nowLibraryTz: moment().tz(this.settings.timezone)
1358                    }
1359                    var limitCheck = this._checkNearestFurthestLimits(args);
1360                    if (!limitCheck || !limitCheck.success) {
1361                        switch (limitCheck.type) {
1362                            case "NEAREST":
1363                                if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1364                                throw (this.settings.roomsNearBookingMessage.replace('{{branch_number}}', locationData.tel).replace('{{amount}}', near));
1365                            case "FURTHEST":
1366                                if (this.settings.mode === 0) $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1367                                throw (far != 0 ? this.settings.roomsFarBookingMessage.replace('{{branch_number}}', locationData.tel).replace('{{amount}}', far) : "This room may only be booked for the current day");
1368                        }
1369                    }
1370                    nowLibraryTz = moment.tz(self.settings.timezone);
1371                    if (selectedBlockStart.isBefore(nowLibraryTz)) {
1372                        allowActiveBlockReservation = (self.clientSettings || {}).rooms_allow_active_block_reservation === true;
1373                        currentBlockStart = self._activeBlockStart({ now: nowLibraryTz, segmentSize: self.settings.segmentSize });
1374                        bookingStillActive = selectedBlockEnd.isAfter(nowLibraryTz);
1375                        if (!(allowActiveBlockReservation && currentBlockStart && selectedBlockStart.isSame(currentBlockStart) && bookingStillActive)) {
1376                            if (this.settings.mode === 0) {
1377                                $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1378                            }
1379                            throw ('The start of your booking is in the past.');
1380                        }
1381                    }
1382                    if (rawDuration % this.settings.blockSize !== 0) {
1383                        $('.btn', this.footer).removeClass('btn-info btn-edit btn-primary').addClass('btn-edit').prop('disabled', true);
1384                        throw (' Bookings must be in multiples of ' + this._formatMinsAsString(this.settings.blockSize) + '.');
1385                    }
1386                } catch (message) {
1387                    this.message.html('<i class="fa fa-exclamation-circle"/> ' + message);
1388                    this.holder.addClass('amnp-has-error');
1389                }
1390            }
1391        },
1392        _update: function () {
1393            this._updateButton();
1394            this._render();
1395        },
1396        // Pure unit-testable function for the active-block back-fill boundary.
1397        // The earliest start a patron may back-fill to is the start of the segment `now` sits in:
1398        // segments are the grid this picker draws and the boundary an early sign-out releases a room
1399        // on. settings.blockSize (segment_size * block_count) is the duration multiple and the charge
1400        // unit — flooring to it lands on a boundary the room is never freed on, so with segment_size
1401        // 30 / block_count 2 a room released at 10:30 offered only 10:00, which the booking that was
1402        // just cut short still holds.
1403        _activeBlockStart: function (args) {
1404            var segmentSize = parseInt(args.segmentSize, 10);
1405            if (!args.now || !segmentSize || segmentSize <= 0) {
1406                return null;
1407            }
1408            var minutesSinceMidnight = args.now.hours() * 60 + args.now.minutes();
1409            var blockStartMinutes = Math.floor(minutesSinceMidnight / segmentSize) * segmentSize;
1410            return args.now
1411                .clone()
1412                .startOf('day')
1413                .add(blockStartMinutes, 'minutes')
1414                .second(0)
1415                .millisecond(0);
1416        },
1417        // Pure unit-testable function for nearest/furthest booking limits
1418        _checkNearestFurthestLimits: function (args) {
1419            if (!args.bookingStartDeviceTz || !args.bookingEndDeviceTz || !args.nowLibraryTz || !args.libraryTimezone) {
1420                throw "Error while checking nearest/furthest booking limits";
1421            }
1422            var bookingStartLibraryTz = moment.tz(args.bookingStartDeviceTz.format('YYYY-MM-DD HH:mm'), args.libraryTimezone);
1423            var bookingEndLibraryTz = moment.tz(args.bookingEndDeviceTz.format('YYYY-MM-DD HH:mm'), args.libraryTimezone);
1424            var nowLibraryTz = args.nowLibraryTz;
1425            var useEndForFurthest = args.useBookingEndForFurthestRestrictions;
1426
1427            if (args.nearestDays > 0) {
1428                var nearestAllowed = nowLibraryTz.clone().add(args.nearestDays, 'days');
1429                if (args.calendarDayRestrictions) {
1430                    nearestAllowed = nearestAllowed.startOf('day');
1431                }
1432                if (nearestAllowed.isAfter(bookingStartLibraryTz)) {
1433                    return {
1434                        success: false,
1435                        type: "NEAREST"
1436                    }
1437                }
1438            }
1439            if (args.furthestDays > -1) {
1440                var furthestAllowed = nowLibraryTz.clone().add(args.furthestDays, 'days');
1441                if (args.calendarDayRestrictions) {
1442                    furthestAllowed = nowLibraryTz.clone().add(args.furthestDays, 'days').endOf('day');
1443                }
1444                if (furthestAllowed.isBefore(useEndForFurthest ? bookingEndLibraryTz : bookingStartLibraryTz)) {
1445                    return {
1446                        success: false,
1447                        type: "FURTHEST"
1448                    }
1449                }
1450            }
1451            return {
1452                success: true
1453            }
1454        }
1455    });
1456    $.fn[pluginName] = function (options) {
1457        return this.each(function () {
1458            if (!$.data(this, "plugin_" + pluginName)) {
1459                $.data(this, "plugin_" + pluginName, new Plugin(this, options));
1460            }
1461        });
1462    };
1463
1464    // Expose a Node/CommonJS export for unit testing without affecting browser usage.
1465    if (typeof module !== 'undefined' && module.exports) {
1466        module.exports = module.exports || {};
1467        module.exports._checkNearestFurthestLimits = Plugin.prototype._checkNearestFurthestLimits;
1468        module.exports.checkNearestFurthestLimits = function (args) {
1469            return Plugin.prototype._checkNearestFurthestLimits(args);
1470        };
1471        module.exports._activeBlockStart = Plugin.prototype._activeBlockStart;
1472        module.exports.activeBlockStart = function (args) {
1473            return Plugin.prototype._activeBlockStart(args);
1474        };
1475    }
1476})(jQuery, window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.