PageSourceSearch

https://mdpls.org/js/am-reserve-picker.v3.js?v=1480

js mdpls.org collected 2026-09-24 08:49:46 UTC 63,417 bytes, 1,569 lines download raw bytes

1;
2(function ($, window, document, undefined) {
3	"use strict";
4	var pluginName = "amReservePicker",
5		defaults = {
6			apiServer: false,
7			client: false,
8			timezone: '',
9			mode: 0,
10			roomId: false,
11			unmediatedOnly: false,
12			mediatedOnly: false,
13			selectedDate: false,
14			selectedLocation: false,
15			selectedRoom: false,
16			classId: 0,
17			sessionId: '',
18			chargingMode: 0,
19			testMode: 0,
20			payment_currency: 'USD',
21			ezproxy: {},
22			embeddedMode: false,
23			parentOrigin: '',
24			enableStaffNotes: false
25		};
26	// The actual plugin constructor
27	function Plugin(element, options) {
28		this.clientSettings = {};
29		this.locations = [];
30		this.selectedLocations = [];
31		this.locationsById = {};
32		this.element = element;
33		this.$element = $(element);
34		this.settings = $.extend(true, {}, defaults, options);
35		this._defaults = defaults;
36		this._name = pluginName;
37		this.initComplete = false;
38		this.paused = false;
39		this.locationOptions = {};
40		this._updating = false;
41		this.groupTypeSelect = null;
42		this.responsiveGroupTypeSelect = null;
43		this.locationOptions = {
44			only_with_external_rooms: 1
45		};
46		this.storage = window.sessionStorage || false;
47		this._cartOpen = false;
48		this.cartHolder = null;
49		this.cartButton = null;
50		this.cartPanel = null;
51		this.cartList = null;
52		this.cartFooter = null;
53		this.cartTotal = null;
54		this.cartCheckout = null;
55		this.cartCount = null;
56		this.pendingFocus = null;
57		this.liveRegion = null;
58		this._embeddedHeightFrame = null;
59		this._lastEmbeddedHeight = 0;
60		this._embeddedHeightObserver = null;
61		this._embeddedResizeObserver = null;
62		this.init();
63	}
64
65	$.extend(true, Plugin.prototype, $.fn['amShared']);
66	$.extend(Plugin.prototype, {
67		init: function () {
68			var self = this;
69			Promise.all([
70				self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/settings')
71			]).then(function (result) {
72				self.clientSettings = result[0] || {};
73				var activeBlockReservationSetting;
74				if (typeof self.clientSettings.rooms_allow_active_block_reservation !== "undefined") {
75					activeBlockReservationSetting = self.clientSettings.rooms_allow_active_block_reservation;
76				} else if (
77					self.settings.clientSettings &&
78					typeof self.settings.clientSettings.rooms_allow_active_block_reservation !== "undefined"
79				) {
80					activeBlockReservationSetting =
81						self.settings.clientSettings.rooms_allow_active_block_reservation;
82				}
83				if (typeof activeBlockReservationSetting !== "undefined") {
84					self.clientSettings.rooms_allow_active_block_reservation =
85						activeBlockReservationSetting === true;
86				}
87				var unmediatedParam = '';
88				if (self.settings.unmediatedOnly) {
89					self.locationOptions.only_with_unmediated = 1;
90					unmediatedParam = '&only_unmediated=1';
91				}
92
93				if (self.settings.mode !== 1) {
94					self.locationOptions.only_with_patron_rooms = 0;
95				}
96
97				self.locationOptions.mode = self.settings.mode;
98				var selectedClass = self.clientSettings.rooms_group_billing_remember ? self._readCookie('selectedClass' + self.settings.client) : false;
99				// CH 14359 - Check if the classId is already set. If it is then it is being passed in directly
100				// Also check to make sure that selected class isn't being set by the cookie.
101				if (selectedClass) {
102					self.locationOptions.class_id = selectedClass;
103				}
104
105				Promise.all([
106					self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/locations', self.locationOptions),
107					self._getJSON(self.settings.apiServer + '/v2/' + self.settings.client + '/userclasses?mode=' + self.settings.mode + '&locations=1' + unmediatedParam)
108				]).then(function (result) {
109
110					self.locations = result[0];
111					self.userclasses = result[1];
112
113					self.locationsById = {};
114					self.locations.forEach(function (location) {
115						self.locationsById[location.id] = location;
116					});
117
118					self.bookings = self.storage && self.clientSettings.rooms_cart_enabled ? JSON.parse(self.storage.getItem('communicoReserveCart') || '[]') : [];
119					self._buildInterface();
120					self._setupEmbeddedBridge();
121					self._bindEvents();
122					self._updateLocations();
123					self.initComplete = true;
124
125					// Should work
126					if (self.settings.classId > 0) {
127						if (typeof self.groupTypeSelect !== 'undefined') {
128							if (self.groupTypeSelect.is(":visible")) {
129								self.groupTypeSelect.val(self.settings.classId);
130							}
131						}
132					}
133
134					self._update();
135				}).catch(function (err) {
136					console.log(err);
137				});
138			}).catch(function (err) {
139				console.log(err);
140			});
141		},
142		_buildInterface: function () {
143			var self = this;
144			var now = moment(); //TODO: needs to use timezone
145
146			var selectedBranches = this._readCookie('reserveBranches');
147
148			if (this.settings.selectedLocation) {
149				selectedBranches = [this.settings.selectedLocation];
150			}
151			this.holder = $('<div class="amnp-holder amnp-holder-v3"></div>').appendTo(this.element);
152			if (this.clientSettings.rooms_cart_enabled) {
153				this.cartHolder = $('<div class="amnp-picker-cart-holder"></div>').appendTo(this.holder);
154				var cartPanelId = 'amnp-picker-cart-' + Math.random().toString(36).slice(2);
155				this.cartButton = $('<button type="button" class="amnp-picker-cart-button"><i class="fa fa-shopping-cart link"></i> Room basket (<span class="basket-total">0</span>)</button>')
156					.attr({
157						'aria-haspopup': 'true',
158						'aria-expanded': 'false',
159						'aria-controls': cartPanelId
160					})
161					.appendTo(this.cartHolder);
162				this.cartCount = this.cartButton.find('.basket-total');
163				this.cartPanel = $('<div class="amnp-picker-cart" role="region" aria-label="Room basket" aria-hidden="true"></div>')
164					.attr('id', cartPanelId)
165					.hide()
166					.appendTo(this.cartHolder);
167				this.cartList = $('<div class="amnp-picker-cart-list"></div>').appendTo(this.cartPanel);
168				this.cartFooter = $('<div class="amnp-picker-cart-footer"></div>').appendTo(this.cartPanel);
169				this.cartTotal = $('<div class="amnp-picker-cart-total"></div>').appendTo(this.cartFooter);
170				this.cartCheckout = $('<button type="button" class="amnp-picker-cart-checkout btn btn-info"><i class="fa fa-shopping-cart"></i> Reserve</button>').attr('aria-label', 'Reserve rooms in basket').appendTo(this.cartFooter);
171			}
172			if (this.clientSettings.events_single_branch_library) {
173				this.holder.addClass('amnp-single-branch');
174			}
175
176			this.header = $('<div class="amnp-picker-header"><h1>Reserve a room</h1><p>Choose a date and room then pick a time</p></div>').appendTo(this.holder);
177			this.navigation = $('<div class="amnp-navigation link"></div>').appendTo(this.holder);
178			this.navigationPrev = $('<button type="button" class="amnp-navigation-control amnp-navigation-prev" data-by="-1" aria-label="Previous day"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>').appendTo(this.navigation);
179			this.navigationTitle = $('<span class="amnp-navigation-title" aria-live="polite"></span>').appendTo(this.navigation);
180			this.navigationNext = $('<button type="button" class="amnp-navigation-control amnp-navigation-next" data-by="1" aria-label="Next day"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>').appendTo(this.navigation);
181
182			this.responsiveKey = $('<div class="amnp-responsive-key"></div>').appendTo(this.holder);
183			this.responsiveDatePicker = $('<div class="input-group date"><input type="text" class="form-control" aria-label="Date picker for mobile"><div class="input-group-addon"><span class="am-events"></span></div></div>').appendTo(this.responsiveKey);
184			$('<br>').appendTo(this.responsiveKey);
185			var branchPicker = $('<div class="amnp-choose-branch" />').appendTo(this.responsiveKey);
186			$('<br><h5>Select a branch:</h5>').appendTo(branchPicker);
187			this.responsiveLocationSelect = $('<select aria-label="Mobile Location Select" />').prop('multiple', true).appendTo(branchPicker);
188			this.responsiveLocationSelect.multiselect({
189				includeSelectAllOption: true,
190				selectAllValue: 'all',
191				buttonWidth: '100%',
192				onChange: function (option, checked, select) {
193					if (!self._updating) {
194						if (self.locations) {
195							if (!checked) {
196								//self.locations.splice(self.locations.indexOf($(option).val()), 1);
197							} else {
198								//self.locations.push($(option).val());)
199							}
200						}
201						self._update();
202					}
203				},
204				onSelectAll: function (e) {
205					if (!self._updating) self._update();
206					//self.responsiveLocationSelect.children('options[value!="all"]').attr('checked', true);
207				},
208				onDeselectAll: function (e) {
209					if (!self._updating) self._update();
210					//self.responsiveLocationSelect.children('options[value!="all"]').prop('checked', false);
211				}
212			});
213
214			this.key = $('<div class="amnp-key"></div>').appendTo(this.holder);
215			if (!this.settings.embeddedMode) {
216				if (this.settings.mode === 1) {
217					this.navigation.sticky({ topSpacing: 80, wrapperClassName: 'sticky-nav' });
218					this.key.sticky({ topSpacing: 140, bottomSpacing: 50, wrapperClassName: 'sticky-key' });
219				} else {
220					this.navigation.sticky({ wrapperClassName: 'sticky-nav' });
221					this.key.sticky({ topSpacing: 60, bottomSpacing: ($('#footer').outerHeight() || 300) + 50, wrapperClassName: 'sticky-key' });
222				}
223			}
224
225			this.stage = $('<div class="amnp-stage"></div>').appendTo(this.holder);
226			this.loader = $('<div class="amnp-loader"><div class="amnp-spinner"></div></div>').appendTo(this.holder);
227			this.liveRegion = $('<div class="amnp-a11y-live" role="status" aria-live="polite"></div>').css({
228				position: 'absolute',
229				width: '1px',
230				height: '1px',
231				margin: '-1px',
232				border: 0,
233				padding: 0,
234				clip: 'rect(0 0 0 0)',
235				overflow: 'hidden'
236			}).appendTo(this.holder);
237
238			if (this.clientSettings.rooms_group_billing_enabled) {
239				this.groupPickerHolder = $('<div class="amnp-group-picker"><span>Booking type</span><select class="form-control" aria-label="Booking type selector"></select></div>').appendTo(this.key);
240				this.groupTypeSelect = $('select', this.groupPickerHolder);
241
242				var patronClass = false;
243				var staffClass = false;
244				this.userclasses.forEach(function (cls) {
245					var t = parseInt(cls.type, 10);
246					if (self.settings.mode !== 1 && t == 0) return;
247					$('<option value="' + cls.id + '">' + cls.name + '</option>').data('details', cls).appendTo(self.groupTypeSelect);
248					if (t === 1) {
249						patronClass = cls.id;
250					}
251
252					if (t === 0) {
253						staffClass = cls.id;
254					}
255				});
256
257				$('<br><h5>Booking type:</h5>').appendTo(this.responsiveKey);
258				this.responsiveGroupTypeSelect = this.groupTypeSelect.clone(true).appendTo(this.responsiveKey);
259				var selectedClass = this.clientSettings.rooms_group_billing_remember ? this._readCookie('selectedClass' + this.settings.client) : false;
260
261				// CH 14359 - Check if the classId is already set. If it is then it is being passed in directly
262				// Also check to make sure that selected class isn't being set by the cookie.
263				if (parseInt(this.settings.classId) > 0 && !selectedClass) selectedClass = this.settings.classId;
264
265				if (this.settings.mode !== 1 && !selectedClass && this.clientSettings.rooms_group_billing_prompt_for_class) {
266					this._promptForClass();
267				} else {
268					this.groupTypeSelect.val(parseInt(selectedClass || patronClass));
269					this.responsiveGroupTypeSelect.val(parseInt(selectedClass || patronClass));
270				}
271
272				if (this.settings.mode === 1 && this.clientSettings.rooms_staff_booking_class_default) {
273					var cls = selectedClass > 0 ? selectedClass : staffClass;
274					this.groupTypeSelect.val(parseInt(cls));
275					this.responsiveGroupTypeSelect.val(parseInt(cls));
276				}
277			}
278
279			//add the date picker
280			var datePickerSection = $('<div class="amnp-date-picker"/>').appendTo(this.key);
281			$('<h3>Choose date</h3>').appendTo(datePickerSection);
282			this.datePicker = $('<div/>').val(now.format('MMMM D YYYY')).appendTo(datePickerSection);
283
284			//add the branch picker
285			var branchPickerSection = $('<div class="amnp-choose-branch"/>').appendTo(this.key);
286
287			if (this.settings.roomId) {
288				branchPickerSection.hide();
289			}
290			$('<h3>Choose branch</h3>').appendTo(branchPickerSection);
291			self.locationSelect = $('<div class="amnp-location-select"/>').appendTo(branchPickerSection);
292			self.toggleAll = $('<label><input class="checkall" type="checkbox">Check all</label>').appendTo(self.locationSelect);
293		},
294		_reset: function () {
295
296		},
297		_buildResponsive: function () {
298			var self = this;
299			self.responsiveLocationSelect.empty();
300			self.locations.forEach(function (location) {
301				self.responsiveLocationSelect.append("<option value='" + location.id + "' data-location='" + location.id + "'>" + location.name + "</option>");
302			});
303
304			self.responsiveLocationSelect.multiselect('rebuild');
305		},
306		// Reads the saved branch selection back out of its cookie. Returns an array of
307		// string ids, or null when there is nothing usable to read - no cookie, a value
308		// that is not valid JSON, or a value that is not an array.
309		//
310		// _updateLocations() must not turn null into "check everything" - that is the
311		// bug this guards. The no-matching-branches recovery in _update() deliberately
312		// does fall back to every branch, which is a different situation: there the
313		// saved ids were read fine but none of them exist any more.
314		_readSavedBranches: function () {
315			var raw = this._readCookie('reserveBranches');
316			if (!raw) return null;
317			var saved;
318			try {
319				saved = JSON.parse(raw);
320			} catch (e) {
321				return null;
322			}
323			if (!Array.isArray(saved)) return null;
324			return saved.map(function (id) {
325				return String(id);
326			});
327		},
328		_updateLocations: function () {
329			var self = this;
330			// The live selection is the source of truth: this runs after every room-list
331			// refresh, so rebuilding from the saved cookie instead would re-check every
332			// branch whenever that cookie can't be read back (blocked or cleared by
333			// browser privacy settings, a consent tool, or a third-party iframe context).
334			// The cookie is only consulted on the first pass, before anything is selected,
335			// so a returning visitor still gets their previous branches back.
336			var selectedBranches = self.selectedLocations && self.selectedLocations.length
337				? self.selectedLocations.map(function (id) { return String(id); })
338				: self._readSavedBranches();
339			if (selectedBranches === null && self.settings.selectedLocation) {
340				selectedBranches = [String(self.settings.selectedLocation)];
341			}
342			self._updating = true;
343			self._buildResponsive();
344			self.locationSelect.empty().append(self.toggleAll);
345			var updateLocations = false;
346			for (var l = 0; l < self.locations.length; l++) {
347				var location = self.locations[l];
348				var box = $('<label><input type="checkbox" data-location="' + location.id + '">' + location.name + '</label>').appendTo(self.locationSelect);
349				if (l === self.locations.length - 1) updateLocations = true;
350
351				// Both sides are normalised to strings: the ids come back from the saved
352				// cookie and from the checkbox attributes as strings, but the locations API
353				// can hand back numbers.
354				if (!selectedBranches || selectedBranches.indexOf(String(location.id)) >= 0) {
355					$('input', box).prop('checked', 'checked');
356					self.responsiveLocationSelect.multiselect('select', location.id, updateLocations);
357				} else {
358					self.responsiveLocationSelect.multiselect('deselect', location.id, updateLocations);
359				}
360			}
361
362			var lsCnt = self.locationSelect.find('input[data-location]:checked').length;
363			var rlsCnt = self.responsiveLocationSelect.find('option[data-location]:selected').length;
364
365			if (lsCnt >= self.locations.length) {
366				if (!self.initComplete) {
367					$('input', self.locationSelect).prop('checked', 'checked');
368					self._updating = false;
369					self._update();
370					return;
371				} else {
372					$('input.checkall', self.locationSelect).prop('checked', 'checked').trigger('change');
373				}
374			}
375
376			if (lsCnt === 0) {
377				self._updating = false;
378				$('input', self.locationSelect).prop('checked', 'checked');
379				if (!self.initComplete) {
380					self._update();
381				} else {
382					$('input.checkall', self.locationSelect).trigger('change');
383				}
384				return;
385			} else {
386				if (self.locations.length > 1) {
387					if (lsCnt < self.locations.length) {
388						$('input.checkall', self.locationSelect).prop('checked', false);
389					}
390				} else {
391					$('input.checkall', self.locationSelect).prop('checked', 'checked');
392				}
393			}
394
395			if (rlsCnt === 0) {
396				self.responsiveLocationSelect.multiselect('selectAll', true);
397			}
398
399			self._updating = false;
400		},
401		_bindEvents: function () {
402			var self = this;
403			self.locationSelect.on('change', '.checkall', function (event) {
404				if (!self.initComplete) return;
405				if ($(this).is(':checked')) {
406					$('input[data-location]', self.locationSelect).prop('checked', 'checked');
407					self.responsiveLocationSelect.multiselect('selectAll', false);
408
409				} else {
410					$('input[data-location]', self.locationSelect).prop('checked', false);
411					self.responsiveLocationSelect.multiselect('deselectAll', false);
412				}
413				self._update();
414			});
415			self.locationSelect.on('change', 'input[data-location]', function (event) {
416				if (!self.initComplete) return;
417				if ($(this).is(':checked')) {
418					self.responsiveLocationSelect.multiselect('select', $(this).attr('data-location'), false);
419				} else {
420					self.responsiveLocationSelect.multiselect('deselect', $(this).attr('data-location'), false);
421				}
422				self._update();
423			});
424
425
426			var s = this.settings.selectedDate ? moment(this.settings.selectedDate) : moment();
427			if (this.clientSettings.rooms_allow_past_bookings && parseInt(this.settings.mode) === 1) {
428				this.responsiveDatePicker.datepicker({
429					format: 'MM d yyyy',
430					todayHighlight: true,
431					title: "Date picker"
432				}).on('changeDate', function (event) {
433					if (!self.initComplete) return;
434					self.datePicker.datepicker('update', event.date);
435					self._update();
436				}).datepicker('update', s.startOf('day').toDate());
437
438				this.datePicker.datepicker({
439					todayBtn: "linked",
440					orientation: "bottom left",
441					format: 'MM d yyyy',
442					todayHighlight: true,
443					title: "Date picker"
444				}).on('changeDate', function (event) {
445					if (!self.initComplete) return;
446					self.responsiveDatePicker.datepicker('update', event.date);
447					self._update();
448				}).datepicker('update', s.startOf('day').toDate());
449
450
451			} else {
452				this.responsiveDatePicker.datepicker({
453					format: 'MM d yyyy',
454					todayHighlight: true,
455					startDate: new Date(),
456					title: "Date picker"
457				}).on('changeDate', function (event) {
458					if (!self.initComplete) return;
459					self.datePicker.datepicker('update', event.date);
460					self._update();
461				}).datepicker('update', s.startOf('day').toDate());
462
463				this.datePicker.datepicker({
464					todayBtn: "linked",
465					orientation: "bottom left",
466					format: 'MM d yyyy',
467					todayHighlight: true,
468					startDate: new Date(),
469					title: "Date picker"
470				}).on('changeDate', function (event) {
471					if (!self.initComplete) return;
472					self.responsiveDatePicker.datepicker('update', event.date);
473					self._update();
474				}).datepicker('update', s.startOf('day').toDate());
475			}
476
477			if (this.clientSettings.rooms_cart_enabled) {
478				this.cartHolder.on('click', '.amnp-booking-summary-asset .amnp-booking-summary-booking-remove', function () {
479					var booking = $(this).closest('.amnp-booking-summary').find('.amnp-booking-summary-total .amnp-booking-summary-booking-remove').data('booking');
480					var asset = $(this).data('asset');
481					booking.assets = booking.assets.filter(function (item) {
482						return item !== asset;
483					});
484					self._updateCart();
485				}).on('click', '.amnp-booking-summary-total .amnp-booking-summary-booking-remove', function () {
486					var booking = $(this).data('booking');
487					self.rooms.forEach(function (r) {
488						r.bookings = r.bookings.filter(function (b) {
489							return b.t_id !== booking.t_id;
490						});
491					});
492
493					self.bookings = self.bookings.filter(function (item) {
494						return item !== booking;
495					});
496					self._updateCart();
497					self._render();
498					self._openCart(false);
499				}).on('keydown', '.amnp-booking-summary-booking-remove', function (event) {
500					var key = event.key;
501					var keyCode = event.which || event.keyCode;
502					if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
503						event.preventDefault();
504						$(this).trigger('click');
505					}
506				});
507
508				this.cartCheckout.on('click', function () {
509					self._checkout();
510				});
511
512				this.cartButton.on('click', function () {
513					self._toggleCart(true);
514				}).on('keydown', function (event) {
515					var key = event.key;
516					var keyCode = event.which || event.keyCode;
517					if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
518						event.preventDefault();
519						self._toggleCart(true);
520					} else if (key === 'Escape' || keyCode === 27) {
521						self._closeCart(true);
522					}
523				}).on('blur', function (event) {
524					if (!self._cartOpen) return;
525					var related = event.relatedTarget || document.activeElement;
526					if (!related || !self.cartHolder.has(related).length) {
527						self._closeCart(false);
528					}
529				});
530
531				this.cartPanel.on('keydown', function (event) {
532					if (event.key === 'Escape' || event.which === 27) {
533						event.preventDefault();
534						self._closeCart(true);
535					}
536				}).on('focusout', function (event) {
537					var related = event.relatedTarget || document.activeElement;
538					if (!self._cartOpen) return;
539					if (!related || !self.cartHolder.has(related).length) {
540						self._closeCart(false);
541					}
542				});
543
544				this.cartHolder.on('mouseenter', function () {
545					if (!self.bookings.length) return;
546					self._openCart(false);
547				}).on('mouseleave', function () {
548					self._closeCart(false);
549				});
550			}
551			if (this.clientSettings.rooms_group_billing_enabled) {
552				this.groupTypeSelect.on('change', function (event) {
553					if (self.clientSettings.rooms_group_billing_remember) {
554						self._createCookie('selectedClass' + self.settings.client, $(this).val(), 365);
555					}
556					self._update(true);
557				});
558
559				this.responsiveGroupTypeSelect.on('change', function (event) {
560					self.groupTypeSelect.val($(this).val());
561					if (self.clientSettings.rooms_group_billing_remember) {
562						self._createCookie('selectedClass' + self.settings.client, $(this).val(), 365);
563					}
564					self._update(true);
565				});
566			}
567			this.navigation.on('click', '.amnp-navigation-control', function () {
568				var newdate = moment(self.datePicker.datepicker('getDate')).add(parseInt($(this).attr('data-by'), 10), 'day').startOf('day');
569				if (newdate.isBefore(moment().startOf('day'))) {
570					if (parseInt(self.settings.mode) === 1) {
571						if (!self.clientSettings.rooms_allow_past_bookings) return;
572					} else {
573						return;
574					}
575				}
576				self.datePicker.datepicker('setDate', newdate.toDate());
577			});
578		},
579		_promptForClass: function () {
580			var self = this;
581			var selectClassPopup = $('<div class="amnp-choose-class-popup"><div class="buttons"><button class="btn btn-primary">done</button></div></div>').amPopup({ title: 'Booking Type', width: '300px' });
582			selectClassPopup.prepend(this.groupTypeSelect.clone());
583			selectClassPopup.prepend('<div>Please choose the type of room booking</div>');
584			selectClassPopup.on('change', 'select', function () {
585				if (self.groupTypeSelect.is(':visible')) {
586					self.groupTypeSelect.val($(this).val()).trigger('change');
587				}
588
589				if (self.responsiveGroupTypeSelect.is(':visible')) {
590					self.responsiveGroupTypeSelect.val($(this).val()).trigger('change');
591				}
592			}).on('click', '.btn', function () {
593				if (self.clientSettings.rooms_group_billing_remember) {
594					if (self.groupTypeSelect.is(':visible')) {
595						self._createCookie('selectedClass' + self.settings.client, self.groupTypeSelect.val(), 365);
596					}
597
598					if (self.responsiveGroupTypeSelect.is(':visible')) {
599						self._createCookie('selectedClass' + self.settings.client, self.responsiveGroupTypeSelect.val(), 365);
600					}
601				}
602				selectClassPopup.data('plugin_amPopup').close();
603			}).data('plugin_amPopup').open();
604		},
605		_hideCover: function () {
606			this.loader.hide();
607		},
608		_setupEmbeddedBridge: function () {
609			var self = this;
610			if (!self.settings.embeddedMode) {
611				return;
612			}
613			window.addEventListener('message', function (event) {
614				var origin = self._getParentOrigin();
615				if (!origin || event.origin !== origin || event.source !== window.parent) {
616					return;
617				}
618				if (!event.data || typeof event.data !== 'object') {
619					return;
620				}
621				var data = event.data;
622				if (data.source !== 'classic-reserve-host' || data.type !== 'height-request') {
623					return;
624				}
625				self._scheduleHeightPost(true);
626			});
627			if (typeof MutationObserver !== 'undefined') {
628				self._embeddedHeightObserver = new MutationObserver(function () {
629					self._scheduleHeightPost();
630				});
631				self._embeddedHeightObserver.observe(self.holder[0], {
632					attributes: true,
633					childList: true,
634					subtree: true
635				});
636			}
637			if (typeof ResizeObserver !== 'undefined') {
638				self._embeddedResizeObserver = new ResizeObserver(function () {
639					self._scheduleHeightPost();
640				});
641				self._embeddedResizeObserver.observe(self.element);
642			}
643			window.addEventListener('load', function () {
644				self._scheduleHeightPost();
645			});
646		},
647		_getParentOrigin: function () {
648			if (typeof this.settings.parentOrigin === 'string' && this.settings.parentOrigin.length > 0) {
649				return this.settings.parentOrigin;
650			}
651			if (document.referrer && document.referrer.length > 0) {
652				try {
653					return (new URL(document.referrer)).origin;
654				} catch (e) {
655					return '';
656				}
657			}
658			return '';
659		},
660		_postToParent: function (type, payload) {
661			if (!this.settings.embeddedMode || !window.parent || window.parent === window) {
662				return;
663			}
664			var origin = this._getParentOrigin();
665			if (!origin) {
666				return;
667			}
668			window.parent.postMessage({
669				source: 'classic-reserve',
670				type: type,
671				payload: payload || {}
672			}, origin);
673		},
674		_scheduleHeightPost: function (forcePost) {
675			var self = this;
676			if (!self.settings.embeddedMode) {
677				return;
678			}
679			if (self._embeddedHeightFrame) {
680				window.cancelAnimationFrame(self._embeddedHeightFrame);
681			}
682			self._embeddedHeightFrame = window.requestAnimationFrame(function () {
683				self._embeddedHeightFrame = null;
684				var rootElement = self.element && self.element.nodeType === 1
685					? self.element
686					: (self.element && self.element.length ? self.element[0] : null);
687				var rootElementJQ = rootElement ? $(rootElement) : null;
688				var holderVisible = self.holder && self.holder.length && self.holder.is(':visible');
689				var holderHeight = holderVisible ? Math.ceil(self.holder.outerHeight(true)) : 0;
690				var holderScrollHeight = holderVisible ? Math.ceil(self.holder[0].scrollHeight || 0
690) : 0;
691				var stageVisible = self.stage && self.stage.length && self.stage.is(':visible');
692				var stageHeight = stageVisible ? Math.ceil(self.stage.outerHeight(true)) : 0;
693				var stageScrollHeight = stageVisible ? Math.ceil(self.stage[0].scrollHeight || 0) : 0;
694				var keyVisible = self.key && self.key.length && self.key.is(':visible');
695				var keyHeight = keyVisible ? Math.ceil(self.key.outerHeight(true)) : 0;
696				var keyScrollHeight = keyVisible ? Math.ceil(self.key[0].scrollHeight || 0) : 0;
697				var elementHeight = rootElementJQ ? Math.ceil(rootElementJQ.outerHeight(true) || 0) : 0;
698				var elementScrollHeight = rootElement ? Math.ceil(rootElement.scrollHeight || 0) : 0;
699				var bookerHolder = rootElementJQ ? rootElementJQ.find('.amnp-booker-holder:visible').first() : null;
700				var bookerHeight = bookerHolder && bookerHolder.length ? Math.ceil(bookerHolder.outerHeight(true)) : 0;
701				var bookerScrollHeight = bookerHolder && bookerHolder.length ? Math.ceil(bookerHolder[0].scrollHeight || 0) : 0;
702				var contentHeight = Math.max(
703					holderHeight,
704					holderScrollHeight,
705					stageHeight,
706					stageScrollHeight,
707					keyHeight,
708					keyScrollHeight,
709					elementHeight,
710					elementScrollHeight,
711					bookerHeight,
712					bookerScrollHeight
713				);
714				contentHeight += 16;
715				if (!contentHeight || (!forcePost && contentHeight === self._lastEmbeddedHeight)) {
716					return;
717				}
718				self._lastEmbeddedHeight = contentHeight;
719				self._postToParent('height-update', { height: contentHeight });
720			});
721		},
722		_showCover: function (spinner) {
723			this.loader.css({
724				'width': '100%',
725				'height': this.stage.outerHeight()
726			}).show();
727			$('.amnp-spinner', this.loader).toggle(spinner);
728		},
729		_render: function () {
730			// console.trace();
731			var self = this;
732
733			this.stage.empty();
734			var curDate = moment(this.datePicker.datepicker('getUTCDate')).utc();
735			var lastBooking = this.bookings.slice(-1)[0];
736			var hightlightRoom = false;
737			if (lastBooking) {
738				if (moment(lastBooking.start).isSame(curDate, 'd')) {
739					hightlightRoom = parseInt(lastBooking.room.id, 10);
740				}
741			}
742
743			var roomsVisible = false;
744			this.rooms.forEach(function (room) {
745				//CH#92 - Parent/Child Fix
746				var currentBookingClass = false;
747				var currentBookingType = false;
748
749				if (self.groupTypeSelect) {
750					if (self.groupTypeSelect.is(':visible')) {
751						currentBookingClass = self.groupTypeSelect.val();
752						currentBookingType = parseInt(self.groupTypeSelect.find(":selected").data('details').type);
753					}
754				}
755
756				if (self.responsiveGroupTypeSelect) {
757					if (self.responsiveGroupTypeSelect.is(':visible')) {
758						currentBookingClass = self.responsiveGroupTypeSelect.val();
759						currentBookingType = parseInt(self.responsiveGroupTypeSelect.find(":selected").data('details').type)
760					}
761				}
762
763				if ((!room.restrictions.viewable && self.settings.mode == 0) || (!room.restrictions.viewable && self.settings.mode === 1 && currentBookingType !== 0)) return;
764				if (self.settings.roomId && parseInt(room.id, 10) !== parseInt(self.settings.roomId, 10)) return;
765				if (room.roomHours[self.CurrentDateString]) {
766					var hours = room.roomHours[self.CurrentDateString];
767				} else {
768					var hours = self.openingHours[room.location_id][self.CurrentDateString];
769				}
770
771				if (self.settings.mode !== 1 && hours.open == hours.close) {
772				} else {
773					roomsVisible = true;
774					room.locations = room.locations == null ? [] : room.locations;
775					if (room.locations.indexOf(room.location_id) == -1) room.locations.push(room.location_id);
776					room.locations.forEach(function (location_id) {
777						if (self.selectedLocations.indexOf(location_id) === -1) return;
778						var location = self.locationsById[location_id];
779						if (typeof location !== 'undefined') {
780							var locationBlock = $('.amnp-location-section[data-locationid=' + location_id + ']', self.stage);
781							if (locationBlock.length == 0) {
782								locationBlock = $('<div class="amnp-location-section" data-location-name="' + location.name + '" data-locationid="' + location_id + '"><div class="amnp-location-section-title"><h3><i class="am-locations"/>' + location.name + '</h3></div></div>').appendTo(self.stage);
783							}
784							var roomDisplay = $('<div/>').appendTo(locationBlock);
785
786							if (hightlightRoom !== false && hightlightRoom === parseInt(room.id, 10)) {
787								roomDisplay.addClass('amnp-cart-added').delay(3000).queue(function () {
788									$(this).removeClass('amnp-cart-added').dequeue();
789								});
790							}
791
792							roomDisplay.amReserveRoom({
793								client: self.settings.client,
794								timezone: self.settings.timezone,
795								apiServer: self.settings.apiServer,
796								multiLocation: self.locationCount > 1,
797								room: room,
798								location: location,
799								hours: hours,
800								singleBranch: self.locations.length === 1,
801								enabledCart: self.clientSettings.rooms_cart_enabled,
802								enablePatronNotes: self.clientSettings.rooms_patron_notes_enabled,
803								enableStaffNotes: self.settings.mode === 1 && self.settings.enableStaffNotes,
804								enableExpectedAttendees: self.clientSettings.rooms_expected_attendees_enabled,
805								expectedAttendeesRequired: self.clientSettings.rooms_expected_attendees_required,
806								useSetupBreakdown: self.clientSettings.rooms_use_setup,
807								adjustSetupBreakdown: self.clientSettings.rooms_adjust_setup,
808								pinlessAuth: self.clientSettings.client_pinless_auth,
809								ecommerceEnabled: self.clientSettings.rooms_ecommerce_enabled,
810								chargingEnabled: self.clientSettings.rooms_charging_enabled,
811								loginRequired: self.clientSettings.rooms_require_login,
812								layoutRequired: self.clientSettings.rooms_layout_required,
813								emailRequired: self.clientSettings.rooms_require_email,
814								phoneRequired: self.clientSettings.rooms_require_phone,
815								roomsMaxBookingMessage: self.clientSettings.rooms_max_booking_message,
816								roomsMinBookingMessage: self.clientSettings.rooms_min_booking_message,
817								roomsNearBookingMessage: self.clientSettings.rooms_near_booking_message,
818								roomsFarBookingMessage: self.clientSettings.rooms_far_booking_message,
819								roomsNotBookableMessage: self.clientSettings.rooms_not_bookable_message,
820								groupBillingEnabled: self.clientSettings.rooms_group_billing_enabled,
821								date: curDate.format('YYYY-MM-DD'),
822								selected: parseInt(self.settings.selectedRoom, 10) === parseInt(room.id, 10),
823								bookingClass: self.clientSettings.rooms_group_billing_enabled ? currentBookingClass : false,
824								bookingType: self.clientSettings.rooms_group_billing_enabled ? currentBookingType : (self.settings.mode === 1 || self.clientSettings.rooms_staff_booking_class_default ? 0 : 1),
825								mode: self.settings.mode,
826								clientSettings: self.clientSettings,
827								payment_currency: self.settings.payment_currency,
828								payment_currency_symbol: self.settings.payment_currency_symbol
829							}).on('room:addtocart', function (event, data) {
830								data.details.t_id = Math.random().toString(36).substr(2, 5);
831
832								if (Array.isArray(data.details.assets) && data.details.assets.length > 0) {
833									var assetCheck = self._checkAssets(data.details);
834									if (!assetCheck.ok) {
835										alert(assetCheck.message);
836										return;
837									}
838								}
839
840								self.bookings.push(data.details);
841								var toUpdate = [data.details.room.id];
842								toUpdate = toUpdate.concat(room.parentRooms, room.childRooms);
843
844								self._addCartBookingToRooms(data.details, toUpdate);
845								self._updateCart();
846								if (data.checkout && self.bookings.length > 0) {
847									self.pendingFocus = null;
848									self._announce('Opening reservation details');
849									self._checkout();
850								} else {
851									var roomTitle = data.details.room && data.details.room.title ? data.details.room.title : 'Room';
852									self.pendingFocus = { type: 'room', roomId: data.details.room.id };
853									self._announce(roomTitle + ' added to basket');
854									self._render();
855								}
856							});
857						}
858					});
859				}
860			});
861			if (!roomsVisible) {
862				$('<div class="amnp-picker-message"><p>No rooms found.</p><p>Please change your search criteria</p></div>').appendTo(self.stage);
863			}
864			self._updateLocations();
865			self._sortStage();
866			self._updateCart();
867			self._hideCover();
868			if (self.pendingFocus && self.pendingFocus.type === 'room') {
869				self._focusRoomById(self.pendingFocus.roomId);
870				self.pendingFocus = null;
871			}
872			self._scheduleHeightPost();
873		},
874		_announce: function (message) {
875			if (!this.liveRegion) return;
876			this.liveRegion.empty();
877			if (message) {
878				this.liveRegion.text(message);
879			}
880		},
881		_focusRoomById: function (roomId) {
882			if (!roomId) return;
883			var holder = this.stage.find('.amnp-room-holder[data-room-id="' + roomId + '"]');
884			if (!holder.length) return;
885			var container = holder.parent();
886			var pluginInstance = container.data('plugin_amReserveRoom');
887			if (pluginInstance && typeof pluginInstance.focusHolder === 'function') {
888				pluginInstance.focusHolder();
889			} else {
890				this.stage.find('.amnp-room-holder').attr('tabindex', '-1');
891				holder.attr('tabindex', '0').focus();
892			}
893		},
894		_openCart: function (focusFirst) {
895			if (!this.clientSettings.rooms_cart_enabled || !this.cartPanel || this._cartOpen) return;
896			this._cartOpen = true;
897			this.cartPanel.stop(true, true).fadeIn(0);
898			this.cartPanel.attr('aria-hidden', 'false');
899			if (this.cartButton) this.cartButton.attr('aria-expanded', 'true');
900			if (this.loader) {
901				var loaderZ = parseInt(this.loader.css('z-index'), 10);
902				if (isNaN(loaderZ)) loaderZ = 999;
903				this.cartPanel.css('z-index', loaderZ + 1);
904				this.loader.css('pointer-events', 'none');
905			}
906			this._showCover(false);
907			if (focusFirst) {
908				this._focusFirstCartItem();
909			}
910			this._scheduleHeightPost();
911		},
912		_closeCart: function (restoreFocus) {
913			if (!this.clientSettings.rooms_cart_enabled || !this.cartPanel) return;
914			if (!this._cartOpen) {
915				if (this.cartButton) this.cartButton.attr('aria-expanded', 'false');
916				this.cartPanel.attr('aria-hidden', 'true');
917				return;
918			}
919			this._cartOpen = false;
920			this.cartPanel.stop(true, true).fadeOut(0);
921			this.cartPanel.attr('aria-hidden', 'true');
922			if (this.cartButton) {
923				this.cartButton.attr('aria-expanded', 'false');
924				if (restoreFocus) this.cartButton.focus();
925			}
926			if (this.loader) {
927				this.loader.css('pointer-events', '');
928			}
929			this._hideCover();
930			this._scheduleHeightPost();
931		},
932		_toggleCart: function (focusFirst) {
933			if (this._cartOpen) {
934				this._closeCart(true);
935			} else {
936				this._openCart(focusFirst);
937			}
938		},
939		_focusFirstCartItem: function () {
940			var focusables = this._getCartFocusables();
941			if (focusables.length) {
942				focusables.first().focus();
943			} else if (this.cartPanel) {
944				this.cartPanel.attr('tabindex', '-1').focus();
945				this.cartPanel.removeAttr('tabindex');
946			}
947		},
948		_getCartFocusables: function () {
949			if (!this.cartPanel) return $();
950			var selector = 'button, [href], [tabindex]:not([tabindex="-1"])';
951			return this.cartPanel.find(selector).filter(':visible');
952		},
953		_addCartBookingToRooms: function (details, toUpdate) {
954			this.rooms.forEach(function (r) {
955				if (toUpdate.indexOf(r.id) !== -1) {
956					r.bookings.push({
957						start_time: details.start,
958						end_time: details.end,
959						breakdown_time: details.breakdown_time,
960						setup_time: details.setup_time,
961						t_id: details.t_id
962					});
963				}
964			});
965		},
966		_checkAssetOverlap: function (booking, asset) {
967			var self = this;
968
969			if (typeof self.bookings !== 'undefined' && self.bookings.length > 0) {
970				for (var b = 0; b < self.bookings.length; b++) {
971					var book = self.bookings[b];
972
973					if (book !== booking) {
974						var bssA = moment(booking.start);
975						var bseA = moment(booking.end);
976						var bssB = moment(book.start);
977						var bseB = moment(book.end);
978
979						var bRangeA = moment.range(bssA, bseA);
980						var bRangeB = moment.range(bssB, bseB);
981
982						if (bRangeA.overlaps(bRangeB)) {
983							return booking.assets.findIndex(function (v, i) { return parseInt(v.id) === parseInt(asset.id) }) > -1;
984						}
985					}
986				}
987			}
988
989			return false;
990		},
991		_checkAssets: function (bookingData) {
992			var self = this;
993			var incomingAssets = bookingData.assets;
994			if (typeof self.bookings !== 'undefined' && self.bookings.length > 0) {
995				for (var i = 0; i < incomingAssets.length; i++) {
996					var cartQty = 0;
997					var assetToCheck = incomingAssets[i];
998					var assetData = assetToCheck.assetData;
999					var bookingMin = parseInt(assetData.booking_min);
1000					var bookingMax = parseInt(assetData.booking_max);
1001					var available = parseInt(assetData.quantity);
1002					var offset = 0;
1003
1004					// Start off by setting the cart qty to the incoming one.
1005					cartQty += parseInt(assetToCheck.qty);
1006
1007					// These checks have to be above the other bookings.
1008					// Reason being that we really only care about the incoming. Fairly sure this is already handled, but it is for sure now.
1009					if (cartQty < bookingMin && bookingMin > 0) {
1010						offset = bookingMin - cartQty;
1011						return { ok: false, message: 'You must select ' + offset + ' more of ' + assetData.name + ' to continue' };
1012					}
1013
1014					if (cartQty > bookingMax && bookingMax > 0) {
1015						offset = cartQty - bookingMax;
1016						return { ok: false, message: 'You must remove ' + offset + ' of ' + assetData.name + ' from the current booking to continue.' };
1017					}
1018
1019					// Loop through all bookings.
1020					for (var b = 0; b < self.bookings.length; b++) {
1021						var book = self.bookings[b];
1022						if (Array.isArray(book.assets) && book.assets.length > 0) {
1023							var found = book.assets.findIndex(function (v, i) { return parseInt(v.id) === parseInt(assetToCheck.id) });
1024							if (found > -1) {
1025								// We have a match.
1026								// Get booked asset data first first
1027
1028								// CH 22796 - Need to check if the dates/times actually overlap, function returns t/f
1029								// f = DO NOT Add because it doesn't overlap
1030								// t = add because it overlaps
1031								if (self._checkAssetOverlap(bookingData, assetToCheck)) {
1032									cartQty += parseInt(book.assets[found].qty);
1033								}
1034
1035								// No break, we want to get ALL bookings in the basket.
1036							}
1037						}
1038					}
1039
1040					// Check to make sure that the available number of assets is not exceeded by all bookings + incoming
1041					// If it is then it was incoming that broke it.
1042					if (cartQty > available) {
1043						offset = cartQty - available;
1044						return { ok: false, message: 'There are only ' + available + ' of ' + assetData.name + ' remaining. You must remove ' + offset + ' from the current booking to continue.' };
1045					}
1046				}
1047			}
1048
1049			return { ok: true };
1050		},
1051		_checkout: function () {
1052			var self = this;
1053			var roomBooker = $('<div/>').appendTo(this.element);
1054			var currentBookingClass = false;
1055
1056			if (!self.settings.embeddedMode) {
1057				self.navigation.unstick();
1058				self.key.unstick();
1059			}
1060
1061			if (self.groupTypeSelect) {
1062				if (self.groupTypeSelect.is(':visible')) {
1063					currentBookingClass = self.groupTypeSelect.val();
1064				}
1065			}
1066
1067			if (self.responsiveGroupTypeSelect) {
1068				if (self.responsiveGroupTypeSelect.is(':visible')) {
1069					currentBookingClass = self.responsiveGroupTypeSelect.val();
1070				}
1071			}
1072			self.holder.hide();
1073			self._scheduleHeightPost();
1074
1075			var payLaterEnabled = self.settings.mode === 1
1076				? self.clientSettings.rooms_enable_pay_later_staff
1077				: self.clientSettings.rooms_enable_pay_later;
1078
1079			var earliestAutoCancelTime = false;
1080			var now = moment().tz(self.settings.timezone);
1081			if (self.clientSettings.rooms_auto_cancel_unpaid_pay_later && self.bookings.length > 0) {
1082				for (var b = 0; b < self.bookings.length; b++) {
1083					var booking = self.bookings[b];
1084					if (booking.room.restrictions.pay_later_auto_cancel_enabled == 1 && booking.room.restrictions.pay_later_auto_cancel_min !== null) {
1085						// Auto-cancellation is enabled for this booking class
1086						var autoCancelMin = parseInt(booking.room.restrictions.pay_later_auto_cancel_min);
1087						var autoCancelTime = moment.tz(booking.start, self.settings.timezone).subtract(autoCancelMin, 'minutes');
1088						if (!earliestAutoCancelTime || autoCancelTime.isBefore(earliestAutoCancelTime)) {
1089							earliestAutoCancelTime = autoCancelTime;
1090						}
1091					}
1092				}
1093			}
1094			// Do not allow patrons to book with pay later after the auto-cancellation time
1095			if (earliestAutoCancelTime && earliestAutoCancelTime.isBefore(now) && self.settings.mode === 0) {
1096				payLaterEnabled = false;
1097			}
1098
1099			// Display payment deadline before booking
1100			var autoCancelNoticeTemplate = self.settings.mode === 0 ? self.clientSettings.rooms_auto_cancel_notice : 'The patron will have until {{time}} on {{date}} to complete their payment.';
1101			var autoCancelNotice = payLaterEnabled && earliestAutoCancelTime && !earliestAutoCancelTime.isBefore(now) ? autoCancelNoticeTemplate.replace('{{date}}', earliestAutoCancelTime.format('MMMM DD, YYYY')).replace('{{time}}', earliestAutoCancelTime.format('h:mma')) : "";
1102
1103			roomBooker.amReserveBooker({
1104				client: self.settings.client,
1105				apiServer: self.settings.apiServer,
1106				singleBranch: self.locations.length === 1,
1107				enabledCart: self.clientSettings.rooms_cart_enabled,
1108				enablePatronNotes: self.clientSettings.rooms_patron_notes_enabled,
1109				enableStaffNotes: self.settings.mode === 1 && self.settings.enableStaffNotes,
1110				enableExpectedAttendees: self.clientSettings.rooms_expected_attendees_enabled,
1111				expectedAttendeesRequired: self.clientSettings.rooms_expected_attendees_required,
1112				useSetupBreakdown: self.clientSettings.rooms_use_setup,
1113				adjustSetupBreakdown: self.clientSettings.rooms_adjust_setup,
1114				pinlessAuth: self.clientSettings.client_pinless_auth,
1115				roomsPatronDetailsReadonly: self.clientSettings.rooms_patron_details_readonly,
1116				ecommerceEnabled: self.clientSettings.rooms_ecommerce_enabled,
1117				chargingEnabled: self.clientSettings.rooms_charging_enabled,
1118				offlinePaymentsEnabled: self.settings.mode === 1 ? self.clientSettings.rooms_staff_offline_payments_enabled : self.clientSettings.rooms_patron_offline_payments_enabled,
1119				cardPaymentEnabled: self.clientSettings.rooms_enable_card,
1120				offlinePaymentsMessage: self.clientSettings.rooms_offline_payments_message,
1121				laterPaymentsMessage: self.clientSettings.rooms_later_payments_message,
1122				autoCancelNotice,
1123				loginRequired: self.clientSettings.rooms_require_login,
1124				layoutRequired: self.clientSettings.rooms_layout_required,
1125				emailRequired: self.clientSettings.rooms_require_email,
1126				phoneRequired: self.clientSettings.rooms_require_phone,
1127				staffRespectPatronLimit: self.clientSettings.rooms_staff_respect_patron_limit,
1128				enforceMaxAttendees: self.clientSettings.rooms_enforce_max_attendees,
1129				groupBillingEnabled: self.clientSettings.rooms_group_billing_enabled,
1130				bookingClass: self.clientSettings.rooms_group_billing_enabled ? currentBookingClass : false,
1131				bookings: self.bookings,
1132				mode: self.settings.mode,
1133				offlineButtonText: self.clientSettings.rooms_offline_button_text,
1134				rooms_mediated_booking_message: self.clientSettings.rooms_mediated_booking_message,
1135				showSMSPrompt: self.clientSettings.notifications_allow_sms_optout,
1136				SMSOptMode: self.clientSettings.notifications_sms_opt_mode,
1137				SMSPromptMessage: self.clientSettings.notifications_sms_optout_message,
1138				sessionId: this.settings.sessionId,
1139				chargingMode: this.settings.chargingMode,
1140				testMode: this.settings.testMode,
1141				ezproxy: this.settings.ezproxy,
1142				rooms_remotelock_show_on_confirmationscreen_mediated: self.clientSettings.rooms_remotelock_show_on_confirmationscreen_mediated,
1143				payment_currency: this.settings.payment_currency,
1144				payment_currency_symbol: this.settings.payment_currency_symbol,
1145				phone_number_regex: this.clientSettings.phone_number_regex,
1146				payLaterEnabled,
1147				payLaterButtonText: self.clientSettings.rooms_pay_later_button_text,
1148			}).on('booker:reset', function (event, status) {
1149				self.storage.removeItem('communicoReserveCart');
1150				self.bookings = [];
1151				self._updateCart();
1152			}).on('booker:closed', function (event, status) {
1153				if (status || !self.clientSettings.rooms_cart_enabled) {
1154					self.bookings = [];
1155					self._updateCart();
1156				}
1157				roomBooker.remove();
1158				self.holder.show();
1159				if (!self.settings.embeddedMode) {
1160					if (self.settings.mode === 1) {
1161						self.navigation.sticky({ topSpacing: 80, wrapperClassName: 'sticky-nav' });
1162						self.key.sticky({ topSpacing: 140, bottomSpacing: 50, wrapperClassName: 'sticky-key' });
1163					} else {
1164						self.navigation.sticky({ wrapperClassName: 'sticky-nav' });
1165						self.key.sticky({ topSpacing: 60, bottomSpacing: ($('#footer').outerHeight() || 300) + 50, wrapperClassName: 'sticky-key' });
1166					}
1167				}
1168				self._update();
1169				self._scheduleHeightPost();
1170			});
1171			var bookerHolder = roomBooker.find('.amnp-booker-holder').first();
1172			if (bookerHolder.length) {
1173				bookerHolder.attr('tabindex', '-1').focus();
1174			}
1175			self._announce('Reservation details ready');
1176			self._scheduleHeightPost();
1177		},
1178		_updateCart: function () {
1179			var self = this;
1180			this.bookings.forEach(function (booking) {
1181				var room = null;
1182				//console.log(booking.booking_class, booking.room.id);
1183				//console.log(self.roomByClassById);
1184				if (parseInt(booking.booking_class) > 0) {
1185					room = self.roomByClassById[booking.booking_class][booking.room.id];
1186				} else {
1187					room = self.roomById[booking.room.id];
1188				}
1189
1190				booking.room = room;
1191			});
1192
1193			if (this.storage) {
1194				this.storage.setItem('communicoReserveCart', JSON.stringify(this.bookings));
1195			}
1196
1197			if (this.cartCount) {
1198				this.cartCount.text(this.bookings.length);
1199			}
1200			if (this.cartButton) {
1201				var countLabel = this.bookings.length === 1 ? '1 item' : this.bookings.length + ' items';
1202				this.cartButton.attr('aria-label', 'Room basket, ' + countLabel + '. Press enter to view basket');
1203			}
1204			if (this.cartList) {
1205				this.cartList.empty();
1206				if (this.bookings.length === 0) {
1207					this.cartList.text('Your cart is empty');
1208					if (this.cartFooter) this.cartFooter.hide();
1209					this._closeCart(false);
1210				} else {
1211					if (this.cartFooter) this.cartFooter.show();
1212					var self = this;
1213					this.bookings.forEach(function (booking) {
1214						self._getBookingSummeryDetails(booking).appendTo(self.cartList);
1215					});
1216				}
1217			} else {
1218				if (this.bookings.length === 0 && this.cartFooter) this.cartFooter.hide();
1219			}
1220			if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) {
1221				if (this.cartTotal) {
1222					this.cartTotal.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateTotalCharge()));
1223				}
1224			} else if (this.cartTotal) {
1225				this.cartTotal.empty();
1226			}
1227			if (this.cartPanel) {
1228				this.cartPanel.attr('aria-hidden', this._cartOpen ? 'false' : 'true');
1229			}
1230			this._scheduleHeightPost();
1231		},
1232		_getBookingSummeryDetails: function (booking) {
1233			var $bookingSummary = $('<div class="amnp-booking-summary"></div>');
1234			var $bookingSummaryDetails = $('<div class="amnp-booking-summary-details"></div>').appendTo($bookingSummary);
1235			var startTime = moment(booking.start);
1236			var endTime = moment(booking.end);
1237
1238			if (booking.room.image && booking.room.image.length > 0) {
1239				$('<img/>').attr('src', '//' + this.settings.client + '.libnet.info/images/roombooking/' + this.settings.client + '/' + booking.room.image).attr('alt', booking.room.title).appendTo($bookingSummaryDetails);
1240			}
1241
1242			var title = $('<div class="amnp-booking-summary-title">' + booking.room.title + '</div>').appendTo($bookingSummaryDetails);
1243			if (!this.settings.singleBranch) {
1244				title.append('<span>, ' + booking.location.name + '</span>');
1245			}
1246			$('<div class="amnp-booking-summary-time">' + startTime.format('h:mma') + ' - ' + endTime.format('h:mma') + '</b>, ' + endTime.format('MMMM DD, YYYY') + '</div>').appendTo($bookingSummaryDetails);
1247
1248			var $bookingSummaryCost = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary);
1249			var minutes = moment.duration(endTime.diff(startTime)).asMinutes();
1250			var realmin = minutes % 60
1251			var hours = Math.floor(minutes / 60);
1252			var displayText = '';
1253			if (hours > 0) {
1254				displayText = hours + (hours === 1 ? ' hour' : ' hours');
1255			}
1256			if (realmin > 0) {
1257				displayText += ' ' + realmin + (realmin === 1 ? ' minute' : ' minutes');
1258			}
1259			$('<div class="amnp-booking-summary-booking-name"></div>').text(displayText).appendTo($bookingSummaryCost);
1260			var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryCost);
1261			if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) {
1262				costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, false)));
1263			}
1264
1265			if (booking.assets.length > 0) {
1266				var $bookingSummaryAssets = $('<div class="amnp-booking-summary-assets"></div>').appendTo($bookingSummary);
1267				var self = this;
1268				booking.assets.forEach(function (asset) {
1269					var a = $('<div class="amnp-booking-summary-asset"></div>').appendTo($bookingSummaryAssets);
1270					$('<div class="amnp-booking-summary-booking-name"></div>').text(asset.name).appendTo(a);
1271					$('<div class="amnp-booking-summary-asset-qty"></div>').text('qty ' + asset.qty).appendTo(a);
1272					var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo(a);
1273					if (self.clientSettings.rooms_charging_enabled) {
1274						costHolder.html(self.settings.payment_currency_symbol + '' + self._formatCurrency(parseFloat(asset.cost) * asset.qty));
1275					}
1276					$('<div class="amnp-booking-summary-booking-remove link">remove</div>').attr({ tabindex: 0, role: 'button', 'aria-label': 'Remove ' + asset.name + ' from basket' }).data('asset', asset).appendTo(a);
1277				});
1278			}
1279			var $bookingSummaryTotal = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary);
1280			var $subTotal = $('<div class="amnp-booking-summary-booking-subtotal"></div>').appendTo($bookingSummaryTotal);
1281			var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryTotal);
1282			var removeLabel = 'Remove booking from basket';
1283			if (booking.room && booking.room.title) {
1284				removeLabel = 'Remove ' + booking.room.title + ' booking starting ' + startTime.format('MMMM Do YYYY, h:mma');
1285			}
1286			$('<div class="amnp-booking-summary-booking-remove link">remove</div>').attr({ tabindex: 0, role: 'button', 'aria-label': removeLabel }).data('booking', booking).appendTo($bookingSummaryTotal);
1287			if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) {
1288				$subTotal.text('sub total');
1289				costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, true)));
1290			}
1291
1292			return $bookingSummary;
1293		},
1294		_formatCurrency: function (amount) {
1295			return amount.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
1296		},
1297		_calculateBookingCharge: function (booking, incAssets) {
1298			var startTime = moment(booking.start);
1299			var endTime = moment(booking.end);
1300
1301			var roomTotal = parseFloat(booking.room.restrictions.charge_amount);
1302			if (parseInt(booking.room.restrictions.charge_period) === 0) {
1303				var blockSize = parseInt(booking.room.restrictions.segment_size) * parseInt(booking.room.restrictions.block_count);
1304				var rawDuration = moment.duration(endTime.diff(startTime)).asMinutes();
1305				roomTotal = (rawDuration / blockSize) * roomTotal;
1306			}
1307
1308			if (booking.layout && booking.layout.cost && parseFloat(booking.layout.cost) > 0) {
1309				roomTotal += parseFloat(booking.layout.cost);
1310			}
1311
1312			if (incAssets) {
1313				var assetCharge = booking.assets.reduce(function (total, asset) {
1314					return total + parseFloat(asset.cost) * asset.qty;
1315				}, 0);
1316			} else {
1317				var assetCharge = 0;
1318			}
1319			return parseFloat(roomTotal + assetCharge);
1320		},
1321		_calculateTotalCharge: function () {
1322			var self = this;
1323			var cartTotal = this.bookings.reduce(function (total, booking) {
1324				return total + self._calculateBookingCharge(booking, true);
1325			}, 0);
1326			return cartTotal;
1327		},
1328		_sortStage: function () {
1329			var self = this;
1330			var sections = $('.amnp-location-section', self.stage);
1331			sections.sort(function (a, b) {
1332				var an = a.getAttribute('data-location-name');
1333				var bn = b.getAttribute('data-location-name');
1334
1335				if (an > bn) {
1336					return 1;
1337				}
1338				if (an < bn) {
1339					return -1;
1340				}
1341				return 0;
1342			});
1343			sections.detach().appendTo(self.stage);
1344		},
1345		_update: function (force) {
1346			var self = this;
1347
1348			// Date selection management
1349			var curDate = moment(self.datePicker.datepicker('getUTCDate')).utc();
1350			$('.amnp-navigation-title', self.navigation).text(curDate.format('LL'));
1351			self.CurrentDateString = curDate.format('YYYY-MM-DD');
1352
1353			if (self._updating) return true;
1354			self._updating = true;
1355
1356			// START - Location selection management
1357			self.selectedLocations = [];
1358			if (self.locationSelect.is(":visible")) {
1359				self.locationSelect.find('input[data-location]:checked').each(function (el) {
1360					self.selectedLocations.push($(this).attr('data-location'));
1361				});
1362			}
1363
1364			if ($(".multiselect", self.holder).is(":visible")) {
1365				self.responsiveLocationSelect.find('option[data-location]:selected').each(function (el) {
1366					self.selectedLocations.push($(this).attr('data-location'));
1367				});
1368			}
1369			if (parseInt(self.settings.selectedLocation) > 0 && self.selectedLocations.length === 0) {
1370				self.selectedLocations.push(self.settings.selectedLocation);
1371			}
1372
1373			if (self.selectedLocations.length == 0) {
1374				self._updating = false;
1375				if (!force) {
1376					self.locationCount = 0;
1377					if (self.locations.length == 0) {
1378						self.stage.empty().append('<h3>No rooms found. Please change your search criteria</h3>');
1379					} else {
1380						self.stage.empty().append('<h3>Please choose at least one branch</h3>');
1381					}
1382					self._scheduleHeightPost();
1383					return;
1384				}
1385				if (self.locationSelect.is(":visible")) {
1386					self.locationSelect.find('.checkall').prop('checked', true);
1387				}
1388
1389				if (self.responsiveLocationSelect.is(":visible")) {
1390					self.responsiveLocationSelect.multiselect('selectAll', true);
1391				}
1392				// Our previous search had no matching branches, so fall back to previously
1393				// selected branches for the current search - or to every branch when there
1394				// is no readable saved selection. Stays an array: locationCount below counts
1395				// branches, not characters.
1396				self.selectedLocations = self._readSavedBranches() || self.locations.map(function (location) {
1397					return String(location.id);
1398				});
1399			}
1400			self._createCookie('reserveBranches', JSON.stringify(self.selectedLocations), 365);
1401			self.locationCount = self.selectedLocations.length;
1402			// END - Location selection management
1403
1404			self._showCover(true);
1405
1406			// START - Set parameters for API calls
1407			var roomOptions = {
1408				date: self.CurrentDateString,
1409				class_id: []
1410			};
1411			var locationOptions = {
1412				only_with_external_rooms: 1
1413			};
1414			// Add locations for existing bookings to the roomclass query
1415			var locs = self.selectedLocations;
1416			locs = locs.concat(this.bookings
1417				.map(function (x) { return x.location.id; })
1418				.filter(function (x) { return !locs.includes(x) }));
1419
1420			if (self.clientSettings.rooms_group_billing_enabled) {
1421				if (self.settings.mode === 0) {
1422					roomOptions.external_only = 1;
1423				}
1424				if (self.groupTypeSelect.is(':visible')) {
1425					roomOptions.class_id.push(self.groupTypeSelect.val());
1426				}
1427				if (self.responsiveGroupTypeSelect.is(':visible')) {
1428					roomOptions.class_id.push(self.responsiveGroupTypeSelect.val());
1429				}
1430
1431				locationOptions.class_id = roomOptions.class_id[0];
1432
1433				if (self.bookings.length > 0) {
1434					for (var b = 0; b < self.bookings.length; b++) {
1435						var booking = self.bookings[b];
1436						if (roomOptions.class_id.indexOf(booking.booking_class) === -1) {
1437							roomOptions.class_id.push(booking.booking_class);
1438						}
1439					}
1440				}
1441			} else {
1442				var classtype = 0;
1443				if (self.settings.mode === 0) {
1444					locationOptions.only_with_patron_rooms = 1;
1445					roomOptions.external_only = 1;
1446					classtype = 1;
1447				}
1448				var defaultClass = self.userclasses.find(function (c) {
1449					return c.type == classtype
1450				});
1451				roomOptions.class_id = [defaultClass.id];
1452
1453			}
1454
1455			// CH# 1678
1456			// Had to move this out of 'mode' check, otherwise it's never hit on patron side.
1457			if (self.settings.unmediatedOnly) {
1458				roomOptions.unmediated_only = 1;
1459				locationOptions.only_with_unmediated = 1;
1460			}
1461
1462			if (self.settings.mediatedOnly) {
1463				roomOptions.mediated_only = 1;
1464			}
1465			locationOptions.mode = self.settings.mode;
1466			// END - Set parameters for API calls
1467
1468			Promise.all([
1469				self._getJSON(self.settings.apiServer + '/v2/' + self.settings.client + '/roomsbyclass/' + locs, roomOptions),
1470				self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/opening-hours/' + self.CurrentDateString + '/1'),
1471				self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/locations', locationOptions)
1472			]).then(function (result) {
1473				var roomsByClasses = result[0];
1474				self.openingHours = result[1];
1475				self.locations = result[2];
1476				self.rooms = result[0][roomOptions.class_id[0]];
1477				self.locationsById = {};
1478
1479				if (self.locations.length === 0) {
1480					self._updating = false;
1481					self.settings.selectedLocation = 0;
1482					self.locationSelect.find('input[data-location]:checked').prop('checked', false);
1483					self.responsiveLocationSelect.find('option[data-location]:selected').prop('selected', false);
1484					self._render();
1485					return;
1486				}
1487
1488				self.locations.forEach(function (location) {
1489					self.locationsById[location.id] = location;
1490				});
1491
1492				self.roomById = {};
1493				self.roomByClassById = {};
1494				if (self.groupTypeSelect) {
1495					var selectedClassId = parseInt(self.groupTypeSelect.val(), 10);
1496					var availableClassIds = Object.keys(roomsByClasses).map(function (classId) {
1497						return parseInt(classId, 10);
1498					});
1499
1500					if (!availableClassIds.includes(selectedClassId)) { self._updating = false; self._update(true) }
1501				}
1502
1503				for (var roomClassKey in roomsByClasses) {
1504					var roomClass = roomsByClasses[roomClassKey];
1505					roomClass.forEach(function (room) {
1506						if (typeof self.roomByClassById[room.restrictions.class_id] === "undefined") self.roomByClassById[room.restrictions.class_id] = {};
1507						self.roomByClassById[room.restrictions.class_id][room.id] = room;
1508					});
1509				};
1510
1511				//loop each room and copy its bookings to other effected rooms
1512
1513				self.rooms.forEach(function (room) {
1514					//if(typeof self.roomByClassById[room.restrictions.class_id] === "undefined") self.roomByClassById[room.restrictions.class_id] = {};
1515					//self.roomByClassById[room.restrictions.class_id][room.id] = room;
1516					self.roomById[room.id] = room;
1517					if (room.bookings.length > 0) {
1518						var otherRooms = room.childRooms.concat(room.parentRooms);
1519						self.rooms.forEach(function (subRoom) {
1520							if (otherRooms.indexOf(subRoom.id) !== -1) {
1521								subRoom.bookings = subRoom.bookings.concat(room.bookings.filter(function (booking) {
1522									return parseInt(booking.room_id, 10) == parseInt(room.id, 10);
1523								}));
1524							}
1525						});
1526					}
1527				});
1528
1529				self.bookings.forEach(function (details) {
1530					if (!moment(details.start).isSame(curDate, 'd')) return;
1531					var toUpdate = [details.room.id];
1532					var room = null;
1533
1534					if (parseInt(details.booking_class) > 0) {
1535						room = self.roomByClassById[details.booking_class][details.room.id];
1536					} else {
1537						room = self.roomById[details.room.id];
1538					}
1539
1540					if (details.layout && details.layout.id) {
1541						var layoutId = parseInt(details.layout.id);
1542						details.layout = room.layouts.find(function (layout) {
1543							return parseInt(layout.id) === layoutId;
1544						});
1545					}
1546
1547					details.room = room;
1548					toUpdate = toUpdate.concat(room.parentRooms, room.childRooms);
1549					self._addCartBookingToRooms(details, toUpdate);
1550				});
1551
1552				self._updating = false;
1553				self._hideCover();
1554				self._render();
1555			}).catch(function (err) {
1556				self._updating = false;
1557				self._scheduleHeightPost();
1558				console.log(err);
1559			});
1560		}
1561	});
1562	$.fn[pluginName] = function (options) {
1563		return this.each(function () {
1564			if (!$.data(this, "plugin_" + pluginName)) {
1565				$.data(this, "plugin_" + pluginName, new Plugin(this, options));
1566			}
1567		});
1568	};
1569})(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.