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"> ' + 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> ' + 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: <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: <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: <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: <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.