{"version":3,"file":"main-55a4be74.min.js","sources":["../src/js/main/PhotoSwiper.js","../src/js/unishop.js","../src/js/main/HttpService.js","../src/js/main/AdminService.js","../src/js/main/ClientEnvInfo.js","../src/js/main/ObjectUtils.js","../src/js/main/GlobalErrorHandler.js","../src/js/main/AsyncSemaphore.js","../src/js/main/SessionStorageService.js","../src/js/main/Cookies.js","../src/js/main/ModalPopup.js","../src/js/main/FormValidator.js","../src/js/main/LoginRegistration.js","../src/js/business/WarrantyForm.js","../src/js/business/SubmitMeasurementsForm.js","../src/js/business/LinkBusinessMdlService.js","../src/js/business/RetailerSearch.js","../src/js/designer-app/StatusMonitor.js","../src/js/designer-app/DesignerAppLauncher.js","../src/js/designer-app/BrowserEnvironment.js","../src/js/designer-app/DesignerTelemetrySession.js","../src/js/designer-app/DesignerAppService.js","../src/js/designer-app/StartDesignerProcessSteps.js","../src/js/main/WindowResizeMonitor.js","../src/js/designer-app/JQueryPopupDialog.js","../src/js/designer-app/StartDesignerProcess.js","../src/js/designer-app/StartDesignerViewModel.js","../src/js/main.js"],"sourcesContent":["/* global PhotoSwipe, PhotoSwipeUI_Default */\r\n\r\n/**\r\n * The code in the PhotoSwiper class is adapted from https://photoswipe.com/documentation/getting-started.html,\r\n * below the \"How to build an array of slides from a list of links\" heading.\r\n */\r\n\r\n/**\r\n * The swiper will add click event handlers to all gallery selector and fullscreen capable elements\r\n * in the DOM. The event handlers will display the associated image(s) in a fullscreen viewer and\r\n * allow browsing the images.\r\n */\r\nexport class PhotoSwiper {\r\n constructor(galleriesSelector, fullScreenCapableSelector, photoSwipeLoadPromise) {\r\n this.galleriesSelector = galleriesSelector;\r\n this.fullScreenCapableSelector = fullScreenCapableSelector;\r\n /* This is a script load promise which allows us to wait for the script (containing the\r\n * PhotoSwipe library) to load before we're using it. */\r\n this.photoSwipeLoadPromise = photoSwipeLoadPromise;\r\n this.galleryIdAttr = 'data-pswp-uid';\r\n this.galleryHistoryAttr = 'data-history'; // on the element containing the galleriesSelector\r\n this.pswpBgAttr = 'data-pswp-bg';\r\n\r\n this.pswp__bg = document.querySelector('.pswp__bg');\r\n this.galleries = [];\r\n this.fullScreenCapables = [];\r\n this.refresh();\r\n }\r\n\r\n elementExists(element, allElements) {\r\n for (var i = 0, l = allElements.length; i < l; i++) {\r\n if (element === allElements[i])\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * This method allows users to refresh the galleries if the DOM is modified after the initial\r\n * creation of this swiper. */\r\n refresh() {\r\n let galleryIndex = this.galleries.length; // 1-based\r\n const galleries = document.querySelectorAll(this.galleriesSelector);\r\n for (let i = 0, l = galleries.length; i < l; i++) {\r\n if (!this.elementExists(galleries[i], this.galleries)) {\r\n galleries[i].setAttribute(this.galleryIdAttr, galleryIndex + 1);\r\n galleries[i].onclick = event => this.handleGalleryClick(event);\r\n this.galleries.push(galleries[i]);\r\n galleryIndex++;\r\n }\r\n }\r\n\r\n const fullScreenCapables = document.querySelectorAll(this.fullScreenCapableSelector);\r\n for (let i = 0, l = fullScreenCapables.length; i < l; i++) {\r\n if (!this.elementExists(fullScreenCapables[i], this.fullScreenCapables)) {\r\n fullScreenCapables[i].setAttribute(this.galleryIdAttr, galleryIndex + 1);\r\n let clickTarget = null;\r\n if (this.hasClass(fullScreenCapables[i], 'custom-zoom-target')) {\r\n /* We're going to handle clicks associated with the element which has a 'zoom-target' class.\r\n * Since we assume fullScreenCapables[i] is an img, the zoom target cannot be child of fullScreenCapables[i].\r\n * Let's use the next sibling instead (if present). */\r\n clickTarget = fullScreenCapables[i].nextElementSibling;\r\n } else {\r\n clickTarget = fullScreenCapables[i];\r\n }\r\n if (clickTarget) {\r\n clickTarget.onclick = event => this.handleFullScreenCapableClick(event);\r\n } else {\r\n console.log('Custom zoom target not found');\r\n }\r\n this.fullScreenCapables.push(galleries[i]);\r\n galleryIndex++;\r\n }\r\n }\r\n\r\n if (this.galleries.length > 0 || fullScreenCapables.length > 0) {\r\n // Parse URL and open photo swiper if it contains #&pid=3&gid=1\r\n const hashData = this.parseUrlHashValue();\r\n if (hashData.pid && (typeof hashData.gid != 'undefined')) {\r\n /* TODO: This code will break if the swiper was updated after the initial creation because\r\n * there might be galleries after the fullScreenCapables. */\r\n const gid = hashData.gid - 1;\r\n const isGallery = gid < this.galleries.length;\r\n const element = isGallery ? this.galleries[gid] : fullScreenCapables[gid - this.galleries.length];\r\n /*await*/ this.openPhotoSwipe(hashData.pid, element, true, true, isGallery);\r\n }\r\n }\r\n }\r\n\r\n // Helper: find the nearest parent element of el\r\n closest(el, fn) {\r\n return el && (fn(el) ? el : this.closest(el.parentNode, fn));\r\n }\r\n\r\n // Helper: returns true if cls is defined on the class attribute of element\r\n hasClass(element, cls) {\r\n return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;\r\n }\r\n\r\n // Helper: parse picture index and gallery index from the URL's hash value (#&pid=1&gid=2)\r\n parseUrlHashValue() {\r\n const hash = window.location.hash.substring(1);\r\n const params = {};\r\n if (hash.length < 5) {\r\n return params;\r\n }\r\n const vars = hash.split('&');\r\n for (let i = 0; i < vars.length; i++) {\r\n if (vars[i]) {\r\n var pair = vars[i].split('=');\r\n if (pair.length >= 2) {\r\n params[pair[0]] = pair[1];\r\n }\r\n }\r\n }\r\n if (params.gid) {\r\n params.gid = parseInt(params.gid, 10);\r\n }\r\n return params;\r\n }\r\n\r\n /**\r\n * If isGallery is true, then galleryParentElement should be the immediate parent of the list of\r\n * elements containing the 'gallery-item' class.\r\n * If isGallery is false, then galleryParentElement should be the img element to display fullscreen.\r\n */\r\n createItemObjects(galleryParentElement, isGallery) {\r\n const galleryItems = isGallery\r\n ? $(galleryParentElement).find('.gallery-item:not(.isotope-hidden)').get()\r\n : [galleryParentElement];\r\n const itemObjects = [];\r\n for (let i = 0; i < galleryItems.length; i++) {\r\n const galleryItem = galleryItems[i]; //
\r\n\r\n // include only element nodes\r\n if (galleryItem.nodeType !== 1) {\r\n continue;\r\n }\r\n\r\n const linkEl = isGallery\r\n ? galleryItem.querySelector('a') // element\r\n : galleryItem;\r\n\r\n if (!linkEl) {\r\n // If we're in a gallery, but no 'a' tag was found under the gallery item, then assume this\r\n // item should not be included in the array of elements for full screen view.\r\n continue;\r\n }\r\n\r\n // Create item object\r\n let itemObject;\r\n if ($(linkEl).data('type') == 'video') {\r\n itemObject = {\r\n html: $(linkEl).data('video')\r\n };\r\n } else {\r\n const size = linkEl.getAttribute('data-size').split('x');\r\n const href = isGallery ? 'href' : 'data-href';\r\n itemObject = {\r\n src: linkEl.getAttribute(href),\r\n w: parseInt(size[0], 10),\r\n h: parseInt(size[1], 10)\r\n };\r\n }\r\n\r\n if (galleryItem.children.length > 1) {\r\n //
content\r\n itemObject.title = $(galleryItem).find('.caption').html();\r\n }\r\n\r\n let image = null;\r\n if (!isGallery) {\r\n image = linkEl;\r\n } else if (linkEl.children.length > 0) {\r\n // Use the child 's src as the source to be displayed while loading the large fullscreen image\r\n image = linkEl.children[0];\r\n }\r\n if (image !== null) {\r\n itemObject.msrc = image.currentSrc || image.getAttribute('src');\r\n }\r\n\r\n itemObject.el = galleryItem; // save link to element for getThumbBoundsFn\r\n itemObjects.push(itemObject);\r\n }\r\n return itemObjects;\r\n }\r\n\r\n /**\r\n * Main Photoswipe kick-off function (on click or url hash value).\r\n * @param {any} index index of the gallery item to display.\r\n * @param {any} galleryParentElement If isGallery is true, then galleryParentElement should be\r\n * the immediate parent of the list of elements containing the 'gallery-item' class. If isGallery\r\n * is false, then galleryParentElement should be the img element to display fullscreen.\r\n * @param {any} isGallery If true, then we are displaying a list of photos/images. Otherwise a\r\n * single photo/image is displayed.\r\n */\r\n async openPhotoSwipe(index, galleryParentElement, disableAnimation, fromURL, isGallery, background) {\r\n if (this.pswp__bg) {\r\n if (background) {\r\n this.pswp__bg.style.background = background;\r\n } else {\r\n this.pswp__bg.style.background = null;\r\n }\r\n }\r\n\r\n const items = this.createItemObjects(galleryParentElement, isGallery);\r\n const historyAttr = galleryParentElement.getAttribute(this.galleryHistoryAttr);\r\n const allowHistory = (typeof historyAttr === 'undefined') || historyAttr === null ||\r\n historyAttr === 'true' || historyAttr === '1';\r\n const options = {\r\n closeOnScroll: false,\r\n // Define gallery UID (for the URL of the gallery items)\r\n galleryUID: galleryParentElement.getAttribute(this.galleryIdAttr),\r\n history: allowHistory,\r\n getThumbBoundsFn: index => {\r\n /* Should return the rect of the image which is maximixed for the purpose of animation.\r\n * See Options -> getThumbBoundsFn section of documentation for more info. */\r\n const image = isGallery ? items[index].el.getElementsByTagName('img')[0] : items[index].el;\r\n if (image) {\r\n const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;\r\n const rect = image.getBoundingClientRect();\r\n return {\r\n x: rect.left,\r\n y: rect.top + pageYScroll,\r\n w: rect.width\r\n };\r\n }\r\n }\r\n };\r\n\r\n // PhotoSwipe opened from URL\r\n if (fromURL) {\r\n if (!allowHistory) {\r\n return;\r\n }\r\n if (options.galleryPIDs) {\r\n // parse real index when custom PIDs are used\r\n // http://photoswipe.com/documentation/faq.html#custom-pid-in-url\r\n for (let j = 0; j < items.length; j++) {\r\n if (items[j].pid == index) {\r\n options.index = j;\r\n break;\r\n }\r\n }\r\n } else {\r\n // in URL indexes are 1-based\r\n options.index = parseInt(index, 10) - 1;\r\n }\r\n } else {\r\n options.index = parseInt(index, 10);\r\n }\r\n\r\n // Exit if index not found\r\n if (isNaN(options.index)) {\r\n return;\r\n }\r\n\r\n if (disableAnimation) {\r\n options.showAnimationDuration = 0;\r\n }\r\n\r\n // Pass data to PhotoSwipe and initialize it\r\n const pswpElement = document.querySelectorAll('.pswp')[0];\r\n if (!pswpElement) {\r\n throw Error('The Photoswipe elements have not been added to the DOM');\r\n }\r\n\r\n /* If we have a script load promise, and the PhotoSwipe class is undefined still, then use the\r\n * promise to wait for the script (containing the PhotoSwipe library) to load. */\r\n const isPromise = this.photoSwipeLoadPromise instanceof Promise;\r\n const photoSwipeType = typeof PhotoSwipe;\r\n if (photoSwipeType === 'undefined' && isPromise) {\r\n await this.photoSwipeLoadPromise;\r\n }\r\n\r\n const gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);\r\n gallery.init();\r\n\r\n gallery.listen('beforeChange', function() {\r\n $('.pswp__video').removeClass('active');\r\n $('.pswp__video').each(function() {\r\n if (!$(this).hasClass('active')) {\r\n $(this).attr('src', $(this).attr('src'));\r\n }\r\n });\r\n });\r\n\r\n gallery.listen('close', function() {\r\n $('.pswp__video').each(function() {\r\n $(this).attr('src', $(this).attr('src'));\r\n });\r\n });\r\n }\r\n\r\n /* 11/24/18: I've added code to allow a custom element to be clicked instead of the photo.\r\n * This is needed when using the gallery to display product finishes for selection by the user.\r\n * A click on the photo should select the finish instead of showing the fullscreen image.\r\n * If the element referencing this click handler (e.g. \".gallery-wrapper\") has the\r\n * \".custom-zoom-target\" class, then child elements with the \".zoom-target\" class can be\r\n * clicked instead. */\r\n async handleGalleryClick(event) {\r\n event = event || window.event;\r\n // eTarget is the ultimate clicked element.\r\n const eTarget = event.target || event.srcElement;\r\n if (this.hasClass(eTarget, 'do-not-zoom')) {\r\n return; // Allow default handling and return\r\n }\r\n event.preventDefault ? event.preventDefault() : event.returnValue = false;\r\n // e.currentTarget is the element this event handler has been attached to (e.g. the .gallery-wrapper element).\r\n const useCustomClick = this.hasClass(event.currentTarget, 'custom-zoom-target');\r\n let isCustomClick = false;\r\n if (useCustomClick) {\r\n const zoomTarget = this.closest(eTarget, el => this.hasClass(el, 'zoom-target'));\r\n if (zoomTarget) {\r\n isCustomClick = true;\r\n }\r\n }\r\n let clickedGalleryItem = null;\r\n if (!useCustomClick || isCustomClick) {\r\n clickedGalleryItem = this.closest(eTarget, el => this.hasClass(el, 'gallery-item'));\r\n }\r\n if (!clickedGalleryItem) {\r\n return;\r\n }\r\n\r\n /* Find index of clicked item by looping through all gallery items. Alternatively, you might\r\n * define index via data-attribute. */\r\n const clickedGallery = clickedGalleryItem.closest(this.galleriesSelector);\r\n const galleryItems = $(clickedGallery).find('.gallery-item:not(.isotope-hidden)').get();\r\n let nodeIndex = 0;\r\n let clickedIndex;\r\n for (let i = 0; i < galleryItems.length; i++) {\r\n if (galleryItems[i].nodeType === 1) {\r\n if (galleryItems[i] === clickedGalleryItem) {\r\n clickedIndex = nodeIndex;\r\n break;\r\n }\r\n nodeIndex++;\r\n }\r\n }\r\n\r\n if (clickedIndex >= 0) {\r\n const clickedImage = this.closest(eTarget, el => el.nodeName == 'IMG');\r\n const background = clickedImage ? clickedImage.getAttribute(this.pswpBgAttr) : null;\r\n await this.openPhotoSwipe(clickedIndex, clickedGallery, false, false, true, background);\r\n }\r\n return;\r\n }\r\n\r\n async handleFullScreenCapableClick(event) {\r\n event = event || window.event;\r\n event.preventDefault ? event.preventDefault() : event.returnValue = false;\r\n // eTarget is the ultimate clicked element.\r\n const eTarget = event.target || event.srcElement;\r\n\r\n /* We need to handle 2 scenarios:\r\n * 1. No custom zoom target has been defined --> The click is on an img.fullscreen-capable\r\n * 2. A custom zoom target has been defined --> The click is on the img's next sibling element,\r\n * in which case the 'custom-zoom-target' is defined on the image, and the 'zoom-target' is\r\n * defined on the sibling. The click will be on the sibling.\r\n */\r\n // e.currentTarget is the element this event handler has been attached to (e.g. the .fullscreen-capable or .zoom-target element).\r\n let clickedImage = null;\r\n const useCustomClick = this.hasClass(event.currentTarget, 'zoom-target');\r\n if (useCustomClick) {\r\n clickedImage = event.currentTarget.previousElementSibling;\r\n if (clickedImage !== null && !this.hasClass(clickedImage, 'custom-zoom-target')) {\r\n clickedImage = null;\r\n }\r\n } else {\r\n clickedImage = this.closest(eTarget, el => this.hasClass(el, 'fullscreen-capable'));\r\n }\r\n if (!clickedImage) {\r\n return;\r\n }\r\n\r\n const background = clickedImage.getAttribute(this.pswpBgAttr);\r\n\r\n await this.openPhotoSwipe(0, clickedImage, false, false, false, background);\r\n return false;\r\n }\r\n}","/*\r\n * Unishop | Universal E-Commerce Template\r\n * Copyright 2018 rokaux\r\n * Theme Custom Scripts\r\n */\r\n\r\n/* global jQuery, iziToast, noUiSlider, globalContent */\r\n\r\nimport { PhotoSwiper } from './main/PhotoSwiper.js';\r\n\r\n/** Handles app specific navigation using the Owl Carousel. */\r\nclass OwlCarouselMgr {\r\n constructor($carousel, options) {\r\n if (!$carousel || !$carousel.length || $carousel.length == 0) {\r\n throw Error('Unassigned carousel parameter or no items in the carousel');\r\n }\r\n this.$carousel = $carousel;\r\n this.$carouselParent = this.$carousel.parent();\r\n this.$thumbnails = this.$carouselParent.find('.product-thumbnails');\r\n this.$thumbnailItems = this.$thumbnails.children('li');\r\n\r\n /* The Owl Carousel library has a bug in that the value of the URLhashListener option is not\r\n * respected (no code in the library checks the value). To work around this without making\r\n * changes to the library itself, we add code here instead. */\r\n this.useUrlNavigation = options.URLhashListener;\r\n if (!this.useUrlNavigation) {\r\n // Add click handlers instead of relying on the href attribute and link navigation:\r\n const anchors = this.$thumbnails.find('a');\r\n anchors.on('click', (e) => {\r\n const anchor = e.currentTarget;\r\n const $item = $(anchor).parent();\r\n\r\n /* This new code takes into account the case that there might be non-displayed thumbnails\r\n * which should not be counted when finding the index. See the ProductPreviewer class for\r\n * more on this. 7/11/2023. */\r\n const targetThumbnail = $item[0];\r\n let i = 0;\r\n let index = -1;\r\n this.$thumbnailItems.each((ndx, thumb) => {\r\n if (!thumb.classList.contains('d-none')) {\r\n if (thumb === targetThumbnail) {\r\n index = i;\r\n }\r\n i++;\r\n }\r\n });\r\n this.$carousel.trigger('to.owl.carousel', [index]);\r\n });\r\n }\r\n\r\n options.onTranslate = e => this.updateActiveItem(e);\r\n $carousel.owlCarousel(options);\r\n }\r\n\r\n /**\r\n * Updates the 'active' css class on thumbnail items and carousel items. Note this method used to\r\n * be named \"activeHash\".\r\n */\r\n updateActiveItem(e) {\r\n //console.log(`Reached updateActiveItem, type=${e.type}, target's classes: ${e.target.className}`);\r\n const hashKey = this.useUrlNavigation ? 'data-hash' : 'data-hiddenhash';\r\n const hrefAttr = this.useUrlNavigation ? 'href' : 'data-href';\r\n\r\n // 1. Get the hash of the carousel item:\r\n const i = e.item.index;\r\n const $owlItems = this.$carousel.find('.owl-item');\r\n const $activeOwlItem = $owlItems.eq(i);\r\n const $hashElement = $activeOwlItem.find(`[${hashKey}]`);\r\n const activeHash = $hashElement.attr(`${hashKey}`);\r\n\r\n if (this.$thumbnails.length) {\r\n // 2. Remove 'active' class from all product thumbnail items:\r\n this.$thumbnailItems.removeClass('active');\r\n\r\n // 3. Set 'active' class on the parent of the anchor element linking to the carousel item\r\n const $activeAnchor = this.$thumbnails.find(`[${hrefAttr}=\"#${activeHash}\"]`);\r\n $activeAnchor.parent().addClass('active');\r\n }\r\n\r\n // 4. Remove 'active' class from all gallery items:\r\n const $allGalleryItems = this.$carousel.find('.gallery-item');\r\n $allGalleryItems.removeClass('active');\r\n\r\n // 5. Set 'active' class on the parent of all elements with a data-hash attribute equal to hash.\r\n // This parent should a the gallery item element.\r\n const $allActiveItems = this.$carousel.find(`[${hashKey}=\"${activeHash}\"]`);\r\n $allActiveItems.parent().addClass('active');\r\n }\r\n}\r\n\r\nexport class Unishop {\r\n constructor(options) {\r\n this.options = options || {};\r\n window.addEventListener('DOMContentLoaded', (event) => {\r\n this.setupOnDOMContentLoaded();\r\n setTimeout(() => this.setupDelayedFeatures(), 2000);\r\n });\r\n }\r\n\r\n setupOnDOMContentLoaded() {\r\n const self = this;\r\n\r\n if (this.options.useStickyNavBar) {\r\n // Check if Page Scrollbar is visible\r\n //------------------------------------------------------------------------------\r\n var hasScrollbar = function () {\r\n // The Modern solution\r\n if (typeof window.innerWidth === 'number') {\r\n return window.innerWidth > document.documentElement.clientWidth;\r\n }\r\n\r\n // rootElem for quirksmode\r\n var rootElem = document.documentElement || document.body;\r\n\r\n // Check overflow style property on body for fauxscrollbars\r\n var overflowStyle;\r\n\r\n if (typeof rootElem.currentStyle !== 'undefined') {\r\n overflowStyle = rootElem.currentStyle.overflow;\r\n }\r\n\r\n overflowStyle = overflowStyle || window.getComputedStyle(rootElem, '').overflow;\r\n\r\n // Also need to check the Y axis overflow\r\n var overflowYStyle;\r\n\r\n if (typeof rootElem.currentStyle !== 'undefined') {\r\n overflowYStyle = rootElem.currentStyle.overflowY;\r\n }\r\n\r\n overflowYStyle = overflowYStyle || window.getComputedStyle(rootElem, '').overflowY;\r\n\r\n var contentOverflows = rootElem.scrollHeight > rootElem.clientHeight;\r\n var overflowShown = /^(visible|auto)$/.test(overflowStyle) || /^(visible|auto)$/.test(overflowYStyle);\r\n var alwaysShowScroll = overflowStyle === 'scroll' || overflowYStyle === 'scroll';\r\n\r\n return (contentOverflows && overflowShown) || (alwaysShowScroll);\r\n };\r\n if (hasScrollbar()) {\r\n /* The only place we take advantage of the hasScrollbar class is while displaying popups, and\r\n * we have enabled the sticky navbar feature. When the popup is opened the element's\r\n * scrollbar is removed, and to avoid the sticky navbar shifting to the right, its width is\r\n * reduced by the width of the scrollbar. Tor, 9/23/2021. */\r\n $('body').addClass('hasScrollbar');\r\n }\r\n }\r\n\r\n\r\n // Sticky Navbar\r\n //------------------------------------------------------------------------------\r\n if (this.options.useStickyNavBar) {\r\n function stickyHeader() {\r\n var $body = $('body');\r\n var $navbar = $('.navbar-sticky');\r\n var $topbarH = $('.topbar').outerHeight();\r\n if (typeof $topbarH === 'undefined') { // then Topbar is not used...\r\n $topbarH = 0;\r\n }\r\n var $navbarH = $navbar.outerHeight();\r\n if ($navbar.length) {\r\n $(window).on('scroll', function () {\r\n if ($(this).scrollTop() > $topbarH) {\r\n $navbar.addClass('navbar-stuck');\r\n if (!$navbar.hasClass('navbar-ghost')) {\r\n $body.css('padding-top', $navbarH);\r\n }\r\n } else {\r\n $navbar.removeClass('navbar-stuck');\r\n $body.css('padding-top', 0);\r\n }\r\n });\r\n }\r\n }\r\n stickyHeader();\r\n }\r\n\r\n\r\n // Language / Currency Switcher\r\n //---------------------------------------------------------\r\n this.setupLanguageCurrencySwitcher();\r\n\r\n // Off-Canvas Container\r\n //---------------------------------------------------------\r\n $('[data-toggle=\"offcanvas\"]').on('click', self.offcanvasOpen);\r\n $('.site-backdrop').on('click', self.offcanvasClose);\r\n\r\n\r\n // Off-Canvas Menu\r\n //---------------------------------------------------------\r\n var menuInitHeight = $('.offcanvas-menu .menu').height();\r\n var backBtnText = globalContent.backBtnLabel;\r\n var subMenu = $('.offcanvas-menu .offcanvas-submenu');\r\n\r\n subMenu.each(function () {\r\n $(this).prepend('
  • ' + backBtnText + '
  • ');\r\n });\r\n\r\n var hasChildLink = $('.has-children .sub-menu-toggle');\r\n var backBtn = $('.offcanvas-menu .offcanvas-submenu .back-btn');\r\n\r\n backBtn.on('click', function (e) {\r\n var self = this,\r\n parent = $(self).parent(),\r\n siblingParent = $(self).parent().parent().siblings().parent(),\r\n menu = $(self).parents('.menu');\r\n\r\n parent.removeClass('in-view');\r\n siblingParent.removeClass('off-view');\r\n if (siblingParent.attr('class') === 'menu') {\r\n menu.css('height', menuInitHeight);\r\n } else {\r\n menu.css('height', siblingParent.height());\r\n }\r\n e.preventDefault();\r\n });\r\n\r\n hasChildLink.on('click', function (e) {\r\n var self = this,\r\n parent = $(self).parent().parent().parent(),\r\n menu = $(self).parents('.menu');\r\n\r\n parent.addClass('off-view');\r\n $(self).parent().parent().find('> .offcanvas-submenu').addClass('in-view');\r\n menu.css('height', $(self).parent().parent().find('> .offcanvas-submenu').height());\r\n\r\n e.preventDefault();\r\n return false;\r\n });\r\n\r\n\r\n if (this.options.useSmoothScrollToElement) {\r\n // Smooth scroll to element\r\n //---------------------------------------------------------\r\n /* If an anchor tag has the scroll-to class, then the code below will use the velocity library\r\n * to perform a smooth scroll to the target element. As of 9/23/2021 this feature is only used\r\n * on home-/landing-/meetus-pages with image sliders where the down-arrow button is enabled.\r\n * Hence, the useSmoothScrollToElement option should be set on these kinds of pages. */\r\n $(document).on('click', '.scroll-to', function (event) {\r\n var target = $(this).attr('href');\r\n if ('#' === target) {\r\n return false;\r\n }\r\n\r\n var $target = $(target);\r\n if ($target.length > 0) {\r\n var $elemOffsetTop = $target.data('offset-top') || 70;\r\n $('html').velocity('scroll', {\r\n offset: $(this.hash).offset().top - $elemOffsetTop,\r\n duration: 1000,\r\n easing: 'easeOutExpo',\r\n mobileHA: false\r\n });\r\n }\r\n event.preventDefault();\r\n });\r\n }\r\n\r\n\r\n // Isotope Grid / Filters (Gallery) - see https://isotope.metafizzy.co\r\n //------------------------------------------------------------------------------\r\n if (this.options.useIsotopeGrid) {\r\n // Used on the product category page. Tor, 9/23/2021.\r\n // Isotope Grid\r\n var isotopeGrid = $('.isotope-grid');\r\n if (isotopeGrid.length) {\r\n var $grid = isotopeGrid.imagesLoaded(function () {\r\n $grid.isotope({\r\n itemSelector: '.grid-item',\r\n transitionDuration: '0.7s',\r\n masonry: {\r\n columnWidth: '.grid-sizer',\r\n gutter: '.gutter-sizer'\r\n }\r\n });\r\n });\r\n }\r\n\r\n // Filtering\r\n if ($('.filter-grid').length > 0) {\r\n var $filterGrid = $('.filter-grid');\r\n $('.nav-pills').on('click', 'a', function (e) {\r\n e.preventDefault();\r\n $('.nav-pills a').removeClass('active');\r\n $(this).addClass('active');\r\n var $filterValue = $(this).attr('data-filter');\r\n $filterGrid.isotope({ filter: $filterValue });\r\n });\r\n }\r\n }\r\n\r\n\r\n // Gallery (Photoswipe)\r\n //------------------------------------------------------------------------------\r\n if (this.options.usePhotoSwiper) {\r\n this.acquirePhotoSwiper();\r\n }\r\n\r\n\r\n // Product Gallery\r\n //------------------------------------------------------------------------------\r\n if (this.options.useProductImageCarousel) {\r\n // Get an array of all elements with the product-carousel class set:\r\n var $productCarousel = $('.product-carousel');\r\n // Initialize an owl carousel for each element in the array:\r\n if ($productCarousel.length) {\r\n // Carousel init\r\n /*\r\n $productCarousel.owlCarousel({\r\n items: 1,\r\n loop: false,\r\n dots: false,\r\n URLhashListener: true,\r\n startPosition: 'URLHash',\r\n onTranslate: activeHash\r\n });\r\n */\r\n const options = {\r\n items: 1,\r\n loop: false,\r\n dots: false,\r\n };\r\n $productCarousel.each(function () {\r\n const mgr = new OwlCarouselMgr($(this), options);\r\n });\r\n }\r\n }\r\n\r\n\r\n //#region Disabled features:\r\n\r\n /* Search feature is currently not used on our site. Tor, 9/23/2021.\r\n // Site Search\r\n //---------------------------------------------------------\r\n function searchActions(openTrigger, closeTrigger, clearTrigger, target) {\r\n $(openTrigger).on('click', function () {\r\n $(target).addClass('search-visible');\r\n setTimeout(function () {\r\n $(target + ' > input').focus();\r\n }, 200);\r\n });\r\n $(closeTrigger).on('click', function () {\r\n $(target).removeClass('search-visible');\r\n });\r\n $(clearTrigger).on('click', function () {\r\n $(target + ' > input').val('');\r\n setTimeout(function () {\r\n $(target + ' > input').focus();\r\n }, 200);\r\n });\r\n }\r\n searchActions('.toolbar .tools .search', '.close-search', '.clear-search', '.site-search');\r\n */\r\n\r\n // Form Validation\r\n //------------------------------------------------------------------------------\r\n /* Tor, 7/18/19: We choose instead to do this manually for each form\r\n window.addEventListener('load', function() {\r\n // Fetch all the forms we want to apply custom Bootstrap validation styles to\r\n var forms = document.getElementsByClassName('needs-validation');\r\n // Loop over them and prevent submission\r\n var validation = Array.prototype.filter.call(forms, function(form) {\r\n form.addEventListener('submit', function(event) {\r\n if (form.checkValidity() === false) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }\r\n form.classList.add('was-validated');\r\n }, false);\r\n });\r\n }, false);\r\n */\r\n\r\n\r\n // Filter List Groups\r\n //---------------------------------------------------------\r\n /* I don't know anywhere this is used. Disabled for now. Tor, 9/23/2021.\r\n // var targetList = $();\r\n function filterList(trigger) {\r\n trigger.each(function () {\r\n var self = $(this);\r\n var target = self.data('filter-list');\r\n var search = self.find('input[type=text]');\r\n var filters = self.find('input[type=radio]');\r\n var list = $(target).find('.list-group-item');\r\n\r\n // Search\r\n search.keyup(function () {\r\n var searchQuery = search.val();\r\n list.each(function () {\r\n var text = $(this).text().toLowerCase();\r\n (text.indexOf(searchQuery.toLowerCase()) == 0) ? $(this).show() : $(this).hide();\r\n });\r\n });\r\n\r\n // Filters\r\n filters.on('click', function () {\r\n var targetItem = $(this).val();\r\n if (targetItem !== 'all') {\r\n list.hide();\r\n $('[data-filter-item=' + targetItem + ']').show();\r\n } else {\r\n list.show();\r\n }\r\n\r\n });\r\n });\r\n }\r\n filterList($('[data-filter-list]'));\r\n */\r\n\r\n // Countdown Function\r\n //------------------------------------------------------------------------------\r\n /* 9/3/2021, Tor: Countdown is currently not in use and has been commented out to reduce js download size.\r\n function countDownFunc(items, trigger) {\r\n items.each(function () {\r\n var countDown = $(this);\r\n var dateTime = $(this).data('date-time');\r\n\r\n var countDownTrigger = (trigger) ? trigger : countDown;\r\n countDownTrigger.downCount({\r\n date: dateTime,\r\n offset: +10\r\n });\r\n });\r\n }\r\n countDownFunc($('.countdown'));\r\n */\r\n\r\n // Toast Notifications\r\n //------------------------------------------------------------------------------\r\n /* I don't know anywhere this is used. Disabled for now. Tor, 9/23/2021.\r\n $('[data-toast]').on('click', function () {\r\n var self = $(this);\r\n var $type = self.data('toast-type');\r\n var $icon = self.data('toast-icon');\r\n var $position = self.data('toast-position');\r\n var $title = self.data('toast-title');\r\n var $message = self.data('toast-message');\r\n var toastOptions = '';\r\n\r\n switch ($position) {\r\n case 'topRight':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'topRight',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInLeft',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n case 'bottomRight':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'bottomRight',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInLeft',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n case 'topLeft':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'topLeft',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInRight',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n case 'bottomLeft':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'bottomLeft',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInRight',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n case 'topCenter':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'topCenter',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInDown',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n case 'bottomCenter':\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'bottomCenter',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInUp',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n break;\r\n default:\r\n toastOptions = {\r\n class: 'iziToast-' + $type || '',\r\n title: $title || 'Title',\r\n message: $message || 'toast message',\r\n animateInside: false,\r\n position: 'topRight',\r\n progressBar: false,\r\n icon: $icon,\r\n timeout: 3200,\r\n transitionIn: 'fadeInLeft',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n }\r\n\r\n iziToast.show(toastOptions);\r\n });\r\n\r\n // Launch default BS Toasts on click // Added v3.1\r\n $('[data-toggle=\"toast\"]').on('click', function () {\r\n var target = '#' + $(this).data('toast-id');\r\n $(target).toast('show');\r\n });\r\n */\r\n\r\n // Wishlist Button\r\n //------------------------------------------------------------------------------\r\n /* Not currently used. Disabled for now. Tor, 9/23/2021.\r\n $('.btn-wishlist').on('click', function () {\r\n var iteration = $(this).data('iteration') || 1;\r\n var toastOptions = {\r\n title: globalContent.wishListPopupTitle,\r\n animateInside: false,\r\n position: 'topRight',\r\n progressBar: false,\r\n timeout: 3200,\r\n transitionIn: 'fadeInLeft',\r\n transitionOut: 'fadeOut',\r\n transitionInMobile: 'fadeIn',\r\n transitionOutMobile: 'fadeOut'\r\n };\r\n\r\n switch (iteration) {\r\n case 1:\r\n $(this).addClass('active');\r\n toastOptions.class = 'iziToast-info';\r\n toastOptions.message = globalContent.wishListPopupContentAdded;\r\n toastOptions.icon = 'icon-bell';\r\n break;\r\n\r\n case 2:\r\n $(this).removeClass('active');\r\n toastOptions.class = 'iziToast-danger';\r\n toastOptions.message = globalContent.wishListPopupContentRemoved;\r\n toastOptions.icon = 'icon-ban';\r\n break;\r\n }\r\n\r\n iziToast.show(toastOptions);\r\n\r\n iteration++;\r\n if (iteration > 2) iteration = 1;\r\n $(this).data('iteration', iteration);\r\n });\r\n */\r\n\r\n // Shop Categories Widget\r\n //------------------------------------------------------------------------------\r\n /* Disabled for now. Tor, 9/23/2021.\r\n // Used on the product category page if we enable sidebar filtering\r\n var categoryToggle = $('.widget-categories .has-children > a');\r\n\r\n function closeCategorySubmenu() {\r\n categoryToggle.parent().removeClass('expanded');\r\n }\r\n categoryToggle.on('click', function (e) {\r\n if ($(e.target).parent().is('.expanded')) {\r\n closeCategorySubmenu();\r\n } else {\r\n closeCategorySubmenu();\r\n $(this).parent().addClass('expanded');\r\n }\r\n });\r\n */\r\n\r\n\r\n // Range Slider\r\n //------------------------------------------------------------------------------\r\n /* 9/3/2021, Tor: ui-range-slider/noUiSlider is currently not in use and has been commented out to reduce js download size.\r\n var rangeSlider = document.querySelector('.ui-range-slider');\r\n if (typeof rangeSlider !== 'undefined' && rangeSlider !== null) {\r\n var dataStartMin = parseInt(rangeSlider.parentNode.getAttribute('data-start-min'), 10);\r\n var dataStartMax = parseInt(rangeSlider.parentNode.getAttribute('data-start-max'), 10);\r\n var dataMin = parseInt(rangeSlider.parentNode.getAttribute('data-min'), 10);\r\n var dataMax = parseInt(rangeSlider.parentNode.getAttribute('data-max'), 10);\r\n var dataStep = parseInt(rangeSlider.parentNode.getAttribute('data-step'), 10);\r\n var valueMin = document.querySelector('.ui-range-value-min span');\r\n var valueMax = document.querySelector('.ui-range-value-max span');\r\n var valueMinInput = document.querySelector('.ui-range-value-min input');\r\n var valueMaxInput = document.querySelector('.ui-range-value-max input');\r\n noUiSlider.create(rangeSlider, {\r\n start: [dataStartMin, dataStartMax],\r\n connect: true,\r\n step: dataStep,\r\n range: {\r\n 'min': dataMin,\r\n 'max': dataMax\r\n }\r\n });\r\n rangeSlider.noUiSlider.on('update', function (values, handle) {\r\n var value = values[handle];\r\n if (handle) {\r\n valueMax.innerHTML = Math.round(value);\r\n valueMaxInput.value = Math.round(value);\r\n } else {\r\n valueMin.innerHTML = Math.round(value);\r\n valueMinInput.value = Math.round(value);\r\n }\r\n });\r\n }\r\n */\r\n\r\n\r\n // Interactive Credit Card\r\n //------------------------------------------------------------------------------\r\n /* Not currently used. Disabled for now. Tor, 9/23/2021.\r\n // The card library is created by Jesse Pollak and can be found here: https://github.com/jessepollak/card.\r\n var $creditCard = $('.interactive-credit-card');\r\n if ($creditCard.length) {\r\n $creditCard.card({\r\n form: '.interactive-credit-card',\r\n container: '.card-wrapper'\r\n });\r\n }\r\n */\r\n\r\n // Google Maps API\r\n //------------------------------------------------------------------------------\r\n /* 9/3/2021, Tor: google-map/gmap3 is currently not in use and has been commented out to reduce js download size.\r\n var $googleMap = $('.google-map');\r\n if ($googleMap.length) {\r\n $googleMap.each(function () {\r\n var mapHeight = $(this).data('height'),\r\n address = $(this).data('address'),\r\n zoom = $(this).data('zoom'),\r\n controls = $(this).data('disable-controls'),\r\n scrollwheel = $(this).data('scrollwheel'),\r\n marker = $(this).data('marker'),\r\n markerTitle = $(this).data('marker-title'),\r\n styles = $(this).data('styles');\r\n $(this).height(mapHeight);\r\n $(this).gmap3({\r\n marker: {\r\n address: address,\r\n data: markerTitle,\r\n options: {\r\n icon: marker\r\n },\r\n events: {\r\n mouseover: function (marker, event, context) {\r\n var map = $(this).gmap3('get'),\r\n infowindow = $(this).gmap3({ get: { name: 'infowindow' } });\r\n if (infowindow) {\r\n infowindow.open(map, marker);\r\n infowindow.setContent(context.data);\r\n } else {\r\n $(this).gmap3({\r\n infowindow: {\r\n anchor: marker,\r\n options: { content: context.data }\r\n }\r\n });\r\n }\r\n },\r\n mouseout: function () {\r\n var infowindow = $(this).gmap3({ get: { name: 'infowindow' } });\r\n if (infowindow) {\r\n infowindow.close();\r\n }\r\n }\r\n }\r\n },\r\n map: {\r\n options: {\r\n zoom: zoom,\r\n disableDefaultUI: controls,\r\n scrollwheel: scrollwheel,\r\n styles: styles\r\n }\r\n }\r\n });\r\n });\r\n }\r\n */\r\n\r\n //#endregion\r\n\r\n } // End of setup function\r\n\r\n setupDelayedFeatures() {\r\n // Disable default link behavior for dummy links that have href='#'\r\n //------------------------------------------------------------------------------\r\n var $emptyLink = $('a[href=\"#\"]');\r\n $emptyLink.on('click', function (e) {\r\n /* Normally, when you click on a link with href=\"#\", the # will be appended to the url.\r\n * Calling preventDefault() stops this default behavior. Tor, 9/23/2021 */\r\n e.preventDefault();\r\n });\r\n\r\n\r\n // Animated Scroll to Top Button\r\n //------------------------------------------------------------------------------\r\n var $scrollTop = $('.scroll-btn.to-top');\r\n if ($scrollTop.length > 0) {\r\n $(window).on('scroll', function () {\r\n if ($(this).scrollTop() > 600) {\r\n $scrollTop.addClass('visible');\r\n } else {\r\n $scrollTop.removeClass('visible');\r\n }\r\n });\r\n $scrollTop.on('click', function (e) {\r\n e.preventDefault();\r\n $('html').velocity('scroll', {\r\n offset: 0,\r\n duration: 1200,\r\n easing: 'easeOutExpo',\r\n mobileHA: false\r\n });\r\n });\r\n }\r\n\r\n\r\n // Tooltips\r\n //------------------------------------------------------------------------------\r\n $('[data-toggle=\"tooltip\"]').tooltip();\r\n\r\n\r\n // Popovers\r\n //------------------------------------------------------------------------------\r\n $('[data-toggle=\"popover\"]').popover();\r\n\r\n\r\n\r\n }\r\n\r\n acquirePhotoSwiper() {\r\n // The swiper will add click event handlers to all wrapper and capable elements in the DOM.\r\n // The event handlers will display the associated image(s) in a fullscreen viewer and allow\r\n // browsing the images. Tor, 9/23/2021.\r\n if (!this.photoSwiper) {\r\n /* Note, we're passing a script load promise to the PhotoSwiper to allow it to wait for the\r\n * script (containing the PhotoSwipe library) to load before using it. */\r\n this.photoSwiper = new PhotoSwiper('.gallery-wrapper', '.fullscreen-capable', this.options.vendorDelayedWaiter.promise);\r\n }\r\n return this.photoSwiper;\r\n }\r\n\r\n getPhotoSwiper() {\r\n return this.acquirePhotoSwiper();\r\n }\r\n\r\n offcanvasOpen(e) {\r\n var $body = $('body');\r\n var targetEl = $(e.target).attr('href');\r\n $(targetEl).addClass('active');\r\n $body.css('overflow', 'hidden');\r\n $body.addClass('offcanvas-open');\r\n e.preventDefault();\r\n }\r\n\r\n offcanvasClose() {\r\n var $body = $('body');\r\n $body.removeClass('offcanvas-open');\r\n setTimeout(function() {\r\n $body.css('overflow', 'visible');\r\n $('.offcanvas-container').removeClass('active');\r\n }, 450);\r\n }\r\n\r\n setupLanguageCurrencySwitcher() {\r\n function hideMenu() {\r\n // Hide menu:\r\n $('.lang-currency-switcher-wrap').removeClass('show');\r\n $('.lang-currency-switcher-wrap .dropdown-menu').removeClass('show');\r\n // Remove global click handler:\r\n $(document).off('click', handleGlobalClickEvent);\r\n }\r\n\r\n function handleGlobalClickEvent(event) {\r\n if ($(event.target).closest('.lang-currency-switcher-wrap a.active').length) {\r\n // We clicked the menu item for the active language...\r\n event.preventDefault();\r\n hideMenu();\r\n } else if (!$(event.target).closest('.lang-currency-switcher-wrap').length) {\r\n // We clicked outside the menu...\r\n hideMenu();\r\n } else {\r\n // We either clicked the menu toggle, or the menu item for another language\r\n // Do nothing, we might be navigating to another page\r\n }\r\n }\r\n\r\n function showMenu($menuToggle) {\r\n // Show menu:\r\n $menuToggle.parent().addClass('show');\r\n $menuToggle.parent().find('.dropdown-menu').addClass('show');\r\n // Add global click handler:\r\n $(document).on('click', handleGlobalClickEvent);\r\n }\r\n\r\n $('.lang-currency-switcher').on('click', function () {\r\n // Language menu switcher clicked\r\n if ($(event.target).closest('.lang-currency-switcher-wrap.show').length) {\r\n hideMenu(); // Menu was already visible, let's hide it\r\n } else {\r\n showMenu($(this));\r\n }\r\n });\r\n }\r\n}\r\n","class HttpException extends Error {\r\n constructor(message, response, errorInfo, source) {\r\n super(message);\r\n this.response = response;\r\n this.errorInfo = errorInfo;\r\n this.source = source;\r\n }\r\n}\r\n\r\nexport const ResultTypes = Object.freeze({\r\n None: 'None',\r\n Text: 'Text',\r\n JSON: 'JSON',\r\n Blob: 'Blob'\r\n});\r\n\r\nexport class HttpService {\r\n constructor(serviceUrl) {\r\n this.serviceUrl = serviceUrl;\r\n this.HDR_SAME_SITE_ONLY = 'x-same-site-only';\r\n /* Set this property to true on services who only allow calls from web pages on the same server.\r\n * This property will cause an 'x-same-site-only' header attribute to be sent to the server.\r\n * Since this is a custom header CORS will trigger a pre-flight request (OPTIONS) when the\r\n * request is not from a page from the same origin, effectively blocking all requests from other\r\n * sites. For this to work, the server must look for the existence of the 'x-same-site-only'\r\n * header and take appropriate action if the attribute is not present. */\r\n this.sameSiteOnly = false;\r\n }\r\n\r\n applySameSiteOnly(options) {\r\n if (this.sameSiteOnly) {\r\n if (!options) {\r\n options = {};\r\n }\r\n if (!options.headers) {\r\n options.headers = {};\r\n }\r\n options.headers[this.HDR_SAME_SITE_ONLY] = true;\r\n }\r\n return options;\r\n }\r\n\r\n prepareGetOptions(options) {\r\n return this.applySameSiteOnly(options);\r\n }\r\n\r\n preparePutAndPostOptions(options) {\r\n return this.applySameSiteOnly(options);\r\n }\r\n\r\n afterGet(response) { }\r\n afterPutAndPost(response) { }\r\n\r\n async doFetch(url, options, resultType) {\r\n try {\r\n if (resultType === ResultTypes.JSON) {\r\n // Add an 'Accept' header to let the server know we expect a json object as the result:\r\n\r\n // Declarative approach, but probably not as performant:\r\n // options = {\r\n // ...options,\r\n // headers: {\r\n // ...options.headers,\r\n // 'Accept': 'application/json'\r\n // }\r\n // };\r\n // More performant approach:\r\n if (!options) { options = {}; }\r\n if (!options.headers) { options.headers = {}; }\r\n options.headers['Accept'] = 'application/json';\r\n }\r\n const response = await fetch(url, options);\r\n const commErrorMsg = 'An error occurred while communicating with the server';\r\n let result;\r\n if (resultType === ResultTypes.JSON) {\r\n const text = await response.text();\r\n try {\r\n result = text ? JSON.parse(text) : {};\r\n } catch (e) { // Unable to parse text into json...\r\n if (response.ok) {\r\n throw new HttpException('The response was not valid JSON', response, text, 'HttpService');\r\n } else {\r\n throw new HttpException(commErrorMsg + '. Also, the response was not valid JSON', response, text, 'HttpService');\r\n }\r\n }\r\n } else if (resultType === ResultTypes.Text) {\r\n result = await response.text();\r\n } else if (resultType === ResultTypes.Blob) {\r\n result = response.ok ? await response.blob() : await response.text();\r\n }\r\n if (!response.ok) {\r\n let message;\r\n /* If the response is a RFC 7807-compliant ProblemDetails json object, then the json would\r\n * look like this (the exception message in the detail property is something we've deciced\r\n * to do inside the Error500NoLangRegionController's Index() action method):\r\n *\r\n * {\r\n * \"type\": \"https://tools.ietf.org/html/rfc7231#section-6.6.1\",\r\n * \"title\": \"An error occurred while processing your request.\",\r\n * \"status\": 500,\r\n * \"detail\": \"Object reference not set to an instance of an object.\",\r\n * \"traceId\": \"00-8eac13e950d101b0fb193a8be9dec7ad-37cbe464992090c2-00\"\r\n * }\r\n *\r\n */\r\n if (result.title) {\r\n if (result.detail) {\r\n message = `${result.title}. Details: '${result.detail}''`;\r\n } else {\r\n message = result.title;\r\n }\r\n } else {\r\n message = result.Message || commErrorMsg;\r\n }\r\n throw new HttpException(message, response, result, 'HttpService');\r\n }\r\n return { response, result };\r\n } catch (ex) {\r\n if (typeof ex.stack === 'string' && ex.stack === '') {\r\n // Manually assign the stack:\r\n ex.stack = (new Error()).stack;\r\n }\r\n throw ex;\r\n }\r\n }\r\n\r\n /**\r\n * Uses a fetch call to retrieve a response expected to be in json format. Use the options\r\n * parameter object to specify a fetch method other than GET (default). If the server api returns the\r\n * text in json format, then the return value of this method will contain that json string. If the\r\n * json is a simple string value, then the value will have leading and trailing quote characters.\r\n */\r\n async fetchJson(relativeUrl, options) {\r\n options = this.prepareGetOptions(options);\r\n const { response, result } = await this.doFetch(this.serviceUrl + relativeUrl, options, ResultTypes.JSON);\r\n this.afterGet(response);\r\n return result;\r\n }\r\n\r\n /**\r\n * Uses a fetch call to retrieve the response expected to be in json format.\r\n */\r\n async get(relativeUrl, options) {\r\n // Note this method is now just a wrapper around fetchJson. New code should call fetchJson directly!\r\n return await this.fetchJson(relativeUrl, options);\r\n }\r\n\r\n /**\r\n * Uses a fetch call to retrieve a response expected to be in plain text format. Use the options\r\n * parameter object to specify a fetch method other than GET (default). If the server api returns the\r\n * text in json format, then the return value of this method will contain that json string. If the\r\n * json is a simple string value, then the value will have leading and trailing quote characters.\r\n */\r\n async fetchText(relativeUrl, options) {\r\n options = this.prepareGetOptions(options);\r\n const { response, result } = await this.doFetch(this.serviceUrl + relativeUrl, options, ResultTypes.Text);\r\n this.afterGet(response);\r\n return result;\r\n }\r\n\r\n /**\r\n * Uses a fetch call to retrieve the response expected to be in binary/blob format.\r\n */\r\n async getBlob(relativeUrl, options) {\r\n options = this.prepareGetOptions(options);\r\n const { response, result } = await this.doFetch(this.serviceUrl + relativeUrl, options, ResultTypes.Blob);\r\n this.afterGet(response);\r\n return result;\r\n }\r\n\r\n /**\r\n * Uses a fetch call to make a POST request to the server. The method assumes that the response\r\n * is in json format.\r\n */\r\n async post(relativeUrl, headers, body, signal) {\r\n let options = {\r\n method: 'POST',\r\n headers,\r\n body,\r\n signal\r\n };\r\n options = this.preparePutAndPostOptions(options);\r\n const { response, result } = await this.doFetch(this.serviceUrl + relativeUrl, options, ResultTypes.JSON);\r\n this.afterPutAndPost(response);\r\n return result;\r\n }\r\n\r\n /**\r\n * Uses a fetch call to make a POST request to the server. The method assumes that any returned text\r\n * is in plain text format. If the server api returns the text in json format, then the return value\r\n * of this method will contain that json string. If the json is a simple string value, then the value\r\n * will have leading and trailing quote characters.\r\n */\r\n async postAndReturnText(relativeUrl, signal) {\r\n let options = {\r\n method: 'POST',\r\n signal\r\n };\r\n return await this.fetchText(relativeUrl, options);\r\n }\r\n\r\n /**\r\n * Uses a fetch call to make a PUT request to the server. The method assumes that the response\r\n * is in json format.\r\n */\r\n async put(relativeUrl, headers, body, signal) {\r\n let options = {\r\n method: 'PUT',\r\n headers,\r\n body,\r\n signal\r\n };\r\n options = this.preparePutAndPostOptions(options);\r\n const { response, result } = await this.doFetch(this.serviceUrl + relativeUrl, options, ResultTypes.JSON);\r\n this.afterPutAndPost(response);\r\n return result;\r\n }\r\n\r\n /**\r\n * Makes a POST request to the server. The method assumes that response is in json format.\r\n */\r\n async postJson(relativeUrl, jsonString, signal) {\r\n return await this.post(relativeUrl, { 'Content-Type': 'application/json' }, jsonString, signal);\r\n }\r\n\r\n /**\r\n * Makes a POST request to the server. The method assumes that response is in json format.\r\n */\r\n async postJsonObject(relativeUrl, obj, signal) {\r\n return await this.postJson(relativeUrl, JSON.stringify(obj), signal);\r\n }\r\n\r\n async putJson(relativeUrl, jsonString, signal) {\r\n return await this.put(relativeUrl, { 'Content-Type': 'application/json' }, jsonString, signal);\r\n }\r\n\r\n async putJsonObject(relativeUrl, obj, signal) {\r\n return await this.putJson(relativeUrl, JSON.stringify(obj), signal);\r\n }\r\n\r\n}\r\n","import { HttpService } from '../main/HttpService.js';\r\n\r\n/**\r\n * Web API client for communicating the Admin Service. This service normally resides on the\r\n * same server as the one serving this javascript file (the url probably won't have a domain).\r\n */\r\nexport class AdminService extends HttpService {\r\n constructor(serviceUrl) {\r\n super(serviceUrl);\r\n this.sameSiteOnly = true; // See super() for info\r\n }\r\n\r\n async submitError(error, signal) {\r\n return await this.postJsonObject('SubmitError', error, signal);\r\n }\r\n\r\n async createLoginKey(signal) {\r\n return await this.post('login-key', null, null, signal);\r\n }\r\n}\r\n","class BrowserDetect {\r\n constructor() {\r\n this.knownBrowsers = [\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Edge\",\r\n identity: \"Edge\",\r\n versionPrefix: \"Edge\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Edg\",\r\n identity: \"Edge\",\r\n versionPrefix: \"Edg\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"OPR\",\r\n identity: \"Opera\",\r\n versionPrefix: \"OPR\"\r\n },\r\n {\r\n prop: window.opera,\r\n identity: \"Opera\",\r\n versionPrefix: \"Version\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Chrome\",\r\n identity: \"Chrome\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"OmniWeb\",\r\n versionPrefix: \"OmniWeb/\",\r\n identity: \"OmniWeb\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Safari\",\r\n identity: \"Safari\",\r\n versionPrefix: \"Version\"\r\n },\r\n {\r\n string: navigator.vendor,\r\n subString: \"Apple\",\r\n identity: \"Safari\",\r\n versionPrefix: \"Version\"\r\n },\r\n {\r\n string: navigator.vendor,\r\n subString: \"iCab\",\r\n identity: \"iCab\"\r\n },\r\n {\r\n string: navigator.vendor,\r\n subString: \"KDE\",\r\n identity: \"Konqueror\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Firefox\",\r\n identity: \"Firefox\"\r\n },\r\n {\r\n string: navigator.vendor,\r\n subString: \"Camino\",\r\n identity: \"Camino\"\r\n },\r\n { // for newer Netscapes (6+)\r\n string: navigator.userAgent,\r\n subString: \"Netscape\",\r\n identity: \"Netscape\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"IEMobile\",\r\n identity: \"Explorer Mobile\",\r\n versionPrefix: \"IEMobile\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"MSIE\",\r\n identity: \"Explorer\",\r\n versionPrefix: \"MSIE\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Trident\",\r\n identity: \"Explorer\",\r\n versionPrefix: \"rv\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Gecko\",\r\n identity: \"Mozilla\",\r\n versionPrefix: \"rv\"\r\n },\r\n { // for older Netscapes (4-)\r\n string: navigator.userAgent,\r\n subString: \"Mozilla\",\r\n identity: \"Netscape\",\r\n versionPrefix: \"Mozilla\"\r\n }\r\n ];\r\n\r\n this.knownOpSystems = [\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Windows Phone\",\r\n identity: \"Windows Phone\"\r\n },\r\n {\r\n string: navigator.platform,\r\n subString: \"Win\",\r\n identity: \"Windows\"\r\n },\r\n {\r\n string: navigator.platform,\r\n subString: \"Mac\",\r\n identity: \"Mac\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"iPad\",\r\n identity: \"iPad\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"iPhone\",\r\n identity: \"iPhone\"\r\n },\r\n {\r\n string: navigator.userAgent,\r\n subString: \"Android\",\r\n identity: \"Android\"\r\n },\r\n {\r\n string: navigator.platform,\r\n subString: \"Linux\",\r\n identity: \"Linux\"\r\n }\r\n ];\r\n }\r\n\r\n detectItemInArray(data) {\r\n for (let i = 0; i < data.length; i++) {\r\n const dataString = data[i].string;\r\n const dataProp = data[i].prop;\r\n const prefix = data[i].versionPrefix || data[i].identity;\r\n if (dataString) {\r\n if (dataString.indexOf(data[i].subString) !== -1) {\r\n return {\r\n result: data[i].identity,\r\n versionPrefix: prefix\r\n };\r\n }\r\n } else if (dataProp) {\r\n return {\r\n result: data[i].identity,\r\n versionPrefix: prefix\r\n };\r\n }\r\n }\r\n return {\r\n result: \"Undetected\",\r\n versionPrefix: null\r\n };\r\n }\r\n\r\n searchVersion(dataString, prefix) {\r\n const index = dataString.indexOf(prefix);\r\n if (index == -1)\r\n return;\r\n return parseFloat(dataString.substring(index + prefix.length + 1));\r\n }\r\n\r\n detectOSCategory(OS) {\r\n switch (OS) {\r\n case \"Windows\":\r\n case \"Windows Phone\":\r\n case \"Mac\":\r\n case \"iPad\":\r\n case \"iPhone\":\r\n case \"Android\":\r\n case \"Linux\":\r\n return OS;\r\n default:\r\n return \"Unknown\";\r\n }\r\n }\r\n\r\n isIOS() {\r\n if (/iPad|iPhone|iPod/.test(navigator.platform)) {\r\n return true;\r\n } else {\r\n return navigator.maxTouchPoints &&\r\n navigator.maxTouchPoints > 2 &&\r\n /MacIntel/.test(navigator.platform);\r\n }\r\n }\r\n\r\n isIpadOS() {\r\n return navigator.maxTouchPoints &&\r\n navigator.maxTouchPoints > 2 &&\r\n /MacIntel/.test(navigator.platform);\r\n }\r\n\r\n refresh() {\r\n let item = this.detectItemInArray(this.knownBrowsers);\r\n this.browser = item.result;\r\n this.version = this.searchVersion(navigator.userAgent, item.versionPrefix)\r\n || this.searchVersion(navigator.appVersion, item.versionPrefix)\r\n || 0;\r\n item = this.detectItemInArray(this.knownOpSystems);\r\n this.OS = item.result;\r\n if (this.OS === 'Mac') {\r\n if (this.isIpadOS) {\r\n this.OS = 'iPad';\r\n } else if (this.isIOS()) {\r\n this.OS = 'iPhone';\r\n }\r\n }\r\n this.OSCategory = this.detectOSCategory(this.OS);\r\n }\r\n}\r\n\r\nclass BrowserDetector {\r\n constructor() {\r\n }\r\n\r\n refresh(ua) {\r\n // Defaults\r\n this.browser = \"Unknown\";\r\n this.platform = \"Unknown\";\r\n this.version = \"\";\r\n this.majorver = \"\";\r\n this.minorver = \"\";\r\n\r\n var uaLen = ua.length;\r\n\r\n // ##### Split into stuff before parens and stuff in parens\r\n var preparens = \"\";\r\n var parenthesized = \"\";\r\n\r\n var i = ua.indexOf(\"(\");\r\n var j = 0;\r\n if (i >= 0) {\r\n preparens = ua.substring(0, i).trim();\r\n parenthesized = ua.substring(i + 1, uaLen);\r\n j = parenthesized.indexOf(\")\");\r\n if (j >= 0)\r\n parenthesized = parenthesized.substring(0, j);\r\n }\r\n else\r\n preparens = ua;\r\n\r\n // ##### First assume browser and version are in preparens\r\n // ##### override later if we find them in the parenthesized stuff\r\n var browVer = preparens;\r\n\r\n var tokens = parenthesized.split(\";\");\r\n var token = \"\";\r\n // # Now go through parenthesized tokens\r\n for (i = 0; i < tokens.length; i++) {\r\n token = tokens[i].trim();\r\n //## compatible - might want to reset from Netscape\r\n if (token == \"compatible\") {\r\n //## One might want to reset browVer to a null string\r\n //## here, but instead, we'll assume that if we don't\r\n //## find out otherwise, then it really is Mozilla\r\n //## (or whatever showed up before the parens).\r\n //## browser - try for Opera or IE\r\n }\r\n else if (token.indexOf(\"MSIE\") >= 0)\r\n browVer = token;\r\n else if (token.indexOf(\"Opera\") >= 0)\r\n browVer = token;\r\n //'## platform - try for X11, SunOS, Win, Mac, PPC\r\n else if ((token.indexOf(\"X11\") >= 0) || (token.indexOf(\"SunOS\") >= 0) || (token.indexOf(\"Linux\") >= 0))\r\n this.platform = \"Unix\";\r\n else if (token.indexOf(/*\"Win\"*/\"Windows\") >= 0) // Changed from \"Win\" to \"Windows\" by Tor because IE9 64-bit reports multiple strings starting with \"Win\"\r\n this.platform = token;\r\n else if ((token.indexOf(\"Mac\") >= 0) || (token.indexOf(\"PPC\") >= 0))\r\n this.platform = token;\r\n else if (token.indexOf(\"rv:\") >= 0)\r\n browVer = token;\r\n }\r\n\r\n var msieIndex = browVer.indexOf(\"MSIE\");\r\n if (msieIndex >= 0)\r\n browVer = browVer.substring(msieIndex, browVer.length);\r\n\r\n var leftover = \"\";\r\n if (browVer.substring(0, \"Mozilla\".length) == \"Mozilla\") {\r\n var edgeNdx = ua.indexOf(\"Edg\");\r\n if (edgeNdx > -1) {\r\n this.browser = \"Edge\";\r\n leftover = ua.substring(edgeNdx + \"Edg\".length + 1, ua.length);\r\n } else {\r\n var chromeNdx = ua.indexOf(\"Chrome\");\r\n if (chromeNdx > -1) {\r\n this.browser = \"Chrome\";\r\n leftover = ua.substring(chromeNdx + \"Chrome\".length + 1, ua.length);\r\n } else {\r\n this.browser = \"Netscape\";\r\n leftover = browVer.substring(\"Mozilla\".length + 1, browVer.length);\r\n }\r\n }\r\n } else if (browVer.substring(0, \"Lynx\".length) == \"Lynx\") {\r\n this.browser = \"Lynx\";\r\n leftover = browVer.substring(\"Lynx\".length + 1, browVer.length);\r\n } else if (browVer.substring(0, \"MSIE\".length) == \"MSIE\") {\r\n this.browser = \"IE\";\r\n leftover = browVer.substring(\"MSIE\".length + 1, browVer.length);\r\n } else if (browVer.substring(0, \"Microsoft Internet Explorer\".length) == \"Microsoft Internet Explorer\") {\r\n this.browser = \"IE\";\r\n leftover = browVer.substring(\"Microsoft Internet Explorer\".length + 1, browVer.length);\r\n } else if (browVer.substring(0, \"Opera\".length) == \"Opera\") {\r\n this.browser = \"Opera\";\r\n leftover = browVer.substring(\"Opera\".length + 1, browVer.length);\r\n } else if ((browVer.substring(0, \"rv:\".length) == \"rv:\") && (ua.indexOf(\"Trident\") > 0)) {\r\n this.browser = \"IE\";\r\n leftover = browVer.substring(\"rv:\".length, browVer.length);\r\n }\r\n\r\n leftover = leftover.trim();\r\n\r\n // # Try to get version info out of leftover stuff\r\n i = leftover.indexOf(\" \");\r\n if (i >= 0)\r\n this.version = leftover.substring(0, i);\r\n else\r\n this.version = leftover;\r\n j = this.version.indexOf(\".\");\r\n if (j >= 0) {\r\n this.majorver = this.version.substring(0, j);\r\n this.minorver = this.version.substring(j + 1, this.version.length);\r\n }\r\n else\r\n this.majorver = this.version;\r\n }\r\n}\r\n\r\n/**\r\n * This class is a merger of the BrowserDetect.js and BrowserDetector.js files found on the\r\n * LangloHomegPages web site. In addition the code has been modernized to fit into this class.\r\n */\r\nexport class ClientEnvInfo {\r\n constructor(autoRefresh) {\r\n this.browserDetect = new BrowserDetect();\r\n this.detector = new BrowserDetector();\r\n if (autoRefresh) {\r\n this.refresh();\r\n }\r\n }\r\n\r\n refresh() {\r\n const detect = this.browserDetect;\r\n detect.refresh();\r\n\r\n /* The BrowserDetector object has overlapping functionality with BrowserDetect, but provides\r\n * access to the platform version. We need that to distinguish Windows OS versions.\r\n * (BrowserDetect contains clearer information on OS (e.g. \"Windows\"), while BrowserDetector\r\n * contains clearer information on OS version (e.g. \"Windows NT 5.1\")). */\r\n\r\n const detector = this.detector;\r\n detector.refresh(navigator.userAgent);\r\n\r\n let osVersion = 0.0;\r\n let tokens = null;\r\n if (detect.OS === 'Windows') {\r\n tokens = detector.platform.split('Windows NT');\r\n if (tokens !== null && tokens.length === 2) {\r\n osVersion = parseFloat(tokens[1]);\r\n }\r\n } else if (detect.OS === 'Windows Phone') {\r\n tokens = detector.platform.split('Windows Phone OS');\r\n if (tokens !== null && tokens.length === 2) {\r\n osVersion = parseFloat(tokens[1]);\r\n } else {\r\n tokens = detector.platform.split('Windows Phone');\r\n if (tokens !== null && tokens.length === 2) {\r\n osVersion = parseFloat(tokens[1]);\r\n }\r\n }\r\n //} else if (detect.OS === 'Mac') {\r\n // //\r\n }\r\n\r\n const browserCategory = detect.browser;\r\n this.device = {\r\n userAgent: navigator.userAgent,\r\n osCategory: detect.OSCategory,\r\n opSys: detect.OS,\r\n osVersion: osVersion,\r\n browserCategory: browserCategory,\r\n browser: detect.version > 0 ? browserCategory + ' ' + detect.version : browserCategory,\r\n browserVersion: detect.version,\r\n opSysAndVersion: osVersion ? detect.OS + ' ' + osVersion : detect.OS + ' (' + detector.platform + ')'\r\n };\r\n }\r\n}","export class ObjectUtils {\r\n\r\n static hasProperties(obj) {\r\n const hasOwnProperty = Object.prototype.hasOwnProperty;\r\n for (var prop in obj) {\r\n if (hasOwnProperty.call(obj, prop)) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /* -----------------------------------------------------------------------------------------\r\n * https://raw.githubusercontent.com/ReactiveSets/toubkal/master/lib/util/value_equals.js\r\n equalContents( a, b [, enforce_properties_order, cyclic] )\r\n\r\n Returns true if a and b are deeply equal, false otherwise.\r\n\r\n Parameters:\r\n - a (Any type): value to compare to b\r\n - b (Any type): value compared to a\r\n\r\n Optional Parameters:\r\n - enforce_properties_order (Boolean): true to check if Object properties are provided\r\n in the same order between a and b\r\n\r\n - cyclic (Boolean): true to check for cycles in cyclic objects\r\n\r\n Implementation:\r\n 'a' is considered equal to 'b' if all scalar values in a and b are strictly equal as\r\n compared with operator '===' except for these two special cases:\r\n - 0 === -0 but are not equal.\r\n - NaN is not === to itself but is equal.\r\n\r\n RegExp objects are considered equal if they have the same lastIndex, i.e. both regular\r\n expressions have matched the same number of times.\r\n\r\n Functions must be identical, so that they have the same closure context.\r\n\r\n \"undefined\" is a valid value, including in Objects\r\n\r\n 106 automated tests.\r\n\r\n Provide options for slower, less-common use cases:\r\n\r\n - Unless enforce_properties_order is true, if 'a' and 'b' are non-Array Objects, the\r\n order of occurence of their attributes is considered irrelevant:\r\n { a: 1, b: 2 } is considered equal to { b: 2, a: 1 }\r\n\r\n - Unless cyclic is true, Cyclic objects will throw:\r\n RangeError: Maximum call stack size exceeded\r\n */\r\n static equalContents(a, b, enforce_properties_order, cyclic) {\r\n var toString = Object.prototype.toString;\r\n\r\n return a === b // strick equality should be enough unless zero\r\n && a !== 0 // because 0 === -0, requires test by _equals()\r\n || _equals(a, b); // handles not strictly equal or zero values\r\n\r\n function _equals(a, b) {\r\n // a and b have already failed test for strict equality or are zero\r\n\r\n var s, l, p, x, y;\r\n\r\n // They should have the same toString() signature\r\n if ((s = toString.call(a)) !== toString.call(b)) return false;\r\n\r\n switch (s) {\r\n default: // Boolean, Date, String\r\n return a.valueOf() === b.valueOf();\r\n\r\n\r\n case '[object Number]':\r\n // Converts Number instances into primitive values\r\n // This is required also for NaN test bellow\r\n a = +a;\r\n b = +b;\r\n\r\n return a ? // a is Non-zero and Non-NaN\r\n a === b\r\n : // a is 0, -0 or NaN\r\n a === a ? // a is 0 or -O\r\n 1 / a === 1 / b // 1/0 !== 1/-0 because Infinity !== -Infinity\r\n : b !== b; // NaN, the only Number not equal to itself!\r\n\r\n\r\n case '[object RegExp]':\r\n return a.source == b.source\r\n && a.global == b.global\r\n && a.ignoreCase == b.ignoreCase\r\n && a.multiline == b.multiline\r\n && a.lastIndex == b.lastIndex;\r\n\r\n\r\n case '[object Function]':\r\n return false; // functions should be strictly equal because of closure context\r\n\r\n\r\n case '[object Array]':\r\n if (cyclic && (x = reference_equals(a, b)) !== null) return x; // intentionally duplicated bellow for [object Object]\r\n\r\n if ((l = a.length) != b.length) return false;\r\n // Both have as many elements\r\n\r\n while (l--) {\r\n if ((x = a[l]) === (y = b[l]) && x !== 0 || _equals(x, y)) continue;\r\n\r\n return false;\r\n }\r\n\r\n return true;\r\n\r\n\r\n case '[object Object]':\r\n if (cyclic && (x = reference_equals(a, b)) !== null) return x; // intentionally duplicated from above for [object Array]\r\n\r\n l = 0; // counter of own properties\r\n\r\n if (enforce_properties_order) {\r\n var properties = [];\r\n\r\n for (p in a) {\r\n if (a.hasOwnProperty(p)) {\r\n properties.push(p);\r\n\r\n if ((x = a[p]) === (y = b[p]) && x !== 0 || _equals(x, y)) continue;\r\n\r\n return false;\r\n }\r\n }\r\n\r\n // Check if 'b' has as the same properties as 'a' in the same order\r\n for (p in b)\r\n if (b.hasOwnProperty(p) && properties[l++] != p)\r\n return false;\r\n } else {\r\n for (p in a) {\r\n if (a.hasOwnProperty(p)) {\r\n ++l;\r\n\r\n if ((x = a[p]) === (y = b[p]) && x !== 0 || _equals(x, y)) continue;\r\n\r\n return false;\r\n }\r\n }\r\n\r\n // Check if 'b' has as not more own properties than 'a'\r\n for (p in b)\r\n if (b.hasOwnProperty(p) && --l < 0)\r\n return false;\r\n }\r\n\r\n return true;\r\n } // switch toString.call( a )\r\n } // _equals()\r\n\r\n /* -----------------------------------------------------------------------------------------\r\n reference_equals( a, b )\r\n\r\n Helper function to compare object references on cyclic objects or arrays.\r\n\r\n Returns:\r\n - null if a or b is not part of a cycle, adding them to object_references array\r\n - true: same cycle found for a and b\r\n - false: different cycle found for a and b\r\n\r\n On the first call of a specific invocation of equal(), replaces self with inner function\r\n holding object_references array object in closure context.\r\n\r\n This allows to create a context only if and when an invocation of equal() compares\r\n objects or arrays.\r\n */\r\n /*eslint no-func-assign: \"off\"*/\r\n function reference_equals(a, b) {\r\n var object_references = [];\r\n\r\n return (reference_equals = _reference_equals)(a, b);\r\n\r\n function _reference_equals(a, b) {\r\n var l = object_references.length;\r\n\r\n while (l--)\r\n if (object_references[l--] === b)\r\n return object_references[l] === a;\r\n\r\n object_references.push(a, b);\r\n\r\n return null;\r\n } // _reference_equals()\r\n } // reference_equals()\r\n } // equalContents()\r\n\r\n static clone(source, deepClone) {\r\n if (!source) {\r\n return undefined;\r\n }\r\n if (source === null) {\r\n return null;\r\n }\r\n if (Array.isArray(source)) {\r\n const dest = Array(source.length);\r\n let i = 0;\r\n source.forEach(item => {\r\n if (deepClone && item !== null && typeof (item) === 'object')\r\n dest[i] = ObjectUtils.clone(item, deepClone);\r\n else\r\n dest[i] = item;\r\n i++;\r\n });\r\n return dest;\r\n } else {\r\n return ObjectUtils.assign({}, source, deepClone);\r\n }\r\n }\r\n\r\n static isAssigned(obj) {\r\n return typeof obj !== 'undefined' && obj !== null;\r\n }\r\n\r\n static assign(dest, source, deepClone) {\r\n for (var i in source) {\r\n if (deepClone && source[i] !== null && typeof (source[i]) === 'object')\r\n dest[i] = ObjectUtils.clone(source[i], deepClone);\r\n else\r\n dest[i] = source[i];\r\n }\r\n return dest;\r\n }\r\n\r\n // find nearest ancestor element causing the predicate callback to return true\r\n static findAncestor(element, predicate) {\r\n return element && (predicate(element) ? element : this.findAncestor(element.parentNode, predicate));\r\n }\r\n\r\n static clearChildren(element) {\r\n while (element.firstChild) {\r\n element.removeChild(element.firstChild);\r\n }\r\n }\r\n\r\n static getScrollParent(node) {\r\n if (node === window) {\r\n return null;\r\n }\r\n const isElement = node instanceof HTMLElement;\r\n const overflowY = isElement && window.getComputedStyle(node).overflowY;\r\n const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';\r\n if (!node) {\r\n return null;\r\n } else if (isScrollable && node.scrollHeight >= node.clientHeight) {\r\n return node;\r\n }\r\n return this.getScrollParent(node.parentNode) || document.body;\r\n }\r\n\r\n /* Not correctly implemented, but also not needed at this point */\r\n //static isScrolledIntoView(el, mustbeFullyVisible) {\r\n // let isVisible = false;\r\n // const scrollParent = this.getScrollParent(el);\r\n // if (scrollParent) {\r\n // const rect = el.getBoundingClientRect();\r\n // const elemTop = rect.top;\r\n // const elemBottom = rect.bottom;\r\n // const innerHeight = scrollParent === window ? window.innerHeight : scrollParent.clientHeight;\r\n // if (mustbeFullyVisible) {\r\n // // Only completely visible elements return true:\r\n // isVisible = (elemTop >= 0) && (elemBottom <= innerHeight);\r\n // } else {\r\n // // Partially visible elements return true:\r\n // isVisible = elemTop < innerHeight && elemBottom >= 0;\r\n // }\r\n // }\r\n // return isVisible;\r\n //}\r\n\r\n static isNullOrEmpty(text) {\r\n return typeof text === 'undefined' || text === null || text === '';\r\n }\r\n\r\n // Uses \"away from zero\" rounding (the Math.round uses \"Banker's rounding\").\r\n static round(value) {\r\n return value < 0 ? -Math.round(-value) : Math.round(value);\r\n }\r\n\r\n /**\r\n * Rounds to neareast decimals, is fast, but is flawed due to floating point rounding inaccuracies.\r\n * roundDecInAcc(1.005, 2) might return 1.00 instead of 1.01.\r\n */\r\n static roundDecInAcc(number, decimals) {\r\n const factorOfTen = Math.pow(10, decimals);\r\n return Math.round(number * factorOfTen) / factorOfTen;\r\n }\r\n\r\n /**\r\n * Rounds to neareast decimals, is not as fast due to string conversion, but handles floating point\r\n * rounding inaccuracies. roundDec(1.005, 2) always returns 1.01.\r\n */\r\n static roundDec(number, decimals) {\r\n const tmp = Math.round(number + 'e' + decimals);\r\n return Number(tmp + 'e-' + decimals);\r\n }\r\n\r\n static removeFromArray(array, item) {\r\n const pos = array.indexOf(item);\r\n if (pos > -1) {\r\n return array.splice(pos, 1);\r\n }\r\n return null;\r\n }\r\n\r\n static async sleep(ms) {\r\n return new Promise(resolve => {\r\n setTimeout(resolve, ms);\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Allows one part of the code to wait for another part of the code to complete an asynchronous\r\n * operation. Wrap the async code with calls to clear() and set(), and call \"await hasBeenSet()\"\r\n * in the code which needs to wait. */\r\nexport class PromiseFlag {\r\n constructor() {\r\n }\r\n\r\n clear() {\r\n this.promise = new Promise(resolve => {\r\n this.resolve = resolve;\r\n });\r\n }\r\n\r\n set() {\r\n if (this.promise) {\r\n this.resolve();\r\n this.promise = null;\r\n this.resolve = null;\r\n }\r\n }\r\n\r\n async hasBeenSet() {\r\n if (this.promise) {\r\n await this.promise;\r\n }\r\n }\r\n}\r\n\r\nexport class StringUtils {\r\n static isNullOrEmpty(value) {\r\n // Note, value == null catches both null and undefined\r\n return (value == null || value === '');\r\n }\r\n\r\n static isNullOrWhiteSpace(value) {\r\n /* We're taking advantage of the fact that in JS\r\n * 1) both null and undefined == null, and\r\n * 2) a string with only whitespace equalsy 0, but\r\n * 3) we have test for 0 and '0' since they also equalsy 0, but they are not whitespace\r\n * (in our definition). */\r\n return value == null || (value == 0 && value !== 0 && value !== '0');\r\n }\r\n\r\n /* Joins the left and right strings with separator. If either (or both) are null or empty,\r\n * then the separator is not used. */\r\n static join(separator, left, right, trimResult /*= true*/) {\r\n const leftEmpty = StringUtils.isNullOrEmpty(left);\r\n const rightEmpty = StringUtils.isNullOrEmpty(right);\r\n if (leftEmpty && rightEmpty)\r\n return null;\r\n\r\n let result;\r\n if (leftEmpty)\r\n result = right;\r\n else if (rightEmpty)\r\n result = left;\r\n else\r\n result = left + separator + right;\r\n return trimResult ? result.Trim() : result;\r\n }\r\n\r\n static insert(str, index, insertedStr) {\r\n return str.slice(0, index) + insertedStr + str.slice(index);\r\n }\r\n\r\n static truncateString(str, len, truncateWholeWords) {\r\n if (len <= 0) {\r\n return null;\r\n }\r\n if (str === null || len >= str.length) {\r\n return str;\r\n }\r\n if (!truncateWholeWords || (len < str.length - 1 && str[len] === ' ')) {\r\n return str.slice(0, len - 1);\r\n }\r\n let ndx = len;\r\n while (ndx > 0 && str[ndx] !== ' ') {\r\n ndx--;\r\n }\r\n return ndx > 0 ? str.slice(0, ndx - 1) : str.slice(0, len - 1);\r\n }\r\n\r\n static removeChars(str, chars) {\r\n if (str === null || chars === null || chars === '')\r\n return str;\r\n var arr = str.split('');\r\n const filteredArr = arr.filter(ch => chars.indexOf(ch) === -1);\r\n return filteredArr.join('');\r\n }\r\n\r\n static addTrailingSlash(str) {\r\n if (!str || str === null || str === '') {\r\n return '/';\r\n }\r\n if (!str.endsWith('/')) {\r\n return str + '/';\r\n }\r\n return str;\r\n }\r\n\r\n static replaceCharAt(str, index, chr) {\r\n if (index > str.length - 1) return str;\r\n return str.substring(0, index) + chr + str.substring(index + 1);\r\n }\r\n\r\n static isJson(str, canBeObject = true, canBeArray = true, canBeString = false, canBeNumber = false) {\r\n try {\r\n const json = JSON.parse(str);\r\n if (!json) {\r\n return false;\r\n }\r\n const type = typeof json;\r\n if (!canBeObject && type === 'object') {\r\n return false;\r\n }\r\n if (!canBeArray && Array.isArray(json)) {\r\n return false;\r\n }\r\n if (!canBeString && type === 'string') {\r\n return false;\r\n }\r\n if (!canBeNumber && type === 'number' && !isNaN(json)) {\r\n return false;\r\n }\r\n return true;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n}\r\n\r\n/** Calls the onComplete callback when both minDuration ms. has elapsed and the complete() method\r\n * has been called. Use this class if you want to call some code, but not until at least x ms\r\n * have elapsed. */\r\nexport class MinDuration {\r\n constructor(minDuration, onComplete) {\r\n this.minDuration = minDuration;\r\n this.onComplete = onComplete;\r\n this.timeout = setTimeout(() => this.timeoutExpired(), this.minDuration);\r\n }\r\n\r\n completeIfReady() {\r\n if (this.isCompleted && this.isTimeoutExpired && this.onComplete) {\r\n this.onComplete(this);\r\n }\r\n }\r\n\r\n timeoutExpired() {\r\n this.isTimeoutExpired = true;\r\n this.completeIfReady();\r\n }\r\n\r\n complete() {\r\n this.isCompleted = true;\r\n this.completeIfReady();\r\n }\r\n}\r\n\r\nexport class Utils {\r\n /** Will start a loop calling requestAnimationFrame until either the \"until\": callback returns true,\r\n * or ms milliseconds elapses. If \"until\" returns true, then the onSuccess is called. Otherwise, the\r\n * onTimeout is called. */\r\n static requestAnimationFramesUntil(ms, until, onSuccess, onTimedOut) {\r\n const startTime = Date.now();\r\n const onFrame = () => {\r\n if (until()) {\r\n onSuccess();\r\n } else if (Date.now() - startTime < ms) {\r\n //console.log('Requesting another frame...');\r\n requestAnimationFrame(onFrame);\r\n } else {\r\n //console.log('Timed out requesting frames!');\r\n if (onTimedOut) {\r\n onTimedOut();\r\n }\r\n }\r\n };\r\n requestAnimationFrame(onFrame);\r\n }\r\n\r\n static addEventListener(element, name, eventHandler, options = true) {\r\n element.addEventListener(name, eventHandler, options);\r\n return { element, name, eventHandler, options };\r\n }\r\n\r\n static removeEventListener(listener) {\r\n if (listener) {\r\n listener.element.removeEventListener(listener.name, listener.eventHandler, listener.options);\r\n }\r\n }\r\n\r\n static cssColorStr(str) {\r\n const ctx = document.createElement('canvas').getContext('2d');\r\n ctx.fillStyle = str;\r\n return ctx.fillStyle;\r\n }\r\n\r\n static cssColor(str) {\r\n let colorStr = GraphicsHelper.cssColorStr(str);\r\n colorStr = colorStr.slice(1);\r\n const color = parseInt('0x' + colorStr);\r\n return color;\r\n }\r\n\r\n /**\r\n * Returns a new array with the values found in both array1 and array2.\r\n */\r\n static getArrayIntersect(array1, array2) {\r\n if (!array1 || array1.length === 0) return array2;\r\n if (!array2 || array2.length === 0) return array1;\r\n const result = [];\r\n array1.forEach(value => {\r\n if (array2.indexOf(value) > -1) {\r\n result.push(value);\r\n }\r\n })\r\n return result;\r\n }\r\n}\r\n\r\nexport class RepeatWhileMouseDown {\r\n constructor(element, interval, onMouseDown, onRepeat, onMouseUp) {\r\n this.interval = interval;\r\n this.initialDelay = 300; // ms\r\n this.onMouseDown = onMouseDown;\r\n this.onRepeat = onRepeat;\r\n this.onMouseUp = onMouseUp;\r\n element.addEventListener('mousedown', () => this.handleMouseDown());\r\n element.addEventListener('mouseup', () => this.handleMouseUp());\r\n // Use native events for touch devices:\r\n element.addEventListener('touchstart', ev => this.handleTouchStart(ev), { passive: false });\r\n element.addEventListener('touchend', ev => this.handleTouchEnd(ev));\r\n }\r\n\r\n handleMouseDown() {\r\n //console.log('mousedown');\r\n this.onMouseDown();\r\n this.delayHandle = setTimeout(() => this.handleMouseDownDelay(), this.initialDelay);\r\n }\r\n\r\n handleMouseUp() {\r\n //console.log('mouseup');\r\n if (this.autoRepeating) {\r\n this.autoRepeating = false;\r\n clearInterval(this.intervalHandle);\r\n this.onMouseUp(false);\r\n } else {\r\n clearTimeout(this.delayHandle);\r\n this.onMouseUp(true);\r\n }\r\n }\r\n\r\n handleMouseDownDelay() {\r\n //console.log('repeating...');\r\n this.autoRepeating = true;\r\n this.intervalHandle = setInterval(() => this.onRepeat(), this.interval);\r\n }\r\n\r\n /**\r\n * Is only fired on touch devices.\r\n */\r\n handleTouchStart(ev) {\r\n //console.log('touchstart');\r\n ev.preventDefault(); // Block the mousedown event, we don't need it\r\n this.onMouseDown();\r\n // The typical timer interval (this.interval) is too long for touch devices\r\n this.interval = this.interval / 2;\r\n this.delayHandle = setTimeout(() => this.handleMouseDownDelay(), this.initialDelay);\r\n }\r\n\r\n /**\r\n * Is only fired on touch devices.\r\n */\r\n handleTouchEnd(ev) {\r\n //console.log('touchend');\r\n // Use same code as for mouse up event:\r\n this.handleMouseUp();\r\n }\r\n}\r\n\r\n/**\r\n * dragToScroll enables pointer devices to scroll an element by using a drag operation. On touch\r\n * devices this functionality is built-in.\r\n */\r\nexport class DragToScroll {\r\n /**\r\n * DragToScroll constructor.\r\n * @param {any} childElement A child element isn't required, but when provided, drag operations\r\n * will only work on the child element or it's children. Drag operations on the parent element\r\n * (outside the bounds of the child element) will be ignored.\r\n */\r\n constructor(parentElement, childElement, autoStart) {\r\n this.parentElement = parentElement;\r\n this.origScrollTop = 0;\r\n this.origScrollLeft = 0;\r\n this.origMouseX = 0;\r\n this.origMouseY = 0;\r\n this.updateChildElement(this.childElement, true);\r\n if (autoStart) {\r\n this.start();\r\n }\r\n }\r\n\r\n start() {\r\n if (!this.isActive) {\r\n if (this.dragElement) {\r\n this.mouseDownListener = Utils.addEventListener(this.dragElement, 'mousedown', (e) => this.handleMouseDown(e));\r\n }\r\n this.isActive = true;\r\n //console.log('Started');\r\n }\r\n }\r\n\r\n stop() {\r\n if (this.isActive) {\r\n this.isActive = false;\r\n //console.log('Stopped');\r\n Utils.removeEventListener(this.mouseDownListener);\r\n Utils.removeEventListener(this.mouseUpListener);\r\n Utils.removeEventListener(this.mouseMoveListener);\r\n }\r\n }\r\n\r\n updateChildElement(childElement, forceSetter) {\r\n if (forceSetter || childElement !== this.childElement) {\r\n if (this.isActive && this.mouseDownListener) {\r\n Utils.removeEventListener(this.mouseDownListener);\r\n }\r\n this.childElement = childElement;\r\n this.dragElement = this.childElement || this.parentElement;\r\n if (this.isActive && this.dragElement) {\r\n this.mouseDownListener = Utils.addEventListener(this.dragElement, 'mousedown', (e) => this.handleMouseDown(e));\r\n }\r\n }\r\n }\r\n\r\n handleMouseDown(e) {\r\n //console.log('mousedown');\r\n this.mouseUpListener = Utils.addEventListener(window, 'mouseup', (e) => this.handleMouseUp(e));\r\n this.mouseMoveListener = Utils.addEventListener(this.dragElement, 'mousemove', (e) => this.handleMouseMove(e));\r\n\r\n // Save the current scroll position:\r\n this.origScrollLeft = this.parentElement.scrollLeft;\r\n this.origScrollTop = this.parentElement.scrollTop;\r\n // Save the current mouse position:\r\n this.origMouseX = e.clientX;\r\n this.origMouseY = e.clientY;\r\n\r\n this.childElement.style.userSelect = 'none'; // Prevent any drag operation from selecting text\r\n if (this.childElement) {\r\n this.oldCursor = this.childElement.style.cursor;\r\n this.childElement.style.cursor = 'grabbing';\r\n }\r\n this.isDragging = true;\r\n }\r\n\r\n handleMouseUp(e) {\r\n //console.log('mouseup');\r\n this.isDragging = false;\r\n Utils.removeEventListener(this.mouseUpListener);\r\n Utils.removeEventListener(this.mouseMoveListener);\r\n this.childElement.style.removeProperty('user-select'); // Undo the userSelect style previously set\r\n if (this.childElement && typeof this.oldCursor !== 'undefined') {\r\n this.childElement.style.cursor = this.oldCursor;\r\n }\r\n }\r\n\r\n handleMouseMove(e) {\r\n //console.log('mousemove');\r\n // How far the mouse has been moved:\r\n const dx = e.clientX - this.origMouseX;\r\n const dy = e.clientY - this.origMouseY;\r\n\r\n // Scroll the element\r\n this.parentElement.scrollTop = this.origScrollTop - dy;\r\n this.parentElement.scrollLeft = this.origScrollLeft - dx;\r\n }\r\n}\r\n\r\n/**\r\n * Creates a QueryStringObject.query property which is an object where all property names are lowercase.\r\n * Each property is an item in the query string passed to the constructor. If the query string is\r\n * undefined, null, or '', then window.location.search is used instead. Note, this class does not\r\n * support duplicate query string keys.\r\n */\r\nexport class QueryStringAsObject {\r\n constructor(queryString) {\r\n queryString = queryString || window.location.search;\r\n this.parse(queryString);\r\n }\r\n\r\n parse(queryString) {\r\n let query = {};\r\n if (queryString) {\r\n const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');\r\n for (var i = 0; i < pairs.length; i++) {\r\n const pair = pairs[i].split('=');\r\n let key = decodeURIComponent(pair[0]).toLowerCase();\r\n query[key] = decodeURIComponent(pair[1] || '');\r\n }\r\n }\r\n return this.query = query;\r\n }\r\n\r\n parseFullUrl(url) {\r\n if (url) {\r\n const index = url.indexOf('?');\r\n if (index !== -1) {\r\n return this.parse(url.substring(index))\r\n }\r\n }\r\n return this.parse(null);\r\n }\r\n}\r\n\r\nexport class UrlUtils {\r\n\r\n static createQueryString(queryStringParams, ignoreNullValues) {\r\n // First, filter out undefined values:\r\n const paramMatrix = Object.entries(queryStringParams); // A matrix where column 1 has each param key, and column 2 each param value\r\n const callback = (accParams, [key, value]) => {\r\n if (value !== undefined && (!ignoreNullValues || value !== null)) {\r\n accParams[key] = value;\r\n }\r\n return accParams;\r\n };\r\n const definedSearchParams = paramMatrix.reduce(callback, {});\r\n\r\n // Then construct the query string:\r\n return new URLSearchParams(definedSearchParams).toString();\r\n }\r\n\r\n static createUrlWithQueryString(url, queryStringParams, ignoreNullValues) {\r\n const queryString = this.createQueryString(queryStringParams, ignoreNullValues);\r\n return `${url}?${queryString}`;\r\n }\r\n}\r\n\r\nexport class AsyncUtils {\r\n static waitAsync(ms, arg) {\r\n return new Promise(resolve => setTimeout(resolve, ms, arg));\r\n }\r\n}","import { ClientEnvInfo } from './ClientEnvInfo.js';\r\nimport { StringUtils } from './ObjectUtils.js';\r\n\r\nlet ErrorHandler = null;\r\n\r\n/* A note about exception stacks: if you are using your own exception classes, by subclassing the\r\n * Error object, e.g. \"class MyException extends Error\", you gain the benefit of adding a stack\r\n * trace to the exception object (see the stack property). */\r\nexport class GlobalErrorHandler {\r\n constructor(adminService) {\r\n this.adminService = adminService;\r\n // Install global error handler:\r\n window.addEventListener('error', event => this.handleErrorEvent(event));\r\n // This one is handling rejected promises (exceptions inside promises and await calls):\r\n window.addEventListener(\"unhandledrejection\", event => this.handleErrorEvent(event));\r\n ErrorHandler = ErrorHandler || this;\r\n this.envInfo = new ClientEnvInfo();\r\n }\r\n\r\n isExcludedErrorEvent(event) {\r\n const href = window.location.href;\r\n if (href.indexOf('fbclid=') > -1 && href.indexof(':face:') > -1) {\r\n /* This is a request from a Facebook server. It seems the server environment changes\r\n * important aspects of the JS runtime environment causing exceptions to be thrown.\r\n * https://sagapixel.com/facebook-advertising/what-is-fbclid/\r\n * https://github.com/aFarkas/lazysizes/issues/520\r\n */\r\n return true;\r\n } else if (typeof event.reason === 'string' && event.reason.includes('Object Not Found Matching Id')) {\r\n /* This seems to come from a .NET application with an embedded Chromium instance using CEFSharp. See here for more:\r\n * https://forum.sentry.io/t/unhandledrejection-non-error-promise-rejection-captured-with-value/14062/19 */\r\n return true;\r\n }\r\n let message;\r\n if (event.message) {\r\n message = event.message;\r\n } else if (event.reason && event.reason.message) {\r\n message = event.reason.message;\r\n }\r\n if (message) {\r\n if (\r\n message.includes('jQuery is not defined') ||\r\n message.includes('$(...).tooltip is not a function') ||\r\n message.includes('$ is not defined') ||\r\n message.includes('vendorDelayedWaiter error notification: Unable to load script.') ||\r\n message.includes('BABYLON is not defined')\r\n ) {\r\n // Symptom of old incompatible browser, script not loaded...\r\n return true;\r\n } else if (message.includes(`Can't find variable: gmo`)) {\r\n /* This is a recent issue with the Google Search app on iOS. See here for more information:\r\n * https://issuetracker.google.com/issues/396043331. 2/14/2025. */\r\n return true;\r\n }\r\n }\r\n if (event.error && event.error.stack && event.error.stack.includes('chrome-extension')) {\r\n // We don't care about error originating in Chrome extensions\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n addErrorInfo(error, errorInfo) {\r\n const exceptionMessage = errorInfo.ExceptionMessage || errorInfo.exceptionMessage || errorInfo.message;\r\n if (exceptionMessage) {\r\n error.message += '. ' + exceptionMessage;\r\n error.stack = errorInfo.StackTrace || errorInfo.stackTrace || errorInfo.stack || '(See server side error report, i.e. email)';\r\n }\r\n const exceptionType = errorInfo.ExceptionType || errorInfo.exceptionType;\r\n if (exceptionType) {\r\n error.message += '\\r\\nException type: ' + errorInfo.exceptionType;\r\n } else if (StringUtils.isJson(errorInfo)) {\r\n error.message += '\\r\\nError info (json): ' + errorInfo;\r\n }\r\n }\r\n\r\n /**\r\n * We're creating an error object for a failed fetch promise inside one of our HttpService objects.\r\n * This is typically a 500 error on the server communicated back to us.\r\n * @param {any} reason The event.reason object, typically the Error object\r\n * @param {any} ts Our timestamp for the error\r\n */\r\n createHttpServiceError(reason, ts) {\r\n const error = {\r\n message: reason.message,\r\n errObjType: typeof reason,\r\n time: ts.toUTCString(),\r\n source: reason.source,\r\n stack: null,\r\n };\r\n if (reason.errorInfo) {\r\n this.addErrorInfo(error, reason.errorInfo);\r\n }\r\n if (reason.stack) {\r\n error.stack += '\\r\\n\\r\\n----------------------\\r\\n\\r\\n' + reason.stack;\r\n }\r\n if (reason.response) {\r\n if (reason.response.url) {\r\n error.source += ', URL: ' + reason.response.url;\r\n }\r\n if (reason.response.status) {\r\n error.message += '\\r\\nResponse status: ' + reason.response.status;\r\n }\r\n }\r\n return error;\r\n }\r\n\r\n /**\r\n * We're creating an error object for a rejected promise. This typically happens when the promise\r\n * is rejected by calling AbortController.abort().\r\n * @param {any} event A PromiseRejectionEvent object\r\n * @param {any} ts Our timestamp for the error\r\n */\r\n createPromiseRejectionError(event, ts) {\r\n const reason = event.reason;\r\n if (typeof reason !== 'object') {\r\n return {\r\n message: reason,\r\n errObjType: typeof reason,\r\n time: ts.toUTCString(),\r\n source: `${event.constructor.name} of type ${event.type}`,\r\n stack: null,\r\n };\r\n } else {\r\n const error = {\r\n message: reason.message,\r\n errObjType: `${reason.constructor.name} (${reason.name})`,\r\n time: ts.toUTCString(),\r\n source: `${event.constructor.name} of type ${event.type}`,\r\n stack: reason.stack, // ...is normally not assigned\r\n };\r\n if (reason.code) {\r\n error.message += ` (code: ${reason.code})`;\r\n }\r\n return error;\r\n }\r\n }\r\n\r\n async handleErrorEvent(event) {\r\n let error = null;\r\n try {\r\n if (!event || !this.adminService || this.isExcludedErrorEvent(event)) return;\r\n\r\n const ts = new Date();\r\n\r\n if (event.promise && event.reason) {\r\n // We're handling an \"unhandledrejection\" event (rejected promise).\r\n const reason = event.reason;\r\n if (reason.source == 'HttpService') {\r\n /* We're handling a failed fetch promise inside one of our HttpService objects.\r\n * This is typically a 500 error on the server communicated back to us. */\r\n error = this.createHttpServiceError(reason, ts);\r\n } else if (event instanceof PromiseRejectionEvent) {\r\n error = this.createPromiseRejectionError(event, ts);\r\n } else {\r\n event.error = reason;\r\n event.filename = '(Promise)'; // We don't have any filename or source info, just know that the error happened inside a promise\r\n }\r\n }\r\n if (error) {\r\n // This is an internal signal that we have already fully populated the error object.\r\n } else {\r\n error = {\r\n message: event.message,\r\n errObjType: typeof event.error,\r\n time: ts.toUTCString(),\r\n source: event.filename,\r\n lineNo: event.lineno,\r\n colNo: event.colno,\r\n stack: null,\r\n };\r\n if (event.error) {\r\n if (typeof event.error == 'string') {\r\n error.message = event.error;\r\n } else if (event.error.errorInfo) {\r\n this.addErrorInfo(error, event.error.errorInfo);\r\n } else if (typeof event.error == 'object') {\r\n error.message = event.error.message || event.message;\r\n }\r\n if (event.error.stack) {\r\n error.stack = event.error.stack + '\\n\\n' + error.stack;\r\n }\r\n if (event.error.constructor) {\r\n error.errObjType = event.error.constructor.name || error.errObjType;\r\n }\r\n }\r\n }\r\n\r\n this.envInfo.refresh();\r\n error.url = window.location.href;\r\n error.referrer = document.referrer;\r\n error.browser = this.envInfo.device.browser;\r\n error.opSys = this.envInfo.device.opSysAndVersion;\r\n error.innerSize = `${window.innerWidth} x ${window.innerHeight} px`;\r\n // We can do this too:\r\n //error.props = [\r\n // { name: 'User Agent', value: this.envInfo.device.userAgent } // If we want to see the user agent in the error report.\r\n //];\r\n\r\n if (error.message === 'Script error.') {\r\n //error.message += ' (Underlying message is most likely hidden due to the browser\\'s interpretation of CORS security risk.)'\r\n /* 5/10/2021: I'm unable to trace the source of these errors, most likely it's one of the third\r\n * party scripts, e.g. Facebook, GA, CookieConsent. Let's ignore these errors for the time being. */\r\n return;\r\n }\r\n\r\n if (!window.isModuleScriptsSupported) {\r\n error.message +=\r\n '\\r\\nAdditional info:' +\r\n '\\r\\n* Are module scripts supported: NO' +\r\n `\\r\\n* Are ES5 scripts added: ${window.isEs5ScriptsAdded ? 'YES' : 'NO'}` +\r\n `\\r\\n* Are ES5 scripts loaded: ${window.isEs5ScriptsLoaded ? 'YES' : 'NO'}`;\r\n }\r\n\r\n if (this.equalsPriorError(error, ts)) {\r\n // Don't submit error, but increase repetition counter and update timestamp\r\n this.priorError.count++;\r\n this.priorError.ts = ts;\r\n } else {\r\n if (this.hasRepeatedPriorError()) {\r\n await this.submitPriorErrorSummary();\r\n }\r\n this.saveAsPriorError(error, ts);\r\n await this.adminService.submitError(error);\r\n }\r\n } catch (ex) {\r\n // Ignore errors inside the error handler to avoid a never ending loop of reporting errors\r\n console.error(\r\n 'Exception thrown inside the GlobalErrorHandler. The original error has probably not been reported to the server:\\r\\n',\r\n ex, '\\r\\nHere is the original error:\\r\\n', error);\r\n }\r\n }\r\n\r\n saveAsPriorError(error, ts) {\r\n error.ts = ts;\r\n error.count = 1;\r\n this.priorError = error;\r\n }\r\n\r\n equalsPriorError(error, ts) {\r\n return this.priorError &&\r\n error.url === this.priorError.url &&\r\n error.message === this.priorError.message &&\r\n error.source === this.priorError.source &&\r\n ts - this.priorError.ts < 10000;\r\n }\r\n\r\n hasRepeatedPriorError() {\r\n return this.priorError && this.priorError.count > 1;\r\n }\r\n\r\n async submitPriorErrorSummary() {\r\n if (this.priorError) {\r\n this.priorError.props = this.priorError.props || [];\r\n this.priorError.props.push({ name: 'Error Repeat Count', value: this.priorError.count });\r\n await this.adminService.submitError(this.priorError);\r\n }\r\n }\r\n\r\n /**\r\n * Method which can be called directly from user code when an exception is caught,\r\n * but should be reported (i.e. in a catch clause where you're not rethrowing the exception).\r\n * Requires access to a GlobalErrorHandler instance.\r\n */\r\n async handleException(ex) {\r\n const event = {\r\n message: ex.message,\r\n error: ex,\r\n };\r\n await this.handleErrorEvent(event);\r\n }\r\n\r\n /**\r\n * Static method which can be called directly from user code when an exception is caught,\r\n * but should be reported (i.e. in a catch clause where you're not rethrowing the exception).\r\n */\r\n static async HandleException(ex) {\r\n if (ErrorHandler) {\r\n await ErrorHandler.handleException(ex);\r\n }\r\n }\r\n}\r\n\r\n","\r\n/**\r\n * This class is designed to control access to a resource in an environment where multiple asynchronous operations\r\n * may be competing for that resource. See more comprehensive comments below the class for usage and details.\r\n */\r\nexport class AsyncSemaphore {\r\n constructor() {\r\n this.isLocked = false;\r\n this.waiting = [];\r\n }\r\n\r\n lockAsync() {\r\n const unlock = () => {\r\n let nextResolve;\r\n if (this.waiting.length > 0) {\r\n nextResolve = this.waiting.pop(0);\r\n nextResolve(unlock);\r\n } else {\r\n this.isLocked = false;\r\n }\r\n };\r\n\r\n if (this.isLocked) {\r\n return new Promise(resolve => {\r\n this.waiting.push(resolve);\r\n });\r\n } else {\r\n this.isLocked = true;\r\n return new Promise(resolve => {\r\n resolve(unlock);\r\n });\r\n }\r\n }\r\n}\r\n\r\n/* The AsyncSemaphore class was created based on a chat on ChatGPT on 2/1/2024.\r\n See https://chat.openai.com/share/1b1313cd-87cd-4221-a869-05871a1a9252. The chat contains some\r\n other topics as well, just scroll down to the part which shows the code example for a Lock class.\r\n The class is renamed to AsyncSemaphore further down.\r\n\r\n Here's an example of how to use AsyncSemaphore. Assume you have a class Telemetry which accesses\r\n sessionStorage in its getTelemetryIdAsync asynchronous method. The method will get the Id from\r\n sessionStorage if present there. Otherwise it needs to make a call to an external web api to get it,\r\n hence the async nature of it.\r\n\r\n Let's assume that you have more than one instance of the Telemetry class in use on the same page.\r\n That means there's a possibility of re-entrancy of the getTelemetryIdAsync method since instance 2 on\r\n the page might call getTelemetryIdAsync() while instance 1 is awaiting the web api call to complete).\r\n The method get's the Id from the web api and saves it into sessionStorage. But, instance 2 needs to\r\n wait for instance 1 to complete to avoid a race condition where instance 2 also gets an Id from the\r\n web api and overwrites instance 1's id in the sessionStorage.\r\n\r\n To solve this challenge, we create a SessionStorageManager class which wraps the access to sessionStorage\r\n and protects it from re-antrant access and race conditions. In the SessionStorageManager class we create\r\n an instance of AsyncSemaphore and add a lockAccessAsync() async method which is used in the\r\n Telemetry.getTelemetryIdAsync() method. Here's the code:\r\n\r\n class SessionStorageManager {\r\n constructor(storageKey) {\r\n this.storageKey = storageKey;\r\n this._semaphore = new AsyncSemaphore();\r\n }\r\n\r\n async lockAccessAsync() {\r\n return await this._semaphore.lockAsync();\r\n }\r\n\r\n getItem() {\r\n return sessionStorage.getItem(this.storageKey);\r\n }\r\n\r\n setItem(item) {\r\n sessionStorage.setItem(this.storageKey, item);\r\n }\r\n }\r\n\r\n class Telemetry {\r\n async getTelemetryIdAsync() {\r\n const unlock = await sessionStorageMgr.lockAccessAsync(); // This call will block if another instance has already called it\r\n try {\r\n let id = sessionStorageMgr.getItem();\r\n if (!id) {\r\n const id = await getIdFromWebApi(); // Access the external web api:\r\n sessionStorageMgr.setItem(id);\r\n }\r\n return id;\r\n } finally {\r\n unlock(); // This will release the lock and let other instances proceed.\r\n }\r\n }\r\n }\r\n*/\r\n\r\n","import { AsyncSemaphore } from './AsyncSemaphore.js';\r\n\r\n/** This class should only be accessed via the SessionStorageService class. */\r\nclass SessionStorageManager {\r\n constructor(storageKey) {\r\n this.storageKey = storageKey;\r\n // See extensive comments in the AsyncSemaphore class (AsyncSemaphore.js) for more on this:\r\n this._semaphore = new AsyncSemaphore();\r\n }\r\n\r\n /**\r\n * Use it to prevent re-entrant access and race conditions. See extensive comments in the\r\n * AsyncSemaphore class (AsyncSemaphore.js) for more.\r\n * @returns an \"unlock()\" callback.\r\n */\r\n async lockAccessAsync() {\r\n return await this._semaphore.lockAsync();\r\n }\r\n\r\n /* Note, we can't preload or cache the storage object due to concurrency issues related to our\r\n * extended use of async method. For instance, a page with more than one Designer button will\r\n * load the session storage more than once, and it's therefore critical that we return the latest\r\n * version (and not a cached instance). */\r\n getStorage() {\r\n const storageAsStr = sessionStorage.getItem(this.storageKey);\r\n const current = storageAsStr ? JSON.parse(storageAsStr) : {};\r\n if (this._storage) {\r\n Object.assign(this._storage, current);\r\n } else {\r\n this._storage = current;\r\n }\r\n return this._storage;\r\n }\r\n\r\n save() {\r\n if (this._storage) {\r\n sessionStorage.setItem(this.storageKey, JSON.stringify(this._storage));\r\n } else {\r\n sessionStorage.removeItem(this.storageKey);\r\n }\r\n return this._storage;\r\n }\r\n}\r\n\r\n/** Allows shared, protected access to the sessionStorage built-in object. */\r\nexport class SessionStorageService {\r\n constructor() {\r\n this.storageManagers = {}\r\n }\r\n\r\n requestStorageManager(key) {\r\n let manager = this.storageManagers[key];\r\n if (!manager) {\r\n this.storageManagers[key] = manager = new SessionStorageManager(key);\r\n }\r\n return manager;\r\n }\r\n}\r\n","/**\r\n * Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie/Simple_document.cookie_framework\r\n */\r\nexport class Cookies {\r\n static getValue(name) {\r\n if (!name) {\r\n return null;\r\n }\r\n const encodedKey = encodeURIComponent(name);\r\n const replacedEncodedKey = encodedKey.replace(/[-.+*]/g, '\\\\$&');\r\n const regex = new RegExp('(?:(?:^|.*;)\\\\s*' + replacedEncodedKey + '\\\\s*\\\\=\\\\s*([^;]*).*$)|^.*$');\r\n return decodeURIComponent(document.cookie.replace(regex, '$1')) || null;\r\n }\r\n\r\n /**\r\n * Sets a cookie with all parameters.\r\n * @param {string} name\r\n * @param {string} value\r\n * @param {(number|string|Date)} [expiresOrMaxAge] - Can either be a number (including Infinity) representing a max-age,\r\n * or a string or Date representing the cookie's expiration date.\r\n * @param {string} [path]\r\n * @param {string} [domain]\r\n * @param {boolean} [secure]\r\n * @param {string} [sameSite]\r\n * @returns {boolean} True if a value was set, otherwise false.\r\n */\r\n static setValue(name, value, expiresOrMaxAge, path, domain, secure, sameSite) {\r\n if (!name || /^(?:expires|max-age|path|domain|secure)$/i.test(name)) {\r\n return false;\r\n }\r\n let expires = '';\r\n if (expiresOrMaxAge) {\r\n switch (expiresOrMaxAge.constructor) {\r\n case Number:\r\n expires = expiresOrMaxAge === Infinity\r\n ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT'\r\n : '; max-age=' + expiresOrMaxAge;\r\n /*\r\n Note: Despite officially defined in RFC 6265, the use of `max-age` is not compatible with any\r\n version of Internet Explorer, Edge and some mobile browsers. Therefore passing a number to\r\n the end parameter might not work as expected. A possible solution might be to convert the the\r\n relative time to an absolute time. For instance, replacing the previous line with:\r\n */\r\n /*\r\n sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : '; expires=' + (new Date(vEnd * 1e3 + Date.now())).toUTCString();\r\n */\r\n break;\r\n case String:\r\n expires = '; expires=' + expiresOrMaxAge;\r\n break;\r\n case Date:\r\n expires = '; expires=' + expiresOrMaxAge.toUTCString();\r\n break;\r\n }\r\n }\r\n document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) +\r\n expires +\r\n (domain ? '; domain=' + domain : '') +\r\n (path ? '; path=' + path : '') +\r\n (sameSite ? '; samesite=' + sameSite : '') +\r\n (secure ? '; secure' : '');\r\n return true;\r\n }\r\n\r\n /**\r\n * Removes the cookie.\r\n * @param {string} name\r\n * @param {string} [path]\r\n * @param {string} [domain]\r\n */\r\n static removeValue(name, path, domain) {\r\n if (!this.exists(name)) {\r\n return false;\r\n }\r\n document.cookie = encodeURIComponent(name) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +\r\n (domain ? '; domain=' + domain : '') + (path ? '; path=' + path : '');\r\n return true;\r\n }\r\n\r\n /**\r\n * Returns true if the cookie exists.\r\n */\r\n static exists(name) {\r\n if (!name || /^(?:expires|max-age|path|domain|secure)$/i.test(name)) {\r\n return false;\r\n }\r\n const pattern = '(?:^|;\\\\s*)' + encodeURIComponent(name).replace(/[-.+*]/g, '\\\\$&') + '\\\\s*\\\\=';\r\n return (new RegExp(pattern)).test(document.cookie);\r\n }\r\n\r\n /** Returns an array of all readable cookies at this location. */\r\n static keys() {\r\n const keysStr = document.cookie.replace(/((?:^|\\s*;)[^=]+)(?=;|$)|^\\s*|\\s*(?:=[^;]*)?(?:\\1|$)/g, '');\r\n const keys = keysStr.split(/\\s*(?:=[^;]*)?;\\s*/);\r\n for (var len = keys.length, i = 0; i < len; i++) {\r\n keys[i] = decodeURIComponent(keys[i]);\r\n }\r\n return keys;\r\n }\r\n}","/* global jQuery */\r\nexport class ModalPopup {\r\n constructor(config, defaultOptions) {\r\n this.config = config;\r\n this.defaultOptions = this.normalizeOptions(defaultOptions);\r\n }\r\n\r\n setup() {\r\n if (!this.isSetup) {\r\n this.options = this.normalizeOptions(null);\r\n\r\n const elements = this.config.Elements;\r\n\r\n // If the popup is wrapped in a