{"version":3,"file":"gallery.js","sources":["../../node_modules/@lunaweb/vega-reactor-js/src/patterns/Component.js","../../node_modules/parvus/dist/js/parvus.esm.js","../../sources/javascripts/front/components/Gallery.js","../../sources/javascripts/gallery.js"],"sourcesContent":["/**\n * Name: Component\n * Description: Base model for all components\n */\n\nexport default class Component {\n static settings = {};\n\n #element;\n #mounted;\n #settings;\n\n #listeners = {};\n\n constructor (element, options = {}) {\n this.#element = element;\n this.#mounted = false;\n this.#settings = {\n ...Component.settings,\n ...options\n };\n }\n\n get element () {\n return this.#element;\n }\n\n get mounted () {\n return this.#mounted;\n }\n\n get settings () {\n return this.#settings;\n }\n\n getNode (parameter) {\n let element = null;\n\n if (parameter instanceof Element) {\n element = parameter;\n } else if (typeof parameter === 'string') {\n const testSelector = this.#element.querySelector(parameter);\n element = testSelector ? testSelector : element;\n }\n\n return element;\n }\n\n fire (type, data) {\n var listeners = this.#listeners[type] || [];\n\n listeners.forEach(function (listener) {\n listener(data);\n });\n }\n\n on (type, handler) {\n if (typeof this.#listeners[type] === 'undefined') {\n this.#listeners[type] = [];\n }\n\n this.#listeners[type].push(handler);\n\n return this;\n }\n\n off (type, handler) {\n var index = this.#listeners[type].indexOf(handler);\n\n if (index > -1) {\n this.#listeners[type].splice(index, 1);\n }\n\n return this;\n }\n\n mount () {\n this.#element.classList.add('is-mounted');\n this.#mounted = true;\n this.fire('mount', this);\n }\n\n unmount () {\n this.#element.classList.remove('is-mounted');\n this.#mounted = false;\n this.fire('unmount', this);\n }\n}\n","/**\n * Parvus\n *\n * @author Benjamin de Oostfrees\n * @version 2.4.0\n * @url https://github.com/deoostfrees/parvus\n *\n * MIT license\n */\n\nvar en = {\n lightboxLabel: 'This is a dialog window that overlays the main content of the page. The modal displays the enlarged image. Pressing the Escape key will close the modal and bring you back to where you were on the page.',\n lightboxLoadingIndicatorLabel: 'Image loading',\n lightboxLoadingError: 'The requested image cannot be loaded.',\n controlsLabel: 'Controls',\n previousButtonLabel: 'Previous image',\n nextButtonLabel: 'Next image',\n closeButtonLabel: 'Close dialog window',\n sliderLabel: 'Images',\n slideLabel: 'Image'\n};\n\n// Default language\nfunction Parvus(userOptions) {\n /**\n * Global variables\n *\n */\n const BROWSER_WINDOW = window;\n const FOCUSABLE_ELEMENTS = ['a:not([inert]):not([tabindex^=\"-\"])', 'button:not([inert]):not([tabindex^=\"-\"]):not(:disabled)', '[tabindex]:not([inert]):not([tabindex^=\"-\"])'];\n const GROUP_ATTRIBUTES = {\n triggerElements: [],\n slider: null,\n sliderElements: [],\n contentElements: []\n };\n const GROUPS = {};\n let groupIdCounter = 0;\n let newGroup = null;\n let activeGroup = null;\n let currentIndex = 0;\n let config = {};\n let lightbox = null;\n let lightboxOverlay = null;\n let lightboxOverlayOpacity = 1;\n let toolbar = null;\n let toolbarLeft = null;\n let toolbarRight = null;\n let controls = null;\n let previousButton = null;\n let nextButton = null;\n let closeButton = null;\n let counter = null;\n let drag = {};\n let isDraggingX = false;\n let isDraggingY = false;\n let pointerDown = false;\n let lastFocus = null;\n let offset = null;\n let offsetTmp = null;\n let resizeTicking = false;\n let transitionDuration = null;\n let isReducedMotion = true;\n\n /**\n * Merge default options with user-provided options\n *\n * @param {Object} userOptions - User-provided options\n * @returns {Object} - Merged options object\n */\n const mergeOptions = userOptions => {\n // Default options\n const DEFAULT_OPTIONS = {\n selector: '.lightbox',\n gallerySelector: null,\n captions: true,\n captionsSelector: 'self',\n captionsAttribute: 'data-caption',\n docClose: true,\n swipeClose: true,\n simulateTouch: true,\n threshold: 50,\n backFocus: true,\n hideScrollbar: true,\n transitionDuration: 300,\n transitionTimingFunction: 'cubic-bezier(0.62, 0.16, 0.13, 1.01)',\n lightboxIndicatorIcon: '',\n previousButtonIcon: '',\n nextButtonIcon: '',\n closeButtonIcon: '',\n l10n: en\n };\n return {\n ...DEFAULT_OPTIONS,\n ...userOptions\n };\n };\n\n /**\n * Check prefers reduced motion\n * https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList\n *\n */\n const MOTIONQUERY = window.matchMedia('(prefers-reduced-motion)');\n const reducedMotionCheck = () => {\n if (MOTIONQUERY.matches) {\n isReducedMotion = true;\n transitionDuration = 0.1;\n } else {\n isReducedMotion = false;\n transitionDuration = config.transitionDuration;\n }\n };\n\n // Check for any OS level changes to the preference\n MOTIONQUERY.addEventListener('change', reducedMotionCheck);\n\n /**\n * Get scrollbar width\n *\n * @return {Number} - The scrollbar width\n */\n const getScrollbarWidth = () => {\n return BROWSER_WINDOW.innerWidth - document.documentElement.clientWidth;\n };\n\n /**\n * Get the group from element\n *\n * @param {HTMLElement} el - The element to retrieve the group from\n * @return {String} - The group of the element\n */\n const getGroup = el => {\n // Check if the data attribute \"group\" exists or set an alternative value\n const EL_GROUP = el.dataset.group || `default-${groupIdCounter}`;\n ++groupIdCounter;\n\n // Set the \"group\" data attribute if it doesn't exist\n if (!el.hasAttribute('data-group')) {\n el.setAttribute('data-group', EL_GROUP);\n }\n return EL_GROUP;\n };\n\n /**\n * Add zoom indicator to element\n *\n * @param {HTMLElement} el - The element to add the zoom indicator to\n */\n const addZoomIndicator = el => {\n if (el.querySelector('img')) {\n const LIGHTBOX_INDICATOR_ICON = document.createElement('div');\n el.classList.add('parvus-zoom');\n LIGHTBOX_INDICATOR_ICON.className = 'parvus-zoom__indicator';\n LIGHTBOX_INDICATOR_ICON.innerHTML = config.lightboxIndicatorIcon;\n el.appendChild(LIGHTBOX_INDICATOR_ICON);\n }\n };\n\n /**\n * Add an element\n *\n * @param {HTMLElement} el - The element to be added\n */\n const add = el => {\n if (!(el.tagName === 'A' && el.hasAttribute('href') || el.tagName === 'BUTTON' && el.hasAttribute('data-target'))) {\n throw new Error('Use a link with the \\'href\\' attribute or a button with the \\'data-target\\' attribute. Both attributes must have a path to the image file.');\n }\n newGroup = getGroup(el);\n if (!GROUPS[newGroup]) {\n GROUPS[newGroup] = structuredClone(GROUP_ATTRIBUTES);\n }\n if (GROUPS[newGroup].triggerElements.includes(el)) {\n throw new Error('Ups, element already added.');\n }\n GROUPS[newGroup].triggerElements.push(el);\n addZoomIndicator(el);\n el.classList.add('parvus-trigger');\n el.addEventListener('click', triggerParvus);\n if (isOpen() && newGroup === activeGroup) {\n const EL_INDEX = GROUPS[newGroup].triggerElements.indexOf(el);\n createSlide(EL_INDEX);\n createImage(el, EL_INDEX, () => {\n loadImage(EL_INDEX);\n });\n updateAttributes();\n updateSliderNavigationStatus();\n updateCounter();\n }\n };\n\n /**\n * Remove an element\n *\n * @param {HTMLElement} el - The element to be removed\n */\n const remove = el => {\n if (!el || !el.hasAttribute('data-group')) {\n return;\n }\n const EL_GROUP = getGroup(el);\n\n // Check if element exists\n if (!GROUPS[EL_GROUP] || !GROUPS[EL_GROUP].triggerElements.includes(el)) {\n return;\n }\n const EL_INDEX = GROUPS[EL_GROUP].triggerElements.indexOf(el);\n GROUPS[EL_GROUP].triggerElements.splice(EL_INDEX, 1);\n GROUPS[EL_GROUP].sliderElements.splice(EL_INDEX, 1);\n\n // Remove lightbox indicator icon if necessary\n if (el.classList.contains('parvus-zoom')) {\n const LIGHTBOX_INDICATOR_ICON = el.querySelector('.parvus-zoom__indicator');\n el.classList.remove('parvus-zoom');\n el.removeChild(LIGHTBOX_INDICATOR_ICON);\n }\n if (isOpen() && EL_GROUP === activeGroup) {\n updateAttributes();\n updateSliderNavigationStatus();\n updateCounter();\n }\n\n // Unbind click event handler\n el.removeEventListener('click', triggerParvus);\n el.classList.remove('parvus-trigger');\n };\n\n /**\n * Create the lightbox\n *\n */\n const createLightbox = () => {\n // Create the lightbox container\n lightbox = document.createElement('div');\n lightbox.setAttribute('role', 'dialog');\n lightbox.setAttribute('aria-modal', 'true');\n lightbox.setAttribute('aria-hidden', 'true');\n lightbox.setAttribute('tabindex', '-1');\n lightbox.setAttribute('aria-label', config.l10n.lightboxLabel);\n lightbox.classList.add('parvus');\n\n // Create the lightbox overlay container\n lightboxOverlay = document.createElement('div');\n lightboxOverlay.classList.add('parvus__overlay');\n\n // Add the lightbox overlay container to the lightbox container\n lightbox.appendChild(lightboxOverlay);\n\n // Create the toolbar\n toolbar = document.createElement('div');\n toolbar.className = 'parvus__toolbar';\n\n // Create the toolbar items\n toolbarLeft = document.createElement('div');\n toolbarRight = document.createElement('div');\n\n // Create the controls\n controls = document.createElement('div');\n controls.className = 'parvus__controls';\n controls.setAttribute('role', 'group');\n controls.setAttribute('aria-label', config.l10n.controlsLabel);\n\n // Add the controls to the right toolbar item\n toolbarRight.appendChild(controls);\n\n // Create the close button\n closeButton = document.createElement('button');\n closeButton.className = 'parvus__btn parvus__btn--close';\n closeButton.setAttribute('type', 'button');\n closeButton.setAttribute('aria-label', config.l10n.closeButtonLabel);\n closeButton.innerHTML = config.closeButtonIcon;\n\n // Add the close button to the controls\n controls.appendChild(closeButton);\n\n // Create the previous button\n previousButton = document.createElement('button');\n previousButton.className = 'parvus__btn parvus__btn--previous';\n previousButton.setAttribute('type', 'button');\n previousButton.setAttribute('aria-label', config.l10n.previousButtonLabel);\n previousButton.innerHTML = config.previousButtonIcon;\n\n // Add the previous button to the controls\n controls.appendChild(previousButton);\n\n // Create the next button\n nextButton = document.createElement('button');\n nextButton.className = 'parvus__btn parvus__btn--next';\n nextButton.setAttribute('type', 'button');\n nextButton.setAttribute('aria-label', config.l10n.nextButtonLabel);\n nextButton.innerHTML = config.nextButtonIcon;\n\n // Add the next button to the controls\n controls.appendChild(nextButton);\n\n // Create the counter\n counter = document.createElement('div');\n counter.className = 'parvus__counter';\n\n // Add the counter to the left toolbar item\n toolbarLeft.appendChild(counter);\n\n // Add the toolbar items to the toolbar\n toolbar.appendChild(toolbarLeft);\n toolbar.appendChild(toolbarRight);\n\n // Add the toolbar to the lightbox container\n lightbox.appendChild(toolbar);\n\n // Add the lightbox container to the body\n document.body.appendChild(lightbox);\n };\n\n /**\n * Create a slider\n *\n */\n const createSlider = () => {\n const SLIDER = document.createElement('div');\n SLIDER.className = 'parvus__slider';\n\n // Hide the slider\n SLIDER.setAttribute('aria-hidden', 'true');\n\n // Update the slider reference in GROUPS\n GROUPS[activeGroup].slider = SLIDER;\n\n // Add the slider to the lightbox container\n lightbox.appendChild(SLIDER);\n };\n\n /**\n * Get next slide index\n *\n * @param {Number} index\n */\n const getNextSlideIndex = currentIndex => {\n const SLIDE_ELEMENTS = GROUPS[activeGroup].sliderElements;\n const TOTAL_SLIDE_ELEMENTS = SLIDE_ELEMENTS.length;\n for (let i = currentIndex + 1; i < TOTAL_SLIDE_ELEMENTS; i++) {\n if (SLIDE_ELEMENTS[i] !== undefined) {\n return i;\n }\n }\n return -1;\n };\n\n /**\n * Get previous slide index\n *\n * @param {number} index - The current slide index\n * @returns {number} - The index of the previous slide, or -1 if there is no previous slide\n */\n const getPreviousSlideIndex = currentIndex => {\n const SLIDE_ELEMENTS = GROUPS[activeGroup].sliderElements;\n for (let i = currentIndex - 1; i >= 0; i--) {\n if (SLIDE_ELEMENTS[i] !== undefined) {\n return i;\n }\n }\n return -1;\n };\n\n /**\n * Create a slide\n *\n * @param {Number} index - The index of the slide\n */\n const createSlide = index => {\n if (GROUPS[activeGroup].sliderElements[index] !== undefined) {\n return;\n }\n const SLIDER_ELEMENT = document.createElement('div');\n const SLIDER_ELEMENT_CONTENT = document.createElement('div');\n const TRIGGER_ELEMENTS = GROUPS[activeGroup].triggerElements;\n const TOTAL_TRIGGER_ELEMENTS = TRIGGER_ELEMENTS.length;\n SLIDER_ELEMENT.className = 'parvus__slide';\n SLIDER_ELEMENT.style.position = 'absolute';\n SLIDER_ELEMENT.style.left = `${index * 100}%`;\n SLIDER_ELEMENT.setAttribute('aria-hidden', 'true');\n SLIDER_ELEMENT.appendChild(SLIDER_ELEMENT_CONTENT);\n\n // Add extra output for screen reader if there is more than one slide\n if (TOTAL_TRIGGER_ELEMENTS > 1) {\n SLIDER_ELEMENT.setAttribute('role', 'group');\n SLIDER_ELEMENT.setAttribute('aria-label', `${config.l10n.slideLabel} ${index + 1}/${TOTAL_TRIGGER_ELEMENTS}`);\n }\n GROUPS[activeGroup].sliderElements[index] = SLIDER_ELEMENT;\n if (index >= currentIndex) {\n const NEXT_SLIDE_INDEX = getNextSlideIndex(index);\n if (NEXT_SLIDE_INDEX !== -1) {\n GROUPS[activeGroup].sliderElements[NEXT_SLIDE_INDEX].before(SLIDER_ELEMENT);\n } else {\n GROUPS[activeGroup].slider.appendChild(SLIDER_ELEMENT);\n }\n } else {\n const PREVIOUS_SLIDE_INDEX = getPreviousSlideIndex(index);\n if (PREVIOUS_SLIDE_INDEX !== -1) {\n GROUPS[activeGroup].sliderElements[PREVIOUS_SLIDE_INDEX].after(SLIDER_ELEMENT);\n } else {\n GROUPS[activeGroup].slider.prepend(SLIDER_ELEMENT);\n }\n }\n };\n\n /**\n * Open Parvus\n *\n * @param {HTMLElement} el\n */\n const open = el => {\n if (!lightbox || !el || !el.classList.contains('parvus-trigger') || isOpen()) {\n return;\n }\n activeGroup = getGroup(el);\n if (!GROUPS[activeGroup].triggerElements.includes(el)) {\n throw new Error('Ups, I can\\'t find the element.');\n }\n currentIndex = GROUPS[activeGroup].triggerElements.indexOf(el);\n lastFocus = document.activeElement;\n history.pushState({\n parvus: 'close'\n }, 'Image', window.location.href);\n bindEvents();\n const NON_LIGHTBOX_ELEMENTS = document.querySelectorAll('body > *:not([aria-hidden=\"true\"])');\n NON_LIGHTBOX_ELEMENTS.forEach(nonLightboxEl => {\n nonLightboxEl.setAttribute('aria-hidden', 'true');\n nonLightboxEl.classList.add('parvus-hidden');\n });\n if (config.hideScrollbar) {\n document.body.style.marginInlineEnd = `${getScrollbarWidth()}px`;\n document.body.style.overflow = 'hidden';\n }\n lightbox.classList.add('parvus--is-opening');\n lightbox.setAttribute('aria-hidden', 'false');\n createSlider();\n createSlide(currentIndex);\n GROUPS[activeGroup].slider.setAttribute('aria-hidden', 'false');\n updateOffset();\n updateAttributes();\n updateSliderNavigationStatus();\n updateCounter();\n setFocusToFirstItem();\n loadSlide(currentIndex);\n createImage(el, currentIndex, () => {\n loadImage(currentIndex, true);\n lightbox.classList.remove('parvus--is-opening');\n GROUPS[activeGroup].slider.classList.add('parvus__slider--animate');\n });\n preload(currentIndex + 1);\n preload(currentIndex - 1);\n\n // Create and dispatch a new event\n fire('open', {\n source: el\n });\n document.body.classList.add('parvus-is-open');\n };\n\n /**\n * Close Parvus\n *\n */\n const close = () => {\n if (!isOpen()) {\n throw new Error('Ups, I\\'m already closed.');\n }\n const IMAGE = GROUPS[activeGroup].contentElements[currentIndex];\n const THUMBNAIL = GROUPS[activeGroup].triggerElements[currentIndex];\n unbindEvents();\n clearDrag();\n if (history.state?.parvus === 'close') {\n history.back();\n }\n const NON_LIGHTBOX_ELEMENTS = document.querySelectorAll('.parvus-hidden');\n NON_LIGHTBOX_ELEMENTS.forEach(nonLightboxEl => {\n nonLightboxEl.removeAttribute('aria-hidden');\n nonLightboxEl.classList.remove('parvus-hidden');\n });\n lightbox.classList.add('parvus--is-closing');\n requestAnimationFrame(() => {\n const THUMBNAIL_SIZE = THUMBNAIL.getBoundingClientRect();\n if (IMAGE && IMAGE.tagName === 'IMG') {\n const IMAGE_SIZE = IMAGE.getBoundingClientRect();\n const WIDTH_DIFFERENCE = THUMBNAIL_SIZE.width / IMAGE_SIZE.width;\n const HEIGHT_DIFFERENCE = THUMBNAIL_SIZE.height / IMAGE_SIZE.height;\n const X_DIFFERENCE = THUMBNAIL_SIZE.left - IMAGE_SIZE.left;\n const Y_DIFFERENCE = THUMBNAIL_SIZE.top - IMAGE_SIZE.top;\n IMAGE.style.transform = `translate(${X_DIFFERENCE}px, ${Y_DIFFERENCE}px) scale(${WIDTH_DIFFERENCE}, ${HEIGHT_DIFFERENCE})`;\n }\n IMAGE.style.opacity = 0;\n IMAGE.style.transition = `transform ${transitionDuration}ms ${config.transitionTimingFunction}, opacity ${transitionDuration}ms ${config.transitionTimingFunction} ${transitionDuration / 2}ms`;\n });\n const transitionendHandler = () => {\n leaveSlide(currentIndex);\n lastFocus = config.backFocus ? lastFocus : GROUPS[activeGroup].triggerElements[currentIndex];\n lastFocus.focus({\n preventScroll: true\n });\n lightbox.setAttribute('aria-hidden', 'true');\n lightbox.classList.remove('parvus--is-closing');\n lightbox.classList.remove('parvus--is-vertical-closing');\n IMAGE.style.transform = '';\n IMAGE.removeEventListener('transitionend', transitionendHandler);\n GROUPS[activeGroup].slider.remove();\n GROUPS[activeGroup].slider = null;\n GROUPS[activeGroup].sliderElements = [];\n GROUPS[activeGroup].contentElements = [];\n counter.removeAttribute('aria-hidden');\n previousButton.removeAttribute('aria-hidden');\n previousButton.removeAttribute('aria-disabled');\n nextButton.removeAttribute('aria-hidden');\n nextButton.removeAttribute('aria-disabled');\n if (config.hideScrollbar) {\n document.body.style.marginInlineEnd = '';\n document.body.style.overflow = '';\n }\n };\n IMAGE.addEventListener('transitionend', transitionendHandler, {\n once: true\n });\n\n // Create and dispatch a new event\n fire('close', {\n detail: {\n source: GROUPS[activeGroup].triggerElements[currentIndex]\n }\n });\n document.body.classList.remove('parvus-is-open');\n };\n\n /**\n * Preload slide with the specified index\n *\n * @param {Number} index - The index of the slide to be preloaded\n */\n const preload = index => {\n if (index < 0 || index >= GROUPS[activeGroup].triggerElements.length || GROUPS[activeGroup].sliderElements[index] !== undefined) {\n return;\n }\n createSlide(index);\n createImage(GROUPS[activeGroup].triggerElements[index], index, () => {\n loadImage(index);\n });\n };\n\n /**\n * Load slide with the specified index\n *\n * @param {Number} index - The index of the slide to be loaded\n */\n const loadSlide = index => {\n GROUPS[activeGroup].sliderElements[index].setAttribute('aria-hidden', 'false');\n };\n\n /**\n * Add caption to the container element\n *\n * @param {HTMLElement} containerEl - The container element to which the caption will be added\n * @param {HTMLElement} el - The trigger element associated with the caption\n * @param {Number} index - The index of the caption\n */\n const addCaption = (containerEl, el, index) => {\n const CAPTION_CONTAINER = document.createElement('div');\n let captionData = null;\n CAPTION_CONTAINER.className = 'parvus__caption';\n if (config.captionsSelector === 'self') {\n if (el.hasAttribute(config.captionsAttribute) && el.getAttribute(config.captionsAttribute) !== '') {\n captionData = el.getAttribute(config.captionsAttribute);\n }\n } else {\n const CAPTION_SELECTOR = el.querySelector(config.captionsSelector);\n if (CAPTION_SELECTOR !== null) {\n if (CAPTION_SELECTOR.hasAttribute(config.captionsAttribute) && CAPTION_SELECTOR.getAttribute(config.captionsAttribute) !== '') {\n captionData = CAPTION_SELECTOR.getAttribute(config.captionsAttribute);\n } else {\n captionData = CAPTION_SELECTOR.innerHTML;\n }\n }\n }\n if (captionData !== null) {\n const CAPTION_ID = `parvus__caption-${index}`;\n CAPTION_CONTAINER.setAttribute('aria-labelledby', CAPTION_ID);\n CAPTION_CONTAINER.id = CAPTION_ID;\n CAPTION_CONTAINER.innerHTML = `
${captionData}
`;\n containerEl.appendChild(CAPTION_CONTAINER);\n }\n };\n const createImage = (el, index, callback) => {\n const {\n contentElements,\n sliderElements\n } = GROUPS[activeGroup];\n if (contentElements[index] !== undefined) {\n if (callback && typeof callback === 'function') {\n callback();\n }\n return;\n }\n const CONTENT_CONTAINER_EL = sliderElements[index].querySelector('div');\n const IMAGE = new Image();\n const IMAGE_CONTAINER = document.createElement('div');\n const THUMBNAIL = el.querySelector('img');\n const LOADING_INDICATOR = document.createElement('div');\n IMAGE_CONTAINER.className = 'parvus__content';\n\n // Create loading indicator\n LOADING_INDICATOR.className = 'parvus__loader';\n LOADING_INDICATOR.setAttribute('role', 'progressbar');\n LOADING_INDICATOR.setAttribute('aria-label', config.l10n.lightboxLoadingIndicatorLabel);\n\n // Add loading indicator to content container\n CONTENT_CONTAINER_EL.appendChild(LOADING_INDICATOR);\n const checkImagePromise = new Promise((resolve, reject) => {\n IMAGE.onload = () => resolve(IMAGE);\n IMAGE.onerror = error => reject(error);\n });\n checkImagePromise.then(loadedImage => {\n loadedImage.style.opacity = 0;\n IMAGE_CONTAINER.appendChild(loadedImage);\n CONTENT_CONTAINER_EL.appendChild(IMAGE_CONTAINER);\n\n // Add caption if available\n if (config.captions) {\n addCaption(CONTENT_CONTAINER_EL, el, index);\n }\n contentElements[index] = loadedImage;\n\n // Set image width and height\n loadedImage.setAttribute('width', loadedImage.naturalWidth);\n loadedImage.setAttribute('height', loadedImage.naturalHeight);\n\n // Set image dimension\n setImageDimension(sliderElements[index], loadedImage);\n }).catch(() => {\n const ERROR_CONTAINER = document.createElement('div');\n ERROR_CONTAINER.classList.add('parvus__content');\n ERROR_CONTAINER.classList.add('parvus__content--error');\n ERROR_CONTAINER.innerHTML = `${config.l10n.lightboxLoadingError}`;\n CONTENT_CONTAINER_EL.appendChild(ERROR_CONTAINER);\n contentElements[index] = ERROR_CONTAINER;\n }).finally(() => {\n CONTENT_CONTAINER_EL.removeChild(LOADING_INDICATOR);\n if (callback && typeof callback === 'function') {\n callback();\n }\n });\n\n // Add `sizes` attribute\n if (el.hasAttribute('data-sizes') && el.getAttribute('data-sizes') !== '') {\n IMAGE.setAttribute('sizes', el.getAttribute('data-sizes'));\n }\n\n // Add `srcset` attribute\n if (el.hasAttribute('data-srcset') && el.getAttribute('data-srcset') !== '') {\n IMAGE.setAttribute('srcset', el.getAttribute('data-srcset'));\n }\n\n // Add `src` attribute\n if (el.tagName === 'A') {\n IMAGE.setAttribute('src', el.href);\n } else {\n IMAGE.setAttribute('src', el.getAttribute('data-target'));\n }\n\n // `alt` attribute\n if (THUMBNAIL && THUMBNAIL.hasAttribute('alt') && THUMBNAIL.getAttribute('alt') !== '') {\n IMAGE.alt = THUMBNAIL.alt;\n } else if (el.hasAttribute('data-alt') && el.getAttribute('data-alt') !== '') {\n IMAGE.alt = el.getAttribute('data-alt');\n } else {\n IMAGE.alt = '';\n }\n };\n\n /**\n * Load Image\n *\n * @param {Number} index - The index of the image to load\n */\n const loadImage = (index, animate) => {\n const IMAGE = GROUPS[activeGroup].contentElements[index];\n if (IMAGE && IMAGE.tagName === 'IMG') {\n const THUMBNAIL = GROUPS[activeGroup].triggerElements[index];\n if (animate) {\n const IMAGE_SIZE = IMAGE.getBoundingClientRect();\n const THUMBNAIL_SIZE = THUMBNAIL.getBoundingClientRect();\n const WIDTH_DIFFERENCE = THUMBNAIL_SIZE.width / IMAGE_SIZE.width;\n const HEIGHT_DIFFERENCE = THUMBNAIL_SIZE.height / IMAGE_SIZE.height;\n const X_DIFFERENCE = THUMBNAIL_SIZE.left - IMAGE_SIZE.left;\n const Y_DIFFERENCE = THUMBNAIL_SIZE.top - IMAGE_SIZE.top;\n requestAnimationFrame(() => {\n IMAGE.style.transform = `translate(${X_DIFFERENCE}px, ${Y_DIFFERENCE}px) scale(${WIDTH_DIFFERENCE}, ${HEIGHT_DIFFERENCE})`;\n IMAGE.style.transition = 'transform 0s, opacity 0s';\n\n // Animate the difference reversal on the next tick\n requestAnimationFrame(() => {\n IMAGE.style.transform = '';\n IMAGE.style.opacity = 1;\n IMAGE.style.transition = `transform ${transitionDuration}ms ${config.transitionTimingFunction}, opacity ${transitionDuration / 2}ms ${config.transitionTimingFunction}`;\n });\n });\n } else {\n IMAGE.style.opacity = 1;\n }\n } else {\n IMAGE.style.opacity = 1;\n }\n };\n const select = index => {\n const OLD_INDEX = currentIndex;\n if (!isOpen()) {\n throw new Error(\"Oops, I'm closed.\");\n } else {\n if (typeof index !== 'number' || isNaN(index)) {\n throw new Error('Oops, no slide specified.');\n }\n const triggerElements = GROUPS[activeGroup].triggerElements;\n if (index === currentIndex) {\n throw new Error(`Oops, slide ${index} is already selected.`);\n }\n if (index < -1 || index >= triggerElements.length) {\n throw new Error(`Oops, I can't find slide ${index}.`);\n }\n }\n if (GROUPS[activeGroup].sliderElements[index] !== undefined) {\n loadSlide(index);\n } else {\n createSlide(index);\n createImage(GROUPS[activeGroup].triggerElements[index], index, () => {\n loadImage(index);\n });\n loadSlide(index);\n }\n currentIndex = index;\n updateOffset();\n if (index < OLD_INDEX) {\n updateSliderNavigationStatus();\n preload(index - 1);\n } else if (index > OLD_INDEX) {\n updateSliderNavigationStatus();\n preload(index + 1);\n }\n leaveSlide(OLD_INDEX);\n updateCounter();\n\n // Create and dispatch a new event\n fire('select', {\n detail: {\n source: GROUPS[activeGroup].triggerElements[currentIndex]\n }\n });\n };\n\n /**\n * Select the previous slide\n *\n */\n const previous = () => {\n if (currentIndex > 0) {\n select(currentIndex - 1);\n } else {\n const {\n slider\n } = GROUPS[activeGroup];\n const offset = offsetTmp + config.threshold;\n requestAnimationFrame(() => {\n slider.style.transform = `translate3d(${offset}px, 0, 0)`;\n setTimeout(() => {\n updateOffset();\n }, 150);\n });\n }\n };\n\n /**\n * Select the next slide\n *\n */\n const next = () => {\n const {\n slider,\n triggerElements\n } = GROUPS[activeGroup];\n if (currentIndex < triggerElements.length - 1) {\n select(currentIndex + 1);\n } else {\n const offset = offsetTmp - config.threshold;\n requestAnimationFrame(() => {\n slider.style.transform = `translate3d(${offset}px, 0, 0)`;\n setTimeout(() => {\n updateOffset();\n }, 150);\n });\n }\n };\n\n /**\n * Leave slide\n *\n * This function is called after moving the index to a new slide.\n *\n * @param {Number} index - The index of the slide to leave.\n */\n const leaveSlide = index => {\n if (GROUPS[activeGroup].sliderElements[index] !== undefined) {\n GROUPS[activeGroup].sliderElements[index].setAttribute('aria-hidden', 'true');\n }\n };\n\n /**\n * Update offset\n *\n */\n const updateOffset = () => {\n activeGroup = activeGroup !== null ? activeGroup : newGroup;\n offset = -currentIndex * lightbox.offsetWidth;\n GROUPS[activeGroup].slider.style.transform = `translate3d(${offset}px, 0, 0)`;\n offsetTmp = offset;\n };\n\n /**\n * Update slider navigation status\n *\n * This function updates the disabled status of the slider navigation buttons\n * based on the current slide position.\n *\n */\n const updateSliderNavigationStatus = () => {\n const {\n triggerElements\n } = GROUPS[activeGroup];\n const TOTAL_TRIGGER_ELEMENTS = triggerElements.length;\n const FIRST_SLIDE = currentIndex === 0;\n const LAST_SLIDE = currentIndex === TOTAL_TRIGGER_ELEMENTS - 1;\n if (TOTAL_TRIGGER_ELEMENTS > 1) {\n if (FIRST_SLIDE) {\n previousButton.setAttribute('aria-disabled', 'true');\n nextButton.removeAttribute('aria-disabled');\n } else if (LAST_SLIDE) {\n previousButton.removeAttribute('aria-disabled');\n nextButton.setAttribute('aria-disabled', 'true');\n } else {\n previousButton.removeAttribute('aria-disabled');\n nextButton.removeAttribute('aria-disabled');\n }\n }\n };\n\n /**\n * Update counter\n *\n * This function updates the counter display based on the current slide index.\n */\n const updateCounter = () => {\n counter.textContent = `${currentIndex + 1}/${GROUPS[activeGroup].triggerElements.length}`;\n };\n\n /**\n * Clear drag after touchend event\n *\n * This function clears the drag state after the touchend event is triggered.\n */\n const clearDrag = () => {\n drag = {\n startX: 0,\n endX: 0,\n startY: 0,\n endY: 0\n };\n };\n\n /**\n * Recalculate drag/swipe event\n *\n */\n const updateAfterDrag = () => {\n const {\n startX,\n startY,\n endX,\n endY\n } = drag;\n const MOVEMENT_X = endX - startX;\n const MOVEMENT_Y = endY - startY;\n const MOVEMENT_X_DISTANCE = Math.abs(MOVEMENT_X);\n const MOVEMENT_Y_DISTANCE = Math.abs(MOVEMENT_Y);\n const {\n triggerElements\n } = GROUPS[activeGroup];\n const TOTAL_TRIGGER_ELEMENTS = triggerElements.length;\n if (isDraggingX) {\n if (MOVEMENT_X > 2 && MOVEMENT_X_DISTANCE >= config.threshold && currentIndex > 0) {\n previous();\n } else if (MOVEMENT_X < 2 && MOVEMENT_X_DISTANCE >= config.threshold && currentIndex !== TOTAL_TRIGGER_ELEMENTS - 1) {\n next();\n } else {\n updateOffset();\n }\n } else if (isDraggingY) {\n if (MOVEMENT_Y_DISTANCE > 2 && config.swipeClose && MOVEMENT_Y_DISTANCE >= config.threshold) {\n close();\n } else {\n lightbox.classList.remove('parvus--is-vertical-closing');\n updateOffset();\n }\n lightboxOverlay.style.opacity = '';\n } else {\n updateOffset();\n }\n };\n\n /**\n * Update Attributes\n *\n */\n const updateAttributes = () => {\n const TRIGGER_ELEMENTS = GROUPS[activeGroup].triggerElements;\n const TOTAL_TRIGGER_ELEMENTS = TRIGGER_ELEMENTS.length;\n const SLIDER = GROUPS[activeGroup].slider;\n const SLIDER_ELEMENTS = GROUPS[activeGroup].sliderElements;\n const IS_TOUCH = config.simulateTouch || isTouchDevice();\n const IS_DRAGGABLE = SLIDER.classList.contains('parvus__slider--is-draggable');\n\n // Add draggable class if neccesary\n if (IS_TOUCH && config.swipeClose && !IS_DRAGGABLE || IS_TOUCH && TOTAL_TRIGGER_ELEMENTS > 1 && !IS_DRAGGABLE) {\n SLIDER.classList.add('parvus__slider--is-draggable');\n } else {\n SLIDER.classList.remove('parvus__slider--is-draggable');\n }\n\n // Add extra output for screen reader if there is more than one slide\n if (TOTAL_TRIGGER_ELEMENTS > 1) {\n SLIDER.setAttribute('role', 'region');\n SLIDER.setAttribute('aria-roledescription', 'carousel');\n SLIDER.setAttribute('aria-label', config.l10n.sliderLabel);\n SLIDER_ELEMENTS.forEach((sliderElement, index) => {\n sliderElement.setAttribute('role', 'group');\n sliderElement.setAttribute('aria-label', `${config.l10n.slideLabel} ${index + 1}/${TOTAL_TRIGGER_ELEMENTS}`);\n });\n } else {\n SLIDER.removeAttribute('role');\n SLIDER.removeAttribute('aria-roledescription');\n SLIDER.removeAttribute('aria-label');\n SLIDER_ELEMENTS.forEach(sliderElement => {\n sliderElement.removeAttribute('role');\n sliderElement.removeAttribute('aria-label');\n });\n }\n\n // Show or hide buttons\n if (TOTAL_TRIGGER_ELEMENTS === 1) {\n counter.setAttribute('aria-hidden', 'true');\n previousButton.setAttribute('aria-hidden', 'true');\n nextButton.setAttribute('aria-hidden', 'true');\n } else {\n counter.removeAttribute('aria-hidden');\n previousButton.removeAttribute('aria-hidden');\n nextButton.removeAttribute('aria-hidden');\n }\n };\n\n /**\n * Resize event handler\n *\n */\n const resizeHandler = () => {\n if (!resizeTicking) {\n resizeTicking = true;\n BROWSER_WINDOW.requestAnimationFrame(() => {\n GROUPS[activeGroup].sliderElements.forEach((slide, index) => {\n setImageDimension(slide, GROUPS[activeGroup].contentElements[index]);\n });\n updateOffset();\n resizeTicking = false;\n });\n }\n };\n\n /**\n * Set image dimension\n *\n * @param {HTMLElement} slideEl - The slide element\n * @param {HTMLElement} contentEl - The content element\n */\n const setImageDimension = (slideEl, contentEl) => {\n if (contentEl.tagName !== 'IMG') {\n return;\n }\n const COMPUTED_STYLE = getComputedStyle(slideEl);\n const CAPTION_EL = slideEl.querySelector('.parvus__caption');\n const CAPTION_REC = CAPTION_EL ? CAPTION_EL.getBoundingClientRect().height : 0;\n const SRC_HEIGHT = contentEl.getAttribute('height');\n const SRC_WIDTH = contentEl.getAttribute('width');\n let maxHeight = slideEl.offsetHeight;\n let maxWidth = slideEl.offsetWidth;\n maxHeight -= parseFloat(COMPUTED_STYLE.paddingTop) + parseFloat(COMPUTED_STYLE.paddingBottom) + parseFloat(CAPTION_REC);\n maxWidth -= parseFloat(COMPUTED_STYLE.paddingLeft) + parseFloat(COMPUTED_STYLE.paddingRight);\n const RATIO = Math.min(maxWidth / SRC_WIDTH || 0, maxHeight / SRC_HEIGHT);\n const NEW_WIDTH = SRC_WIDTH * RATIO || 0;\n const NEW_HEIGHT = SRC_HEIGHT * RATIO || 0;\n if (SRC_HEIGHT > NEW_HEIGHT && SRC_HEIGHT < maxHeight && SRC_WIDTH > NEW_WIDTH && SRC_WIDTH < maxWidth || SRC_HEIGHT < NEW_HEIGHT && SRC_HEIGHT < maxHeight && SRC_WIDTH < NEW_WIDTH && SRC_WIDTH < maxWidth) {\n contentEl.style.width = '';\n contentEl.style.height = '';\n } else {\n contentEl.style.width = `${NEW_WIDTH}px`;\n contentEl.style.height = `${NEW_HEIGHT}px`;\n }\n };\n\n /**\n * Click event handler to trigger Parvus\n *\n * @param {Event} event - The click event object\n */\n const triggerParvus = function triggerParvus(event) {\n event.preventDefault();\n open(this);\n };\n\n /**\n * Event handler for click events\n *\n * @param {Event} event - The click event object\n */\n const clickHandler = event => {\n const {\n target\n } = event;\n if (target === previousButton) {\n previous();\n } else if (target === nextButton) {\n next();\n } else if (target === closeButton || config.docClose && !isDraggingY && !isDraggingX && target.classList.contains('parvus__slide')) {\n close();\n }\n event.stopPropagation();\n };\n\n /**\n * Get the focusable children of the given element\n *\n * @return {Array