[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: midzer\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For more information see: https://docs.github.com/en/actions/guides/publishing-nodejs-packages#publishing-packages-to-npm-and-github-packages\n\nname: Node.js Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v4\n\n      # Setup .npmrc file to publish to npm\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 18\n          registry-url: 'https://registry.npmjs.org'\n\n      - run: npm ci\n\n      # Publish to npm\n      - run: npm publish --access public\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      # Setup .npmrc file to publish to GitHub Packages\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 18\n          registry-url: 'https://npm.pkg.github.com'\n          scope: '@midzer'\n\n      # Publish to GitHub Packages\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\n.idea\nnode_modules\ntest\n"
  },
  {
    "path": ".nvmrc",
    "content": "lts/hydrogen\n"
  },
  {
    "path": ".stylelintrc",
    "content": "{\n  \"rules\": {\n    \"indentation\": 2,\n    \"string-quotes\": \"single\",\n    \"no-duplicate-selectors\": true,\n    \"color-hex-case\": \"lower\",\n    \"color-hex-length\": \"long\",\n    \"color-named\": \"never\",\n    \"selector-no-qualifying-type\": true,\n    \"selector-max-id\": 0,\n    \"selector-combinator-space-after\": \"always\",\n    \"selector-attribute-quotes\": \"always\",\n    \"selector-attribute-operator-space-before\": \"never\",\n    \"selector-attribute-operator-space-after\": \"never\",\n    \"selector-attribute-brackets-space-inside\": \"never\",\n    \"declaration-block-trailing-semicolon\": \"always\",\n    \"declaration-no-important\": true,\n    \"declaration-colon-space-before\": \"never\",\n    \"declaration-colon-space-after\": \"always\",\n    \"number-leading-zero\": \"always\",\n    \"function-url-quotes\": \"always\",\n    \"font-weight-notation\": \"ignore\",\n    \"font-family-name-quotes\": \"always-where-recommended\",\n    \"comment-whitespace-inside\": \"always\",\n    \"comment-empty-line-before\": \"always\",\n    \"at-rule-no-vendor-prefix\": true,\n    \"rule-empty-line-before\": \"always\",\n    \"selector-pseudo-element-colon-notation\": \"double\",\n    \"selector-pseudo-class-parentheses-space-inside\": \"never\",\n    \"media-feature-range-operator-space-before\": \"always\",\n    \"media-feature-range-operator-space-after\": \"always\",\n    \"media-feature-parentheses-space-inside\": \"never\",\n    \"media-feature-colon-space-before\": \"never\",\n    \"media-feature-colon-space-after\": \"always\"\n  }\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v3.2.0\n\n### New\n\n- introduce ARIA live region instead of focus approach\n  + configurable `announcementLabel`\n  + optional data attribute `data-label` as text to be used for screen readers\n- replace has() CSS selector for wider browser support\n\n## v3.1.3\n\n### Fixed\n\n- replace legacy allowfullscreen with allow attribute on IFrames\n\n## v3.1.2\n\n### Fixed\n\n- do not show caption initially on mobile devices\n\n## v3.1.1\n\n### Fixed\n\n- remove() for single slide removal\n- multiple Tobii instances race conditions\n\n## v3.1.0\n\n### New\n\n- empty or false `selector` option does only init Tobii (to `add()` elements later) \n\n### Fixed\n\n- do not clone html-type target\n\n## v3.0.0\n\n### Breaking Changes\n\n- remove legacy prefixes\n- zoom icon default to false\n- remove autoplay settings, Media elements like YouTube `<video>` and `<audio>`, just autoplay\n- drop IE11 support\n- do not compress non-IIFE builds\n\n### Documentation\n\n- add banner to minified IIFE build\n\n### Chore\n\n- cleanup unused settings\n\n## v2.8.5\n\n### Fixed\n\n- zoom only for zoomable elements\n\n## v2.8.4\n\n### Fixed\n\n- increase focus delay for figure due transition bug on low-end Android devices\n\n## v2.8.3\n\n### Fixed\n\n- fix double click zoom and delayed tap on Android by introducing a threshold\n\n## v2.8.2\n\n### Fixed\n\n- fix aria-label of figure\n- tweak caption-toggle CSS\n\n## v2.8.1\n\n### Fixed\n\n- Allow multiple lightbox instances\n\n## v2.8.0\n\n### New\n\n- Toggle caption display on click/touch\n\n## v2.7.3\n\n### Fixed\n\n- Fix unclickable top region for docClose\n\n## v2.7.2\n\n### Fixed\n\n- Encapsulate counter text in P element for better accessibility compliance\n- Add new dialogTitle setting to allow dialog title customization for better accessibility compliance\n- Revert \"fix not clickable close() region for slider\"\n\n## v2.7.1\n\n### Fixed\n\n- GitHub Action workflow\n\n## v2.7.0\n\n### New\n\n- Accessibility improvements\n- Sizes attribute to properly handle responsive images\n- Programmatically set focus on figure elements on slide change\n\n### Fixed\n\n- srcset before src to avoid loading images twice\n- scroll to top when opening lightbox\n\n## v2.6.6\n\n### Fixed\n\n- Previous release regression\n\n## v2.6.5\n\n### Fixed\n\n- Reset zoom in any case on cleanup from 2.6.0\n\n## v2.6.4\n\n### Fixed\n\n- Next button after 2nd slide regression from 2.6.0\n\n## v2.6.2\n\n### Fixed\n\n- GitHub Action workflow\n\n## v2.6.1\n\n### Fixed\n\n- Single tap cycle on mobile not working in some cases\n- Some elements on demo page were broken\n- Allow Node 18 again\n\n## v2.6.0\n\n### New\n\n- Pinch zoom feature for touch devices\n- Double click and wheel zoom with mouse/touch clamped pan\n- Delayed tap on mobile for prev/next navigation\n\n### Fixed\n\n- Not clickable close() region above slider\n\n## v2.5.0\n\n### New\n\n- Apply opacity to buttons on :hover\n- Change opacity on close button\n- Support for audio element\n- Bigger inline content in demo and\n- Introduce captionHTML parameter\n- Replace em function\n\n### Fixed\n\n- Big local video elements\n- Missing close button in rare cases\n- Update lightbox on remove element\n- Adding/removing elements dynamically\n- YouTube link in demo\n\n## v2.4.0\n\n### Changes\n\n- tobii.mjs -> tobii.modern.js\n\n### Fixed\n\n- All CSS custom properties are now prefixed with `--tobii-` to avoid conflicts (e.g. `--tobii-base-font-size` instead of `--base-font-size: 18px`).\n\n### Deprecated\n\n- Unprefixed forms of CSS custom properties are deprecated and will no longer be supported in the next major release. Update now by adding the `--tobii-` prefix to your variables:\n    - Before: `--base-font-size: 18px;`\n    - After: `--tobii-base-font-size: 18px;`\n"
  },
  {
    "path": "LICENSE.md",
    "content": "# The MIT License (MIT)\n\nCopyright (c) 2017-2020 rqrauhvmra, 2021 midzer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Tobii\n\n[![Version](https://badgen.net/npm/v/@midzer/tobii)](https://github.com/midzer/tobii/releases)\n[![License](https://badgen.net/npm/license/@midzer/tobii)](https://github.com/midzer/tobii/blob/master/LICENSE.md)\n![Dependencies](https://badgen.net/npm/dependents/@midzer/tobii)\n![npm bundle size](https://badgen.net/bundlephobia/minzip/@midzer/tobii)\n\nAn accessible, open-source lightbox with no dependencies.\n\n[See it in Action](https://midzer.github.io/tobii/demo/)\n\n![Open slide with a picture of the Berlin television tower](https://rqrauhvmra.com/tobi/tobi.png)\n\n## Table of contents\n\n- [Features](#features)\n- [Get Tobii](#get-tobii)\n  - [Download](#download)\n  - [Package managers](#package-managers)\n- [Usage](#usage)\n- [Media types](#media-types)\n  - [Image](#image)\n  - [Inline HTML](#inline-html)\n  - [Iframe](#iframe)\n  - [YouTube](#youtube)\n- [Grouping](#grouping)\n- [Options](#options)\n- [API](#api)\n- [Events](#events)\n- [Browser support](#browser-support)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Features\n\n- No dependencies\n- Supports multiple content types:\n  - Images\n  - Inline HTML\n  - Iframes\n  - Videos (YouTube, Vimeo)\n- Grouping\n- Events\n- Customizable with settings and CSS\n- Accessible:\n  - ARIA roles\n  - Keyboard navigation:\n    - `Prev`/ `Next` Keys: Navigate through slides\n    - `ESCAPE` Key: Close Tobii\n    - `TAB` Key: Focus elements within Tobii, not outside\n  - User preference media features:\n    - `prefers-reduced-motion` media query\n  - When Tobii opens, focus is set to the first focusable element in Tobii\n  - When Tobii closes, focus returns to what was in focus before Tobii opened\n- Touch & mouse drag/swipe support:\n  - Horizontal to navigate through slides\n  - Vertical up to close Tobii\n- Double click, pinch and wheel zoom:\n  - Hold pointer to pan\n- Responsive\n\n## Get Tobii\n\n### Download\n\nCSS: `dist/tobii.min.css`\n\nJavaScript:\n\n* `dist/tobii.min.js`: minified IIFE build\n* `dist/tobii.modern.js`: Build specially designed to work in all modern browsers\n* `dist/tobii.module.js`: ESM build\n* `dist/tobii.umd.js`: UMD build\n* `dist/tobii.js`: CommonJS/Node build\n\n### Package managers\n\nTobii is also available on npm.\n\n`npm install @midzer/tobii --save`\n\n## Usage\n\nYou can install Tobii by linking the `.css` and `.js` files to your HTML file. The HTML code may look like this:\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Your page title</title>\n\n  <!-- CSS -->\n  <link href=\"tobii.min.css\" rel=\"stylesheet\">\n</head>\n<body>\n  <!-- Your HTML content -->\n\n  <!-- JS -->\n  <script src=\"tobii.min.js\"></script>\n</body>\n</html>\n```\n\nInitialize the script by running:\n\n```js\nconst tobii = new Tobii()\n```\n\n## Media types\n\n### Image\n\nThe standard way of using Tobii is a linked thumbnail image with the class name `lightbox` to a larger image:\n\n```html\n<a href=\"path/to/image.jpg\" class=\"lightbox\">\n  <img src=\"path/to/thumbnail.jpg\" alt=\"I am a caption\">\n</a>\n```\n\nInstead of a thumbnail, you can also refer to a larger image with a text link:\n\n```html\n<a href=\"path/to/image.jpg\" class=\"lightbox\">\n  Open image\n</a>\n```\n\nIf you use a Markdown parser or CMS and want to make all images in a post\nautomatically viewable in a lightbox, use the following JavaScript code to add\nall images to the same album:\n\n```javascript\ndocument.addEventListener('DOMContentLoaded', () => {\n  // This assumes your article is wrapped in an element with the class \"content-article\".\n  document.querySelectorAll('.content-article img').forEach((articleImg) => {\n    // Add lightbox elements in blog articles for Tobii.\n    const lightbox = document.createElement('a');\n    lightbox.href = articleImg.src;\n    lightbox.classList.add('lightbox');\n    lightbox.dataset.group = 'article';\n    articleImg.parentNode.appendChild(lightbox);\n    lightbox.appendChild(articleImg);\n  });\n});\n```\n\n### Inline-HTML\n\nFor inline HTML, create an element with a unique ID:\n\n```html\n<div id=\"selector\">\n  <!-- Your HTML content -->\n</div>\n```\n\nThen create a link with the class name `lightbox` and a `href` attribute that matches the ID of the element:\n\n```html\n<a href=\"#selector\" data-type=\"html\" class=\"lightbox\">\n  Open HTML content\n</a>\n```\n\nor a button with the class name `lightbox` and a `data-target` attribute that matches the ID of the element:\n\n```html\n<button type=\"button\" data-type=\"html\" data-target=\"#selector\" class=\"lightbox\">\n  Open HTML content\n</button>\n```\n\nIn both ways, the attribute `data-type` with the value `html` is required.\n\n### Iframe\n\nFor an iframe, create a link with the class name `lightbox`:\n\n```html\n<a href=\"https://www.wikipedia.org\" data-type=\"iframe\" class=\"lightbox\">\n  Open Wikipedia\n</a>\n```\n\nor a button with the class name `lightbox` and a `data-target` attribute:\n\n```html\n<button type=\"button\" data-type=\"iframe\" data-target=\"https://www.wikipedia.org\" class=\"lightbox\">\n  Open Wikipedia\n</button>\n```\n\nIn both ways, the attribute `data-type` with the value `iframe` is required.\n\n#### Optional attributes\n\n- `data-height` set the height and `data-width` the width of the iframe.\n\n### YouTube\n\nFor a YouTube video, create a link with the class name `lightbox` and a `data-id` attribute with the YouTube video ID:\n\n```html\n<a href=\"#\" data-type=\"youtube\" data-id=\"KU2sSZ_90PY\" class=\"lightbox\">\n  Open YouTube video\n</a>\n```\n\nor a button with the class name `lightbox` and a `data-id` attribute with the YouTube video ID:\n\n```html\n<button type=\"button\" data-type=\"youtube\" data-id=\"KU2sSZ_90PY\" class=\"lightbox\">\n  Open YouTube video\n</button>\n```\n\nIn both ways, the attribute `data-type` with the value `youtube` is required.\n\n#### Optional attributes\n\n- `data-controls` indicates whether the video player controls are displayed: `0` do not display and `1` display controls in the player.\n- `data-height` set the height and `data-width` the width of the player. I recommend using an external library for responsive iframes.\n\n## Grouping\n\nIf you have a group of related types that you would like to combine into a set, add the `data-group` attribute:\n\n```html\n<a href=\"path/to/image_1.jpg\" class=\"lightbox\" data-group=\"vacation\">\n  <img src=\"path/to/thumbnail_1.jpg\" alt=\"I am a caption\">\n</a>\n\n<a href=\"path/to/image_2.jpg\" class=\"lightbox\" data-group=\"vacation\">\n  <img src=\"path/to/thumbnail_2.jpg\" alt=\"I am a caption\">\n</a>\n\n// ...\n\n<a href=\"path/to/image_4.jpg\" class=\"lightbox\" data-group=\"birthday\">\n  <img src=\"path/to/thumbnail_4.jpg\" alt=\"I am a caption\">\n</a>\n\n// ...\n```\n\n## Options\n\nYou can pass an object with custom options as an argument.\n\n```js\nconst tobii = new Tobii({\n  captions: false\n})\n```\n\nThe following options are available:\n\n| Property | Type | Default | Description |\n| --- | --- | --- | --- |\n| selector | string | \".lightbox\" | All elements with this class trigger Tobii. Pass `\"\"` or `false` to init Tobii only (and `add()` later) |\n| captions | bool | true | Display captions, if available. |\n| captionsSelector | \"self\", \"img\" | \"img\" | Set the element where the caption is. Set it to \"self\" for the `a` tag itself. |\n| captionAttribute | string | \"alt\" | Get the caption from given attribute. |\n| captionText | function | null | Custom callback which returns the caption text for the current element. The first argument of the callback is the element. If set, `captionsSelector` and `captionAttribute` are ignored. |\n| captionHTML | bool | false | Allow HTML captions. |\n| captionToggle | bool | true | Allows users to hide or show the caption by clicking or tapping on it. |\n| captionToggleLabel | string | [\"Hide caption\", \"Show caption\"] | Labels for the caption display toggle button. |\n| nav | bool, \"auto\" | \"auto\" | Display navigation buttons. \"auto\" hides buttons on touch-enabled devices. |\n| navText | string | [\"inline svg\", \"inline svg\"] | Text or HTML for the navigation buttons. |\n| navLabel | string | [\"Previous\", \"Next\"] | ARIA label for screen readers. |\n| announcementLabel | string | [\"Slide\", \"of\"] | ARIA label for screen readers. |\n| close | bool | true | Display close button. |\n| closeText | string | \"inline svg\" | Text or HTML for the close button. |\n| closeLabel | string | \"Close\" | ARIA label for screen readers. |\n| dialogTitle | string | \"Lightbox\" | ARIA label for screen readers. |\n| loadingIndicatorLabel | string | \"Image loading\" | ARIA label for screen readers. |\n| counter | bool | true | Display current image index. |\n| keyboard | bool | true | Allow keyboard navigation. |\n| zoom | bool | false | Display zoom icon. |\n| zoomText | string | \"inline svg\" | Text or HTML for the zoom icon. |\n| docClose | bool | true | Click outside to close Tobii. |\n| swipeClose | bool | true | Swipe up to close Tobii. |\n| draggable | bool | true | Use dragging and touch swiping. |\n| threshold | number | 100 | Touch and mouse dragging threshold (in px). |\n\n### Data attributes\n\nYou can also use data attributes to customize HTML elements.\n\n```js\n<a href=\"path/to/image.jpg\" class=\"lightbox\" data-group=\"custom-group\">\n  Open image.\n</a>\n```\n\nThe following options are available:\n\n| Property | Description |\n| --- | --- |\n| data-type | Sets media type. Possible values: `html`,`iframe`,`youtube`. |\n| data-id | Required for YouTube media type. |\n| data-target | Can be used to set target for \"iframe\" and \"html\" types. |\n| data-group | Set custom group |\n| data-width | Set container width for iframe or YouTube types. |\n| data-height | Set container height for iframe or YouTube types. |\n| data-controls | Indicates whether the video player controls are displayed: 0 do not display and 1 display controls in the player. |\n| data-allow | Allows to set allow attribute on iframes. |\n| data-srcset | Allows to have Responsive image or retina images |\n| data-zoom | Allows to enable or disable zoom icon. Values: \"true\" or \"false\" |\n| data-label | Text to be used as an extra announcement for screen readers when this slide is shown. If present, Tobii’s aria‑live region will say “Slide X of Y. [data‑label]”. If data-label is missing, it will fall back to the alt attribute of the img, if available. |\n\n## API\n\n| Function | Description |\n| --- | --- |\n| `open(index)` | Open Tobii. Optional `index` (Integer), zero-based index of the slide to open. |\n| `select(index)` | Select a slide with `index` (Integer), zero-based index of the slide to select. |\n| `previous()` | Select the previous slide. |\n| `next()` | Select the next slide. |\n| `selectGroup(value)` | Select a group with `value` (string), name of the group to select. |\n| `close()` | Close Tobii. |\n| `add(element)` | Add `element` (DOM element). |\n| `remove(element)` | Remove `element` (DOM element). |\n| `isOpen()` | Check if Tobii is open. |\n| `slidesIndex()` | Return the current slide index. |\n| `slidesCount()` | Return the current number of slides. |\n| `currentGroup()` | Return the current group name. |\n| `reset()` | Reset Tobii. |\n| `destroy()` | Destroy Tobii. |\n\n## Events\n\nBind events with the `.on()` and `.off()` methods.\n\n```js\nconst tobii = new Tobii()\n\nconst listener = function listener () {\n  console.log('eventName happened')\n}\n\n// bind event listener\ntobii.on(eventName, listener)\n\n// unbind event listener\ntobii.off(eventName, listener)\n```\n\n| eventName | Description |\n| --- | --- |\n| `open` | Triggered after Tobii has been opened. |\n| `close` | Triggered after Tobii has been closed. |\n| `previous` | Triggered after the previous slide is selected. |\n| `next` | Triggered after the next slide is selected. |\n\n## Browser support\n\nTobii supports the following browser (all the latest versions):\n\n- Chrome\n- Firefox\n- Edge\n- Safari\n\n## Build instructions\nSee [Wiki > Build instructions](https://github.com/midzer/tobii/wiki/Build-instructions)\n\n## Contributing\n\n- Open an issue or a pull request to suggest changes or additions\n- Spread the word\n\n## License\n\nTobii is available under the MIT license. See the [LICENSE](https://github.com/midzer/Tobii/blob/master/LICENSE.md) file for more info.\n"
  },
  {
    "path": "add-banner.js",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport pkg from './package.json' with { type: 'json' };\n\nconst banner = `/*!\n * ${pkg.name} ${pkg.version}\n * Licensed under the ${pkg.license} license.\n * ${pkg.homepage}\n */\n`;\n\nconst filePath = path.join(process.env.PWD, 'dist/tobii.min.js');\nconst content = fs.readFileSync(filePath, 'utf8');\nconst output = banner + '\\n' + content;\nfs.writeFileSync(filePath, output);\n\nconsole.log('Banner prepended to ' + filePath);\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Tobii LightBox Demo</title>\n  <meta name=\"description\" content=\"Demo page for tobii - an accessible, open-source lightbox with no dependencies\">\n  <link rel=\"stylesheet\" href=\"../dist/tobii.min.css\">\n  <link rel=\"stylesheet\" href=\"./styles.css\">\n</head>\n\n<body>\n\n<h1>Tobii LightBox Demo</h1>\n\n<table>\n  <tr>\n    <th style=\"width: 200px;\">\n      Image (single)\n    </th>\n    <td>\n      <a href=\"https://img.youtube.com/vi/HHi8qOtHnhE/maxresdefault.jpg\" class=\"lightbox\" data-group=\"single\">\n        <img alt=\"Aquarium\" title=\"Aquarium view\" src=\"https://img.youtube.com/vi/HHi8qOtHnhE/mqdefault.jpg\" />\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Images (grouped)\n    </th>\n    <td>\n      <a href=\"https://img.youtube.com/vi/spxtEt6RaS4/maxresdefault.jpg\" class=\"lightbox\" data-group=\"gallery\">\n        <img alt=\"\" src=\"https://img.youtube.com/vi/spxtEt6RaS4/mqdefault.jpg\"/>\n      </a>\n      <a href=\"https://img.youtube.com/vi/DGIXT7ce3vQ/maxresdefault.jpg\" class=\"lightbox\" data-group=\"gallery\">\n        <img alt=\"\" src=\"https://img.youtube.com/vi/DGIXT7ce3vQ/mqdefault.jpg\"/>\n      </a>\n      <a href=\"https://img.youtube.com/vi/Q8eh58Z249o/maxresdefault.jpg?param=test\" class=\"lightbox\" data-group=\"gallery\">\n        <img alt=\"\" src=\"https://img.youtube.com/vi/Q8eh58Z249o/mqdefault.jpg?param=test\"/>\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th style=\"width: 200px;\">\n      Image (retina)\n    </th>\n    <td>\n      <a href=\"https://i.postimg.cc/7D1zM4PH/Wind7-Ult64-img0-Scale2304x1440.png\" class=\"lightbox\"\n         data-srcset=\"https://s20.postimg.cc/svc6skku5/Wind7_Ult64_img0_SCALE3456x2160.png 2x\"\n         data-group=\"retina\">\n        Open\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Inline HTML\n    </th>\n    <td>\n      <a href=\"?fallback_url\" class=\"lightbox\" data-type=\"html\" data-target=\"#selector\" data-group=\"inline\">\n        Text\n      </a>\n\n      <div style=\"display: none;\">\n        <div id=\"selector\" data-group=\"iframe\">\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain. Debating\n          offended at branched <a href=\"#\">striking be subjects</a>.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n          Instrument cultivated alteration any favourable expression law far nor. Both new like tore but year.\n          An from mean on with when sing pain. Oh to as principles devonshire companions unsatiable an\n          delightful. The ourselves suffering the sincerity. Inhabit her manners adapted age certain.\n        </div>\n      </div>\n\n      <br />\n\n      <a href=\"?fallback_url\" class=\"lightbox\" data-type=\"html\" data-target=\"#selector_audio\" data-group=\"inline_audio\">\n        Audio\n      </a>\n\n      <div style=\"display: none;\">\n        <div id=\"selector_audio\" data-group=\"iframe\">\n          <audio controls=\"\" preload=\"none\">\n            <source src=\"https://upload.wikimedia.org/wikipedia/commons/transcoded/b/bb/Test_ogg_mp3_48kbps.wav/Test_ogg_mp3_48kbps.wav.mp3\"\n                    type=\"audio/mpeg\" />\n          </audio>\n        </div>\n      </div>\n\n      <br />\n\n      <a href=\"?fallback_url\" class=\"lightbox\" data-type=\"html\" data-target=\"#selector_video\" data-group=\"inline_video\">\n        Video\n      </a>\n\n      <style>\n        .tobii__slide .tobii-group-inline_video{\n          /* set container to fit all space. Class above contains data group */\n          max-width:none;\n          padding: 0;\n        }\n      </style>\n      <div style=\"display: none;\">\n        <div style=\"background-color:black;\" id=\"selector_video\" data-group=\"iframe\">\n\n          <video width=\"1280\" height=\"704\" controls>\n            <source src=\"https://www.w3schools.com/html/mov_bbb.mp4\" type=\"video/mp4\">\n          </video>\n        </div>\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Iframe\n    </th>\n    <td>\n      <a href=\"https://www.wikipedia.org\" class=\"lightbox\"\n\t\t data-type=\"iframe\"\n\t\t data-group=\"iframe\"\n\t\t data-width=\"1024px\"\n\t\t data-height=\"576px\">\n        Open\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      YouTube: (mode: iframe)\n    </th>\n    <td>\n      <a href=\"https://www.youtube.com/embed/RK1K2bCg4J8?autoplay=1\" class=\"lightbox\"\n         data-type=\"iframe\"\n         data-group=\"iframe-youtube\"\n         data-width=\"1120px\"\n         data-height=\"630px\">\n        <img alt=\"\" src=\"https://img.youtube.com/vi/RK1K2bCg4J8/mqdefault.jpg\" />\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      YouTube: (mode: API)\n    </th>\n    <td>\n      <a href=\"https://www.youtube.com/watch?v=RK1K2bCg4J8\" class=\"lightbox\"\n         data-type=\"youtube\" data-id=\"RK1K2bCg4J8\" data-group=\"youtube\">\n        Open\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Vimeo:\n    </th>\n    <td>\n      <a href=\"https://player.vimeo.com/video/15209448?autoplay=1\" class=\"lightbox\"\n         data-type=\"iframe\"\n         data-group=\"iframe-vimeo\"\n         data-width=\"1280px\"\n         data-height=\"720px\">\n        <img alt=\"\" src=\"https://i.vimeocdn.com/video/91219950_1280x720\" />\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th style=\"width: 200px;\">\n      Events\n    </th>\n    <td>\n      <a href=\"https://img.youtube.com/vi/HHi8qOtHnhE/maxresdefault.jpg\" class=\"lightbox\" data-group=\"events\">\n        Open\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Manual call\n    </th>\n    <td>\n      <a href=\"#\" id=\"manual\">\n        Open\n      </a>\n    </td>\n  </tr>\n  <tr>\n    <th>\n      Image error\n    </th>\n    <td>\n      <a href=\"https://example.org/404.jpg\" class=\"lightbox\" data-group=\"single-error\">\n        Open\n      </a>\n    </td>\n  </tr>\n</table>\n\n<!-- JS -->\n<!-- use this in production: <script src=\"tobii.min.js\"></script> -->\n\n<script type=\"module\">\n  //using modern mode as module (optional)\n  import Tobii from '../dist/tobii.module.js'\n\n  //prepare manual executions\n  let manual = document.getElementById('manual')\n  manual.classList.add('lightbox')\n  manual.href = 'https://via.placeholder.com/600.jpg'\n  manual.attributes.group = 'manual'\n\n  //init\n  const tobii = new Tobii()\n\n  //set events\n  tobii.on('open', function (e) {\n    if(e.detail.group === 'events')\n      console.log('event: ' + 'open', e.detail)\n  })\n  tobii.on('previous', function (e) {\n    if(e.detail.group === 'events')\n      console.log('event: ' + 'previous', e.detail)\n  })\n  tobii.on('next', function (e) {\n    if(e.detail.group === 'events')\n      console.log('event: ' + 'next', e.detail)\n  })\n  tobii.on('close', function (e) {\n    if(e.detail.group === 'events')\n      console.log('event: ' + 'close', e.detail)\n  })\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "demo/styles.css",
    "content": "html {\n  background-color: #f5f5f7;\n}\n\nh1 {\n  margin-top: 0;\n  text-decoration: underline;\n  text-align: center;\n}\n\nbody {\n  max-width: 1024px;\n  margin: 32px auto;\n  padding: 1em 1em;\n  border-radius: 8px;\n  background-color: #bfbfbf;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif;\n  font-size: 16px;\n  line-height: 1.5;\n  box-shadow: #555 0 1px 3px 0, #555 0 3px 8px 3px;\n}\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\nth {\n  text-align: left;\n}\n\nth, td {\n  padding: 5px 1em;\n}\n\nimg {\n  max-width: 200px;\n  height: auto;\n  border: 1px solid #555;\n  box-shadow: 1px 2px 2px 0 #555;\n}\n"
  },
  {
    "path": "dist/tobii.js",
    "content": "class ImageType {\n  constructor() {\n    this.figcaptionId = 0;\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const FIGURE = document.createElement('figure');\n    const IMAGE = document.createElement('img');\n    const THUMBNAIL = el.querySelector('img');\n    const LOADING_INDICATOR = document.createElement('div');\n\n    // Accessibility: allow setting focus programmatically on figure elements.\n    FIGURE.tabIndex = -1;\n\n    // Add role=\"group\" to figure\n    FIGURE.setAttribute('role', 'group');\n\n    // Hide figure until the image is loaded\n    FIGURE.style.opacity = '0';\n    if (THUMBNAIL) {\n      IMAGE.alt = THUMBNAIL.alt || '';\n    }\n    IMAGE.setAttribute('data-src', el.href);\n    if (el.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset'));\n    }\n    if (el.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes'));\n    }\n\n    // Add image to figure\n    FIGURE.appendChild(IMAGE);\n\n    // Create figcaption\n    let captionContent;\n    if (typeof this.userSettings.captionText === 'function') {\n      captionContent = this.userSettings.captionText(el);\n    } else if (this.userSettings.captionsSelector === 'self' && el.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = el.getAttribute(this.userSettings.captionAttribute);\n    } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL && THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute);\n    }\n    if (this.userSettings.captions && captionContent) {\n      const FIGCAPTION = document.createElement('figcaption');\n      FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`;\n      const SPAN = document.createElement('span');\n      if (this.userSettings.captionHTML) {\n        SPAN.innerHTML = captionContent;\n      } else {\n        SPAN.textContent = captionContent;\n      }\n      FIGCAPTION.appendChild(SPAN);\n      if (this.userSettings.captionToggle) {\n        const isMobile = window.innerWidth < 768;\n        const BUTTON = document.createElement('button');\n        BUTTON.className = 'caption-toggle';\n        BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0];\n        BUTTON.setAttribute('aria-controls', FIGCAPTION.id);\n        BUTTON.setAttribute('aria-expanded', !isMobile);\n        if (isMobile) {\n          FIGCAPTION.classList.add('caption-hidden');\n        }\n        SPAN.setAttribute('aria-hidden', isMobile);\n        const preventAndStopEvent = event => {\n          event.preventDefault();\n          event.stopPropagation();\n        };\n        BUTTON.addEventListener('pointerdown', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('pointerup', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('click', event => {\n          preventAndStopEvent(event);\n          const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true';\n          const buttonLabel = isExpanded ? this.userSettings.captionToggleLabel[1] : this.userSettings.captionToggleLabel[0];\n          BUTTON.textContent = BUTTON.title = buttonLabel;\n          BUTTON.setAttribute('aria-expanded', !isExpanded);\n          FIGCAPTION.classList.toggle('caption-hidden');\n          SPAN.setAttribute('aria-hidden', isExpanded);\n        });\n        FIGCAPTION.appendChild(BUTTON);\n      }\n      FIGURE.appendChild(FIGCAPTION);\n      IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id);\n\n      // Add aria-label to the figure containing the caption content\n      FIGURE.setAttribute('aria-label', SPAN.textContent);\n      ++this.figcaptionId;\n    }\n\n    // Add figure to container\n    container.appendChild(FIGURE);\n\n    // Create loading indicator\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n\n    // Add loading indicator to container\n    container.appendChild(LOADING_INDICATOR);\n\n    // Register type\n    container.setAttribute('data-type', 'image');\n    container.classList.add('tobii-image');\n  }\n  onPreload(container) {\n    // Same as preload\n    this.onLoad(container);\n  }\n  onLoad(container) {\n    const IMAGE = container.querySelector('img');\n    if (!IMAGE.hasAttribute('data-src')) {\n      return;\n    }\n    const FIGURE = container.querySelector('figure');\n    const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n    const handleImageEvent = () => {\n      container.removeChild(LOADING_INDICATOR);\n      FIGURE.style.opacity = '1';\n    };\n    IMAGE.addEventListener('load', handleImageEvent);\n    IMAGE.addEventListener('error', handleImageEvent);\n    if (IMAGE.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset'));\n      IMAGE.removeAttribute('data-srcset');\n    }\n    if (IMAGE.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes'));\n      IMAGE.removeAttribute('data-sizes');\n    }\n    IMAGE.setAttribute('src', IMAGE.getAttribute('data-src'));\n    IMAGE.removeAttribute('data-src');\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    // Nothing\n  }\n  onReset() {\n    this.figcaptionId = 0;\n  }\n}\n\nclass IframeType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    container.setAttribute('data-HREF', HREF);\n    if (el.hasAttribute('data-allow')) {\n      container.setAttribute('data-allow', el.getAttribute('data-allow'));\n    }\n    if (el.hasAttribute('data-width')) {\n      container.setAttribute('data-width', `${el.getAttribute('data-width')}`);\n    }\n    if (el.hasAttribute('data-height')) {\n      container.setAttribute('data-height', `${el.getAttribute('data-height')}`);\n    }\n\n    // dont create empty iframes here - very slow\n\n    // Register type\n    container.setAttribute('data-type', 'iframe');\n    container.classList.add('tobii-iframe');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    let IFRAME = container.querySelector('iframe');\n\n    // Create loading indicator\n    const LOADING_INDICATOR = document.createElement('div');\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n    container.appendChild(LOADING_INDICATOR);\n    if (IFRAME == null) {\n      // create iframe\n      IFRAME = document.createElement('iframe');\n      const HREF = container.getAttribute('data-href');\n      IFRAME.setAttribute('frameborder', '0');\n      IFRAME.setAttribute('src', HREF);\n\n      // Set allow parameters\n      let allowValue = 'fullscreen';\n      if (HREF.includes('youtube.com')) {\n        allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';\n      } else if (HREF.includes('vimeo.com')) {\n        allowValue += '; autoplay; picture-in-picture';\n      } else if (container.hasAttribute('data-allow')) {\n        allowValue = container.getAttribute('data-allow');\n      }\n      IFRAME.setAttribute('allow', allowValue);\n      if (container.hasAttribute('data-width')) {\n        IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`;\n      }\n      if (container.hasAttribute('data-height')) {\n        IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`;\n      }\n\n      // Hide until loaded\n      IFRAME.style.opacity = '0';\n\n      // Add iframe to container\n      container.appendChild(IFRAME);\n\n      // Handle load and error\n      const removeLoader = () => {\n        IFRAME.style.opacity = '1';\n        const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n        if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR);\n      };\n      IFRAME.addEventListener('load', removeLoader);\n      IFRAME.addEventListener('error', removeLoader);\n    } else {\n      // was already created\n      IFRAME.setAttribute('src', container.getAttribute('data-href'));\n    }\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    const IFRAME = container.querySelector('iframe');\n    IFRAME.removeAttribute('src');\n    IFRAME.style.opacity = '0';\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\nclass HtmlType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    const TARGET = document.querySelector(TARGET_SELECTOR);\n    if (!TARGET) {\n      throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`);\n    }\n\n    // Add content to container\n    container.appendChild(TARGET);\n\n    // Register type\n    container.setAttribute('data-type', 'html');\n    container.classList.add('tobii-html');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container, group) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) {\n        // Continue where video was stopped\n        VIDEO.currentTime = VIDEO.getAttribute('data-time');\n      }\n\n      // Start playback (and loading if necessary)\n      VIDEO.play();\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      // Start playback (and loading if necessary)\n      audio.play();\n    }\n    container.classList.add('tobii-group-' + group);\n  }\n  onLeave(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (!VIDEO.paused) {\n        // Stop if video is playing\n        VIDEO.pause();\n      }\n\n      // Backup currentTime (needed for revisit)\n      if (VIDEO.readyState > 0) {\n        VIDEO.setAttribute('data-time', VIDEO.currentTime);\n      }\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      if (!audio.paused) {\n        // Stop if is playing\n        audio.pause();\n      }\n    }\n  }\n  onCleanup(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) {\n        // Some data has been loaded but not the whole package.\n        // In order to save bandwidth, stop downloading as soon as possible.\n        const VIDEO_CLONE = VIDEO.cloneNode(true);\n        this._removeSources(VIDEO);\n        VIDEO.load();\n        VIDEO.parentNode.removeChild(VIDEO);\n        container.appendChild(VIDEO_CLONE);\n      }\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n\n  /**\n   * Remove all `src` attributes\n   *\n   * @param {HTMLElement} el - Element to remove all `src` attributes\n   */\n  _removeSources(el) {\n    const SOURCES = el.querySelectorAll('src');\n    if (SOURCES) {\n      SOURCES.forEach(source => {\n        source.removeAttribute('src');\n      });\n    }\n  }\n}\n\nclass YoutubeType {\n  constructor() {\n    this.playerId = 0;\n    this.PLAYER = [];\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const IFRAME_PLACEHOLDER = document.createElement('div');\n\n    // Add iframePlaceholder to container\n    container.appendChild(IFRAME_PLACEHOLDER);\n    this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {\n      host: 'https://www.youtube-nocookie.com',\n      height: el.getAttribute('data-height') || '360',\n      width: el.getAttribute('data-width') || '640',\n      videoId: el.getAttribute('data-id'),\n      playerVars: {\n        controls: el.getAttribute('data-controls') || 1,\n        rel: 0,\n        playsinline: 1\n      }\n    });\n\n    // Set player ID\n    container.setAttribute('data-player', this.playerId);\n\n    // Register type\n    container.setAttribute('data-type', 'youtube');\n    container.classList.add('tobii-youtube');\n    this.playerId++;\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    this.PLAYER[container.getAttribute('data-player')].playVideo();\n  }\n  onLeave(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onCleanup(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\n/**\n * Tobii\n *\n * @author midzer\n * @version 3.2.0\n * @url https://github.com/midzer/tobii\n *\n * MIT License\n */\nfunction Tobii(userOptions) {\n  /**\n   * Global variables\n   *\n   */\n  const SUPPORTED_ELEMENTS = {\n    image: new ImageType(),\n    // default\n    html: new HtmlType(),\n    iframe: new IframeType(),\n    youtube: new YoutubeType()\n  };\n  const FOCUSABLE_ELEMENTS = ['a[href]:not([tabindex^=\"-\"]):not([inert])', 'area[href]:not([tabindex^=\"-\"]):not([inert])', 'input:not([disabled]):not([inert])', 'select:not([disabled]):not([inert])', 'textarea:not([disabled]):not([inert])', 'button:not([disabled]):not([inert])', 'iframe:not([tabindex^=\"-\"]):not([inert])', 'audio:not([tabindex^=\"-\"]):not([inert])', 'video:not([tabindex^=\"-\"]):not([inert])', '[contenteditable]:not([tabindex^=\"-\"]):not([inert])', '[tabindex]:not([tabindex^=\"-\"]):not([inert])'];\n  let userSettings = {};\n  const WAITING_ELS = [];\n  const GROUP_ATTS = {\n    gallery: [],\n    slider: null,\n    sliderElements: [],\n    elementsLength: 0,\n    currentIndex: 0,\n    x: 0\n  };\n  let lightbox = null;\n  let prevButton = null;\n  let nextButton = null;\n  let closeButton = null;\n  let counter = null;\n  let lastFocus = null;\n  let offset = null;\n  let isYouTubeDependencyLoaded = false;\n  let groups = {};\n  let activeGroup = null;\n  let pointerDownCache = [];\n  let lastTapTime = 0;\n  let liveRegion = null;\n  const MIN_SCALE = 1;\n  const MAX_SCALE = 4;\n  const DOUBLE_TAP_TIME = 500; // milliseconds\n  const SCALE_SENSITIVITY = 10;\n  const TRANSFORM = {\n    element: null,\n    originX: 0,\n    originY: 0,\n    translateX: 0,\n    translateY: 0,\n    scale: MIN_SCALE\n  };\n  const DRAG = {\n    startX: 0,\n    startY: 0,\n    x: 0,\n    y: 0,\n    distance: 0\n  };\n\n  /**\n   * Merge default options with user options\n   *\n   * @param {Object} userOptions - Optional user options\n   * @returns {Object} - Custom options\n   */\n  const mergeOptions = userOptions => {\n    // Default options\n    const OPTIONS = {\n      selector: '.lightbox',\n      captions: true,\n      captionsSelector: 'img',\n      captionAttribute: 'alt',\n      captionText: null,\n      captionHTML: false,\n      captionToggle: true,\n      captionToggleLabel: ['Hide caption', 'Show caption'],\n      nav: 'auto',\n      navText: ['<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"15 6 9 12 15 18\" /></svg>', '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"9 6 15 12 9 18\" /></svg>'],\n      navLabel: ['Previous image', 'Next image'],\n      announcementLabel: ['Slide', 'of'],\n      close: true,\n      closeText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" /></svg>',\n      closeLabel: 'Close lightbox',\n      dialogTitle: 'Lightbox',\n      loadingIndicatorLabel: 'Image loading',\n      counter: true,\n      keyboard: true,\n      zoom: false,\n      zoomText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"16 4 20 4 20 8\" /><line x1=\"14\" y1=\"10\" x2=\"20\" y2=\"4\" /><polyline points=\"8 20 4 20 4 16\" /><line x1=\"4\" y1=\"20\" x2=\"10\" y2=\"14\" /><polyline points=\"16 20 20 20 20 16\" /><line x1=\"14\" y1=\"14\" x2=\"20\" y2=\"20\" /><polyline points=\"8 4 4 4 4 8\" /><line x1=\"4\" y1=\"4\" x2=\"10\" y2=\"10\" /></svg>',\n      docClose: true,\n      swipeClose: true,\n      hideScrollbar: true,\n      draggable: true,\n      threshold: 100,\n      theme: 'tobii--theme-default'\n    };\n    return {\n      ...OPTIONS,\n      ...userOptions\n    };\n  };\n\n  /**\n   * Init\n   *\n   */\n  const init = userOptions => {\n    // Merge user options into defaults\n    userSettings = mergeOptions(userOptions);\n\n    // Create the lightbox container\n    lightbox = document.createElement('div');\n    lightbox.setAttribute('role', 'dialog');\n    lightbox.setAttribute('aria-hidden', 'true');\n    lightbox.setAttribute('aria-modal', 'true');\n    lightbox.setAttribute('aria-label', userSettings.dialogTitle);\n    lightbox.classList.add('tobii');\n\n    // Add theme class\n    lightbox.classList.add(userSettings.theme);\n\n    // Create the previous button\n    prevButton = document.createElement('button');\n    prevButton.className = 'tobii__btn tobii__btn--previous';\n    prevButton.setAttribute('type', 'button');\n    prevButton.setAttribute('aria-label', userSettings.navLabel[0]);\n    prevButton.innerHTML = userSettings.navText[0];\n    lightbox.appendChild(prevButton);\n\n    // Create the next button\n    nextButton = document.createElement('button');\n    nextButton.className = 'tobii__btn tobii__btn--next';\n    nextButton.setAttribute('type', 'button');\n    nextButton.setAttribute('aria-label', userSettings.navLabel[1]);\n    nextButton.innerHTML = userSettings.navText[1];\n    lightbox.appendChild(nextButton);\n\n    // Create the close button\n    closeButton = document.createElement('button');\n    closeButton.className = 'tobii__btn tobii__btn--close';\n    closeButton.setAttribute('type', 'button');\n    closeButton.setAttribute('aria-label', userSettings.closeLabel);\n    closeButton.innerHTML = userSettings.closeText;\n    lightbox.appendChild(closeButton);\n\n    // Create the counter\n    counter = document.createElement('div');\n    counter.className = 'tobii__counter';\n    lightbox.appendChild(counter);\n\n    // Create the live region\n    liveRegion = document.createElement('div');\n    liveRegion.className = 'tobii__sr';\n    liveRegion.setAttribute('aria-live', 'polite');\n    liveRegion.setAttribute('aria-atomic', 'true');\n    lightbox.appendChild(liveRegion);\n\n    // Append to body\n    document.body.appendChild(lightbox);\n\n    // Init only\n    if (!userSettings.selector) return;\n\n    // Get a list of all elements within the document\n    const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector);\n    if (!LIGHTBOX_TRIGGER_ELS) {\n      throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`);\n    }\n    LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el));\n  };\n\n  /**\n   * Check dependencies\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const checkDependencies = el => {\n    // Check if there is a YouTube video and if the YouTube iframe-API is ready\n    if (document.querySelector('[data-type=\"youtube\"]') !== null && !isYouTubeDependencyLoaded) {\n      if (document.getElementById('iframe_api') === null) {\n        const TAG = document.createElement('script');\n        const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0];\n        TAG.id = 'iframe_api';\n        TAG.src = 'https://www.youtube.com/iframe_api';\n        FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG);\n      }\n      if (WAITING_ELS.indexOf(el) === -1) {\n        WAITING_ELS.push(el);\n      }\n      window.onYouTubePlayerAPIReady = () => {\n        WAITING_ELS.forEach(waitingEl => {\n          add(waitingEl);\n        });\n        isYouTubeDependencyLoaded = true;\n      };\n    } else {\n      add(el);\n    }\n  };\n\n  /**\n   * Get group name from element\n   *\n   * @param {HTMLElement} el\n   * @return {string}\n   */\n  const getGroupName = el => {\n    return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default';\n  };\n\n  /**\n   * Copy an object. (The secure way)\n   *\n   * @param {object} object\n   * @return {object}\n   */\n  const copyObject = object => {\n    return JSON.parse(JSON.stringify(object));\n  };\n\n  /**\n   * Add element\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const add = el => {\n    const newGroup = getGroupName(el);\n    if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {\n      groups[newGroup] = copyObject(GROUP_ATTS);\n\n      // Create slider\n      groups[newGroup].slider = document.createElement('div');\n      groups[newGroup].slider.className = 'tobii__slider';\n\n      // Hide slider\n      groups[newGroup].slider.setAttribute('aria-hidden', 'true');\n      lightbox.appendChild(groups[newGroup].slider);\n    }\n\n    // Check if element already exists\n    if (groups[newGroup].gallery.indexOf(el) === -1) {\n      groups[newGroup].gallery.push(el);\n      groups[newGroup].elementsLength++;\n\n      // Set zoom icon if necessary\n      if (userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false' || el.getAttribute('data-zoom') === 'true') {\n        const TOBII_ZOOM = document.createElement('div');\n        TOBII_ZOOM.className = 'tobii-zoom__icon';\n        TOBII_ZOOM.innerHTML = userSettings.zoomText;\n        el.classList.add('tobii-zoom');\n        el.appendChild(TOBII_ZOOM);\n      }\n\n      // Bind click event handler\n      el.addEventListener('click', triggerTobii);\n\n      // Create slide\n      const SLIDE_ELEMENT = document.createElement('div');\n      const SLIDE_ELEMENT_CONTENT = document.createElement('div');\n      SLIDE_ELEMENT.className = 'tobii__slide';\n      SLIDE_ELEMENT.style.position = 'absolute';\n      SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`;\n\n      // Hide slide\n      SLIDE_ELEMENT.setAttribute('aria-hidden', 'true');\n\n      // Create type elements\n      const model = getModel(el);\n      model.init(el, SLIDE_ELEMENT_CONTENT, userSettings);\n\n      // Add slide content container to slide element\n      SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT);\n\n      // Add slide element to slider\n      groups[newGroup].slider.appendChild(SLIDE_ELEMENT);\n      groups[newGroup].sliderElements.push(SLIDE_ELEMENT);\n      ++groups[newGroup].x;\n      if (isOpen() && newGroup === activeGroup) {\n        updateConfig();\n        updateLightbox();\n      }\n    } else {\n      throw new Error('Ups, element already added.');\n    }\n  };\n\n  /**\n   * Remove element\n   *\n   * @param {HTMLElement} el - Element to remove\n   */\n  const remove = el => {\n    const GROUP_NAME = getGroupName(el);\n\n    // Check if element exists\n    const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el);\n    if (galleryIndex === -1) {\n      throw new Error(`Ups, I can't find a slide for the element ${el}.`);\n    }\n    const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex];\n\n    // If the element to be removed is the currently visible slide\n    if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) {\n      if (groups[GROUP_NAME].elementsLength === 1) {\n        close();\n        throw new Error('Ups, I\\'ve closed. There are no slides more to show.');\n      } else {\n        // Navigate away before removal\n        if (groups[GROUP_NAME].currentIndex === 0) {\n          next();\n        } else {\n          previous();\n        }\n        updateConfig();\n        updateLightbox();\n      }\n    }\n    groups[GROUP_NAME].gallery.splice(galleryIndex, 1);\n    groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1);\n    groups[GROUP_NAME].elementsLength--;\n    --groups[GROUP_NAME].x;\n\n    // Remove zoom icon if necessary\n    if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) {\n      const ZOOM_ICON = el.querySelector('.tobii-zoom__icon');\n      ZOOM_ICON.parentNode.classList.remove('tobii-zoom');\n      ZOOM_ICON.parentNode.removeChild(ZOOM_ICON);\n    }\n\n    // Unbind click event handler\n    el.removeEventListener('click', triggerTobii);\n\n    // Remove slide\n    SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT);\n  };\n  const getModel = el => {\n    const type = el.getAttribute('data-type');\n    if (SUPPORTED_ELEMENTS[type] !== undefined) {\n      return SUPPORTED_ELEMENTS[type];\n    } else {\n      // unknown - use default\n      if (el.hasAttribute('data-type')) {\n        console.log('Unknown lightbox element type: ' + type);\n      }\n      return SUPPORTED_ELEMENTS.image;\n    }\n  };\n\n  /**\n   * Open Tobii\n   *\n   * @param {number} index - Index to load\n   */\n  const open = (index = 0) => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m aleady open.');\n    }\n    if (index === -1 || index >= groups[activeGroup].elementsLength) {\n      throw new Error(`Ups, I can't find slide ${index}.`);\n    }\n    document.documentElement.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open-' + activeGroup);\n    updateConfig();\n\n    // Hide close if necessary\n    if (!userSettings.close) {\n      closeButton.disabled = false;\n      closeButton.setAttribute('aria-hidden', 'true');\n    }\n\n    // Save user’s focus\n    lastFocus = document.activeElement;\n\n    // Use `history.pushState()` to make sure the \"Back\" button behavior\n    // that aligns with the user's expectations\n    const stateObj = {\n      tobii: 'close'\n    };\n    const url = window.location.href;\n    window.history.pushState(stateObj, 'Image', url);\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    bindEvents();\n\n    // Load slide\n    load(groups[activeGroup].currentIndex);\n\n    // Show slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'false');\n\n    // Show lightbox\n    lightbox.setAttribute('aria-hidden', 'false');\n    updateLightbox();\n\n    // Preload previous and next slide\n    preload(groups[activeGroup].currentIndex + 1);\n    preload(groups[activeGroup].currentIndex - 1);\n    groups[activeGroup].slider.classList.add('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const openEvent = new window.CustomEvent('open', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(openEvent);\n  };\n\n  /**\n   * Close Tobii\n   *\n   */\n  const close = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m already closed.');\n    }\n    document.documentElement.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open-' + activeGroup);\n    unbindEvents();\n\n    // Remove entry in browser history\n    if (window.history.state !== null) {\n      if (window.history.state.tobii === 'close') {\n        window.history.back();\n      }\n    }\n\n    // Reenable the user’s focus\n    lastFocus.focus();\n\n    // Don't forget to cleanup our current element\n    leave(groups[activeGroup].currentIndex);\n    cleanup(groups[activeGroup].currentIndex);\n\n    // Hide lightbox\n    lightbox.setAttribute('aria-hidden', 'true');\n\n    // Hide slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'true');\n\n    // Reset current index\n    groups[activeGroup].currentIndex = 0;\n\n    // Remove the hack to prevent animation during opening\n    groups[activeGroup].slider.classList.remove('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const closeEvent = new window.CustomEvent('close', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(closeEvent);\n  };\n\n  /**\n   * Preload slide\n   *\n   * @param {number} index - Index to preload\n   */\n  const preload = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onPreload(CONTAINER);\n  };\n\n  /**\n   * Load slide\n   * Will be called when opening the lightbox or moving index\n   *\n   * @param {number} index - Index to load\n   */\n  const load = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLoad(CONTAINER, activeGroup);\n  };\n\n  /**\n   * Select a slide\n   *\n   * @param {number} index - Index to select\n   */\n  const select = index => {\n    const currIndex = groups[activeGroup].currentIndex;\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (isOpen()) {\n      if (!index && index !== 0) {\n        throw new Error('Ups, no slide specified.');\n      }\n      if (index === groups[activeGroup].currentIndex) {\n        throw new Error(`Ups, slide ${index} is already selected.`);\n      }\n      if (index === -1 || index >= groups[activeGroup].elementsLength) {\n        throw new Error(`Ups, I can't find slide ${index}.`);\n      }\n    }\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    leave(currIndex);\n    load(index);\n    if (index < currIndex) {\n      updateLightbox('left');\n      cleanup(currIndex);\n      preload(index - 1);\n    }\n    if (index > currIndex) {\n      updateLightbox('right');\n      cleanup(currIndex);\n      preload(index + 1);\n    }\n  };\n\n  /**\n   * Select the previous slide\n   *\n   */\n  const previous = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex > 0) {\n      leave(groups[activeGroup].currentIndex);\n      load(--groups[activeGroup].currentIndex);\n      updateLightbox('left');\n      cleanup(groups[activeGroup].currentIndex + 1);\n      preload(groups[activeGroup].currentIndex - 1);\n    }\n\n    // Create and dispatch a new event\n    const previousEvent = new window.CustomEvent('previous', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(previousEvent);\n  };\n\n  /**\n   * Select the next slide\n   *\n   */\n  const next = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) {\n      leave(groups[activeGroup].currentIndex);\n      load(++groups[activeGroup].currentIndex);\n      updateLightbox('right');\n      cleanup(groups[activeGroup].currentIndex - 1);\n      preload(groups[activeGroup].currentIndex + 1);\n    }\n\n    // Create and dispatch a new event\n    const nextEvent = new window.CustomEvent('next', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(nextEvent);\n  };\n\n  /**\n   * Select a group\n   *\n   * @param {string} name - Name of the group to select\n   */\n  const selectGroup = name => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m open.');\n    }\n    if (!name) {\n      throw new Error('Ups, no group specified.');\n    }\n    if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {\n      throw new Error(`Ups, I don't have a group called \"${name}\".`);\n    }\n    activeGroup = name;\n  };\n\n  /**\n   * Leave slide\n   * Will be called before moving index\n   *\n   * @param {number} index - Index to leave\n   */\n  const leave = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLeave(CONTAINER);\n  };\n\n  /**\n   * Cleanup slide\n   * Will be called after moving index\n   *\n   * @param {number} index - Index to cleanup\n   */\n  const cleanup = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onCleanup(CONTAINER);\n    DRAG.startX = 0;\n    DRAG.startY = 0;\n    DRAG.x = 0;\n    DRAG.y = 0;\n    DRAG.distance = 0;\n    lastTapTime = 0;\n    if (isZoomed()) resetZoom();\n    TRANSFORM.element = null;\n  };\n\n  /**\n   * Update offset\n   *\n   */\n  const updateOffset = () => {\n    offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth;\n    groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`;\n  };\n\n  /**\n   * Update counter\n   *\n   */\n  const updateCounter = () => {\n    counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`;\n  };\n\n  /**\n   * Update focus\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateFocus = dir => {\n    const group = groups[activeGroup];\n    const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto';\n    const hasMultipleSlides = group.elementsLength > 1;\n    if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) {\n      setButtonState(prevButton, true, true);\n      setButtonState(nextButton, true, true);\n      if (group.currentIndex === 0) {\n        setButtonState(nextButton, false, false);\n        nextButton.focus();\n      } else if (group.currentIndex === group.elementsLength - 1) {\n        setButtonState(prevButton, false, false);\n        prevButton.focus();\n      } else {\n        setButtonState(prevButton, false, false);\n        setButtonState(nextButton, false, false);\n        if (dir === 'left') {\n          prevButton.focus();\n        } else {\n          nextButton.focus();\n        }\n      }\n    } else if (userSettings.close) {\n      closeButton.focus();\n    }\n  };\n\n  /**\n   * Resize event\n   *\n   */\n  const resizeHandler = () => {\n    updateOffset();\n  };\n\n  /**\n   * Click event handler to trigger Tobii\n   *\n   */\n  const triggerTobii = event => {\n    event.preventDefault();\n    activeGroup = getGroupName(event.currentTarget);\n    open(groups[activeGroup].gallery.indexOf(event.currentTarget));\n  };\n\n  /**\n   * Click event handler\n   *\n   */\n  const clickHandler = event => {\n    if (event.target === prevButton) {\n      previous();\n    } else if (event.target === nextButton) {\n      next();\n    } else if (event.target === closeButton || event.target.classList.contains('tobii__slide') || event.target.classList.contains('tobii') && userSettings.docClose) {\n      close();\n    }\n    event.stopPropagation();\n  };\n\n  /**\n   * Set the hidden/disabled state of a button\n   *\n   */\n  const setButtonState = (button, hidden, disabled) => {\n    button.setAttribute('aria-hidden', hidden ? 'true' : 'false');\n    button.disabled = disabled;\n  };\n\n  /**\n   * Keydown event handler\n   *\n   */\n  const keydownHandler = event => {\n    if (event.code === 'Tab') {\n      const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', ')));\n      if (FOCUSABLE.length === 0) return;\n      const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement);\n      if (event.shiftKey && FOCUSED_INDEX === 0) {\n        // SHIFT+Tab on first → jump to last\n        FOCUSABLE[FOCUSABLE.length - 1].focus();\n        event.preventDefault();\n      } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) {\n        // Tab on last → jump to first\n        FOCUSABLE[0].focus();\n        event.preventDefault();\n      }\n    } else if (event.code === 'Escape') {\n      // `ESC` Key: Close Tobii\n      event.preventDefault();\n      close();\n    } else if (event.code === 'ArrowLeft') {\n      // `PREV` Key: Show the previous slide\n      event.preventDefault();\n      previous();\n    } else if (event.code === 'ArrowRight') {\n      // `NEXT` Key: Show the next slide\n      event.preventDefault();\n      next();\n    }\n  };\n\n  /**\n   * Contextmenu event handler\n   * This is a fix for chromium based browser on mac.\n   * The 'contextmenu' terminates a mouse event sequence.\n   * https://bugs.chromium.org/p/chromium/issues/detail?id=506801\n   *\n   */\n  const contextmenuHandler = () => {\n    pointerDownCache = [];\n    updateOffset();\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n  };\n\n  /**\n   * Pointerdown event handler\n   *\n   */\n  const pointerdownHandler = event => {\n    // Prevent dragging / swiping on textareas, inputs and selects\n    if (isIgnoreElement(event.target)) {\n      return;\n    }\n    event.preventDefault();\n    event.stopPropagation();\n    DRAG.startX = DRAG.x = event.clientX;\n    DRAG.startY = DRAG.y = event.clientY;\n    DRAG.distance = 0;\n\n    // This event is cached to support 2-finger gestures\n    pointerDownCache.push(event);\n    if (pointerDownCache.length === 2) {\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      DRAG.startX = DRAG.x = x;\n      DRAG.startY = DRAG.y = y;\n      DRAG.distance = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / TRANSFORM.scale;\n    }\n  };\n\n  /**\n   * Pointermove event handler\n   *\n   */\n  const pointermoveHandler = event => {\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Find this event in the cache and update its record with this event\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache[index] = event;\n    if (pointerDownCache.length === 2) {\n      // 2-pointer horizontal pinch/zoom gesture\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      const scale = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / DRAG.distance;\n      zoomPan(event.target, clamp(scale, MIN_SCALE, MAX_SCALE), x, y, x - DRAG.x, y - DRAG.y);\n      DRAG.x = x;\n      DRAG.y = y;\n      return;\n    }\n    if (isZoomed()) {\n      const deltaX = event.clientX - DRAG.x;\n      const deltaY = event.clientY - DRAG.y;\n      pan(deltaX, deltaY);\n    }\n    DRAG.x = event.clientX;\n    DRAG.y = event.clientY;\n    if (!isZoomed()) {\n      // Drag animation\n      const deltaX = DRAG.startX - DRAG.x;\n      const deltaY = DRAG.startY - DRAG.y;\n\n      // Skip animation if drag distance is too low\n      if (distance(deltaX, deltaY) < 10) return;\n      if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) {\n        // Horizontal swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset - Math.round(deltaX)}px, 0)`;\n      } else if (userSettings.swipeClose) {\n        // Vertical swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset}px, -${Math.round(deltaY)}px)`;\n      }\n    }\n  };\n\n  /**\n   * Pointerup event handler\n   *\n   */\n  const pointerupHandler = event => {\n    // Intercept regular click handler\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Remove this event from the target's cache\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache.splice(index, 1);\n    const x = event.clientX;\n    const y = event.clientY;\n    const deltaX = DRAG.startX - x;\n    const deltaY = DRAG.startY - y;\n    const distanceX = Math.abs(deltaX);\n    const distanceY = Math.abs(deltaY);\n    if (distanceX > 8 || distanceY > 8) {\n      if (!isZoomed()) {\n        // Evaluate drag\n        if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) {\n          previous();\n        } else if (deltaX > 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) {\n          next();\n        } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) {\n          close();\n        } else {\n          updateOffset();\n        }\n      }\n    } else {\n      // Evaluate tap\n      const now = Date.now();\n      const tapLength = now - lastTapTime;\n      if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) {\n        // Double click\n        event.preventDefault();\n        lastTapTime = 0;\n        if (isZoomed()) {\n          resetZoom();\n        } else {\n          zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0);\n        }\n      } else {\n        lastTapTime = now;\n        if (isTouchDevice()) {\n          // Delayed tap on mobile\n          window.setTimeout(() => {\n            const {\n              left,\n              top,\n              bottom,\n              right,\n              width\n            } = event.target.getBoundingClientRect();\n            if (y < top || y > bottom || !lastTapTime) return;\n            if (x > left && x < left + width / 2) {\n              previous();\n            } else if (x < right && x > right - width / 2) {\n              next();\n            }\n          }, DOUBLE_TAP_TIME);\n        }\n      }\n    }\n  };\n\n  /**\n   * Wheel event handler\n   *\n   */\n  const wheelHandler = event => {\n    const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1;\n    if (!isZoomed() && !deltaScale) return;\n    event.preventDefault();\n    const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale);\n    zoomPan(event.target, clamp(newScale, MIN_SCALE, MAX_SCALE), event.clientX, event.clientY, 0, 0);\n  };\n  const clampedTranslate = (axis, translate) => {\n    // Whole clamping functionality heavily inspired\n    // by https://github.com/Neophen/pinch-zoom-pan\n    const {\n      element,\n      scale,\n      originX,\n      originY\n    } = TRANSFORM;\n    const axisIsX = axis === 'x';\n    const origin = axisIsX ? originX : originY;\n    const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight';\n    const containerSize = element.parentNode[axisKey];\n    const imageSize = element[axisKey];\n    const bounds = element.getBoundingClientRect();\n    const imageScaledSize = axisIsX ? bounds.width : bounds.height;\n    const defaultOrigin = imageSize / 2;\n    const originOffset = (origin - defaultOrigin) * (scale - 1);\n    const range = Math.max(0, Math.round(imageScaledSize) - containerSize);\n    const max = Math.round(range / 2);\n    const min = 0 - max;\n    return clamp(translate, min + originOffset, max + originOffset);\n  };\n  const clamp = (value, min, max) => Math.max(Math.min(value, max), min);\n  const isZoomed = () => TRANSFORM.scale !== MIN_SCALE;\n  const pan = (deltaX, deltaY) => {\n    if (deltaX !== 0) {\n      TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX);\n    }\n    if (deltaY !== 0) {\n      TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY);\n    }\n    const {\n      element,\n      originX,\n      originY,\n      translateX,\n      translateY,\n      scale\n    } = TRANSFORM;\n    element.style.transformOrigin = `${originX}px ${originY}px`;\n    element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;\n  };\n  const zoomPan = (el, newScale, x, y, deltaX, deltaY) => {\n    if (el.tagName !== 'IMG') return;\n    const {\n      left,\n      top\n    } = el.getBoundingClientRect();\n    const originX = x - left;\n    const originY = y - top;\n    const newOriginX = originX / TRANSFORM.scale;\n    const newOriginY = originY / TRANSFORM.scale;\n    TRANSFORM.element = el;\n    TRANSFORM.originX = newOriginX;\n    TRANSFORM.originY = newOriginY;\n    TRANSFORM.scale = newScale;\n    pan(deltaX, deltaY);\n  };\n  const distance = (dx, dy) => Math.hypot(dx, dy);\n  const midPoint = (x1, y1, x2, y2) => ({\n    x: (x1 + x2) / 2,\n    y: (y1 + y2) / 2\n  });\n  const resetZoom = () => {\n    TRANSFORM.scale = MIN_SCALE;\n    TRANSFORM.originX = 0;\n    TRANSFORM.originY = 0;\n    TRANSFORM.translateX = 0;\n    TRANSFORM.translateY = 0;\n    pan(0, 0);\n  };\n\n  /**\n   * Bind events\n   *\n   */\n  const bindEvents = () => {\n    if (userSettings.keyboard) {\n      window.addEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.addEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.addEventListener('popstate', close);\n\n    // Click event\n    on('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      on('pointerdown', pointerdownHandler);\n      on('pointermove', pointermoveHandler);\n      on('pointerup', pointerupHandler);\n      on('pointercancel', contextmenuHandler);\n      on('pointerout', contextmenuHandler);\n      on('pointerleave', contextmenuHandler);\n      on('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    on('wheel', wheelHandler);\n  };\n\n  /**\n   * Unbind events\n   *\n   */\n  const unbindEvents = () => {\n    if (userSettings.keyboard) {\n      window.removeEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.removeEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.removeEventListener('popstate', close);\n\n    // Click event\n    off('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      off('pointerdown', pointerdownHandler);\n      off('pointermove', pointermoveHandler);\n      off('pointerup', pointerupHandler);\n      off('pointercancel', contextmenuHandler);\n      off('pointerout', contextmenuHandler);\n      off('pointerleave', contextmenuHandler);\n      off('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    off('wheel', wheelHandler);\n  };\n\n  /**\n   * Update userSettings\n   *\n   */\n  const updateConfig = () => {\n    const group = groups[activeGroup];\n    const slider = group.slider;\n    if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) {\n      slider.classList.add('tobii__slider--is-draggable');\n    }\n    const hideButtons = !userSettings.nav || group.elementsLength === 1 || userSettings.nav === 'auto' && isTouchDevice();\n    setButtonState(prevButton, hideButtons, hideButtons);\n    setButtonState(nextButton, hideButtons, hideButtons);\n    const hideCounter = !userSettings.counter || group.elementsLength === 1;\n    counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false');\n  };\n\n  /**\n   * Update live region\n   *\n   */\n  const updateAnnouncement = () => {\n    const group = groups[activeGroup];\n    const currIndex = group.currentIndex;\n    const total = group.elementsLength;\n    const trigger = group.gallery[currIndex];\n    const [slide, of] = userSettings.announcementLabel;\n    let extra;\n    if (trigger.hasAttribute('data-label')) {\n      extra = trigger.getAttribute('data-label');\n    } else {\n      const img = trigger.querySelector('img');\n      extra = img?.alt || '';\n    }\n    const base = `${slide} ${currIndex + 1} ${of} ${total}`;\n\n    // Announce reliably\n    liveRegion.textContent = '';\n    window.setTimeout(() => {\n      liveRegion.textContent = extra ? `${base}. ${extra}` : base;\n    }, 10);\n  };\n\n  /**\n   * Update lightbox\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateLightbox = (dir = null) => {\n    updateOffset();\n    updateCounter();\n    updateAnnouncement();\n    updateFocus(dir);\n  };\n\n  /**\n   * Reset Tobii\n   *\n   */\n  const reset = () => {\n    if (isOpen()) close();\n    Object.values(groups).forEach(group => group.gallery.forEach(remove));\n    groups = {};\n    activeGroup = null;\n    Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset());\n  };\n\n  /**\n   * Destroy Tobii\n   *\n   */\n  const destroy = () => {\n    reset();\n    lightbox.parentNode.removeChild(lightbox);\n  };\n\n  /**\n   * Check if Tobii is open\n   *\n   */\n  const isOpen = () => {\n    return lightbox.getAttribute('aria-hidden') === 'false';\n  };\n\n  /**\n   * Detect whether device is touch capable\n   *\n   */\n  const isTouchDevice = () => {\n    return 'ontouchstart' in window;\n  };\n\n  /**\n   * Checks whether element's tagName is part of array\n   *\n   */\n  const isIgnoreElement = el => {\n    return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton || el === nextButton || el === closeButton;\n  };\n\n  /**\n   * Return current index\n   *\n   */\n  const slidesIndex = () => {\n    return groups[activeGroup].currentIndex;\n  };\n\n  /**\n   * Return elements length\n   *\n   */\n  const slidesCount = () => {\n    return groups[activeGroup].elementsLength;\n  };\n\n  /**\n   * Return current group\n   *\n   */\n  const currentGroup = () => {\n    return activeGroup;\n  };\n\n  /**\n   * Bind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const on = (eventName, callback) => {\n    lightbox.addEventListener(eventName, callback);\n  };\n\n  /**\n   * Unbind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const off = (eventName, callback) => {\n    lightbox.removeEventListener(eventName, callback);\n  };\n  init(userOptions);\n  return {\n    open,\n    previous,\n    next,\n    close,\n    add: checkDependencies,\n    remove,\n    reset,\n    destroy,\n    isOpen,\n    slidesIndex,\n    select,\n    slidesCount,\n    selectGroup,\n    currentGroup,\n    on,\n    off\n  };\n}\n\nmodule.exports = Tobii;\n"
  },
  {
    "path": "dist/tobii.modern.js",
    "content": "function _extends() {\n  return _extends = Object.assign ? Object.assign.bind() : function (n) {\n    for (var e = 1; e < arguments.length; e++) {\n      var t = arguments[e];\n      for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n    }\n    return n;\n  }, _extends.apply(null, arguments);\n}\n\nclass ImageType {\n  constructor() {\n    this.figcaptionId = 0;\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const FIGURE = document.createElement('figure');\n    const IMAGE = document.createElement('img');\n    const THUMBNAIL = el.querySelector('img');\n    const LOADING_INDICATOR = document.createElement('div');\n\n    // Accessibility: allow setting focus programmatically on figure elements.\n    FIGURE.tabIndex = -1;\n\n    // Add role=\"group\" to figure\n    FIGURE.setAttribute('role', 'group');\n\n    // Hide figure until the image is loaded\n    FIGURE.style.opacity = '0';\n    if (THUMBNAIL) {\n      IMAGE.alt = THUMBNAIL.alt || '';\n    }\n    IMAGE.setAttribute('data-src', el.href);\n    if (el.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset'));\n    }\n    if (el.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes'));\n    }\n\n    // Add image to figure\n    FIGURE.appendChild(IMAGE);\n\n    // Create figcaption\n    let captionContent;\n    if (typeof this.userSettings.captionText === 'function') {\n      captionContent = this.userSettings.captionText(el);\n    } else if (this.userSettings.captionsSelector === 'self' && el.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = el.getAttribute(this.userSettings.captionAttribute);\n    } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL && THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute);\n    }\n    if (this.userSettings.captions && captionContent) {\n      const FIGCAPTION = document.createElement('figcaption');\n      FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`;\n      const SPAN = document.createElement('span');\n      if (this.userSettings.captionHTML) {\n        SPAN.innerHTML = captionContent;\n      } else {\n        SPAN.textContent = captionContent;\n      }\n      FIGCAPTION.appendChild(SPAN);\n      if (this.userSettings.captionToggle) {\n        const isMobile = window.innerWidth < 768;\n        const BUTTON = document.createElement('button');\n        BUTTON.className = 'caption-toggle';\n        BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0];\n        BUTTON.setAttribute('aria-controls', FIGCAPTION.id);\n        BUTTON.setAttribute('aria-expanded', !isMobile);\n        if (isMobile) {\n          FIGCAPTION.classList.add('caption-hidden');\n        }\n        SPAN.setAttribute('aria-hidden', isMobile);\n        const preventAndStopEvent = event => {\n          event.preventDefault();\n          event.stopPropagation();\n        };\n        BUTTON.addEventListener('pointerdown', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('pointerup', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('click', event => {\n          preventAndStopEvent(event);\n          const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true';\n          const buttonLabel = isExpanded ? this.userSettings.captionToggleLabel[1] : this.userSettings.captionToggleLabel[0];\n          BUTTON.textContent = BUTTON.title = buttonLabel;\n          BUTTON.setAttribute('aria-expanded', !isExpanded);\n          FIGCAPTION.classList.toggle('caption-hidden');\n          SPAN.setAttribute('aria-hidden', isExpanded);\n        });\n        FIGCAPTION.appendChild(BUTTON);\n      }\n      FIGURE.appendChild(FIGCAPTION);\n      IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id);\n\n      // Add aria-label to the figure containing the caption content\n      FIGURE.setAttribute('aria-label', SPAN.textContent);\n      ++this.figcaptionId;\n    }\n\n    // Add figure to container\n    container.appendChild(FIGURE);\n\n    // Create loading indicator\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n\n    // Add loading indicator to container\n    container.appendChild(LOADING_INDICATOR);\n\n    // Register type\n    container.setAttribute('data-type', 'image');\n    container.classList.add('tobii-image');\n  }\n  onPreload(container) {\n    // Same as preload\n    this.onLoad(container);\n  }\n  onLoad(container) {\n    const IMAGE = container.querySelector('img');\n    if (!IMAGE.hasAttribute('data-src')) {\n      return;\n    }\n    const FIGURE = container.querySelector('figure');\n    const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n    const handleImageEvent = () => {\n      container.removeChild(LOADING_INDICATOR);\n      FIGURE.style.opacity = '1';\n    };\n    IMAGE.addEventListener('load', handleImageEvent);\n    IMAGE.addEventListener('error', handleImageEvent);\n    if (IMAGE.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset'));\n      IMAGE.removeAttribute('data-srcset');\n    }\n    if (IMAGE.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes'));\n      IMAGE.removeAttribute('data-sizes');\n    }\n    IMAGE.setAttribute('src', IMAGE.getAttribute('data-src'));\n    IMAGE.removeAttribute('data-src');\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    // Nothing\n  }\n  onReset() {\n    this.figcaptionId = 0;\n  }\n}\n\nclass IframeType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    container.setAttribute('data-HREF', HREF);\n    if (el.hasAttribute('data-allow')) {\n      container.setAttribute('data-allow', el.getAttribute('data-allow'));\n    }\n    if (el.hasAttribute('data-width')) {\n      container.setAttribute('data-width', `${el.getAttribute('data-width')}`);\n    }\n    if (el.hasAttribute('data-height')) {\n      container.setAttribute('data-height', `${el.getAttribute('data-height')}`);\n    }\n\n    // dont create empty iframes here - very slow\n\n    // Register type\n    container.setAttribute('data-type', 'iframe');\n    container.classList.add('tobii-iframe');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    let IFRAME = container.querySelector('iframe');\n\n    // Create loading indicator\n    const LOADING_INDICATOR = document.createElement('div');\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n    container.appendChild(LOADING_INDICATOR);\n    if (IFRAME == null) {\n      // create iframe\n      IFRAME = document.createElement('iframe');\n      const HREF = container.getAttribute('data-href');\n      IFRAME.setAttribute('frameborder', '0');\n      IFRAME.setAttribute('src', HREF);\n\n      // Set allow parameters\n      let allowValue = 'fullscreen';\n      if (HREF.includes('youtube.com')) {\n        allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';\n      } else if (HREF.includes('vimeo.com')) {\n        allowValue += '; autoplay; picture-in-picture';\n      } else if (container.hasAttribute('data-allow')) {\n        allowValue = container.getAttribute('data-allow');\n      }\n      IFRAME.setAttribute('allow', allowValue);\n      if (container.hasAttribute('data-width')) {\n        IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`;\n      }\n      if (container.hasAttribute('data-height')) {\n        IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`;\n      }\n\n      // Hide until loaded\n      IFRAME.style.opacity = '0';\n\n      // Add iframe to container\n      container.appendChild(IFRAME);\n\n      // Handle load and error\n      const removeLoader = () => {\n        IFRAME.style.opacity = '1';\n        const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n        if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR);\n      };\n      IFRAME.addEventListener('load', removeLoader);\n      IFRAME.addEventListener('error', removeLoader);\n    } else {\n      // was already created\n      IFRAME.setAttribute('src', container.getAttribute('data-href'));\n    }\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    const IFRAME = container.querySelector('iframe');\n    IFRAME.removeAttribute('src');\n    IFRAME.style.opacity = '0';\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\nclass HtmlType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    const TARGET = document.querySelector(TARGET_SELECTOR);\n    if (!TARGET) {\n      throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`);\n    }\n\n    // Add content to container\n    container.appendChild(TARGET);\n\n    // Register type\n    container.setAttribute('data-type', 'html');\n    container.classList.add('tobii-html');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container, group) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) {\n        // Continue where video was stopped\n        VIDEO.currentTime = VIDEO.getAttribute('data-time');\n      }\n\n      // Start playback (and loading if necessary)\n      VIDEO.play();\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      // Start playback (and loading if necessary)\n      audio.play();\n    }\n    container.classList.add('tobii-group-' + group);\n  }\n  onLeave(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (!VIDEO.paused) {\n        // Stop if video is playing\n        VIDEO.pause();\n      }\n\n      // Backup currentTime (needed for revisit)\n      if (VIDEO.readyState > 0) {\n        VIDEO.setAttribute('data-time', VIDEO.currentTime);\n      }\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      if (!audio.paused) {\n        // Stop if is playing\n        audio.pause();\n      }\n    }\n  }\n  onCleanup(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) {\n        // Some data has been loaded but not the whole package.\n        // In order to save bandwidth, stop downloading as soon as possible.\n        const VIDEO_CLONE = VIDEO.cloneNode(true);\n        this._removeSources(VIDEO);\n        VIDEO.load();\n        VIDEO.parentNode.removeChild(VIDEO);\n        container.appendChild(VIDEO_CLONE);\n      }\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n\n  /**\n   * Remove all `src` attributes\n   *\n   * @param {HTMLElement} el - Element to remove all `src` attributes\n   */\n  _removeSources(el) {\n    const SOURCES = el.querySelectorAll('src');\n    if (SOURCES) {\n      SOURCES.forEach(source => {\n        source.removeAttribute('src');\n      });\n    }\n  }\n}\n\nclass YoutubeType {\n  constructor() {\n    this.playerId = 0;\n    this.PLAYER = [];\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const IFRAME_PLACEHOLDER = document.createElement('div');\n\n    // Add iframePlaceholder to container\n    container.appendChild(IFRAME_PLACEHOLDER);\n    this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {\n      host: 'https://www.youtube-nocookie.com',\n      height: el.getAttribute('data-height') || '360',\n      width: el.getAttribute('data-width') || '640',\n      videoId: el.getAttribute('data-id'),\n      playerVars: {\n        controls: el.getAttribute('data-controls') || 1,\n        rel: 0,\n        playsinline: 1\n      }\n    });\n\n    // Set player ID\n    container.setAttribute('data-player', this.playerId);\n\n    // Register type\n    container.setAttribute('data-type', 'youtube');\n    container.classList.add('tobii-youtube');\n    this.playerId++;\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    this.PLAYER[container.getAttribute('data-player')].playVideo();\n  }\n  onLeave(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onCleanup(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\nfunction Tobii(userOptions) {\n  /**\n   * Global variables\n   *\n   */\n  const SUPPORTED_ELEMENTS = {\n    image: new ImageType(),\n    // default\n    html: new HtmlType(),\n    iframe: new IframeType(),\n    youtube: new YoutubeType()\n  };\n  const FOCUSABLE_ELEMENTS = ['a[href]:not([tabindex^=\"-\"]):not([inert])', 'area[href]:not([tabindex^=\"-\"]):not([inert])', 'input:not([disabled]):not([inert])', 'select:not([disabled]):not([inert])', 'textarea:not([disabled]):not([inert])', 'button:not([disabled]):not([inert])', 'iframe:not([tabindex^=\"-\"]):not([inert])', 'audio:not([tabindex^=\"-\"]):not([inert])', 'video:not([tabindex^=\"-\"]):not([inert])', '[contenteditable]:not([tabindex^=\"-\"]):not([inert])', '[tabindex]:not([tabindex^=\"-\"]):not([inert])'];\n  let userSettings = {};\n  const WAITING_ELS = [];\n  const GROUP_ATTS = {\n    gallery: [],\n    slider: null,\n    sliderElements: [],\n    elementsLength: 0,\n    currentIndex: 0,\n    x: 0\n  };\n  let lightbox = null;\n  let prevButton = null;\n  let nextButton = null;\n  let closeButton = null;\n  let counter = null;\n  let lastFocus = null;\n  let offset = null;\n  let isYouTubeDependencyLoaded = false;\n  let groups = {};\n  let activeGroup = null;\n  let pointerDownCache = [];\n  let lastTapTime = 0;\n  let liveRegion = null;\n  const MIN_SCALE = 1;\n  const MAX_SCALE = 4;\n  const DOUBLE_TAP_TIME = 500; // milliseconds\n  const SCALE_SENSITIVITY = 10;\n  const TRANSFORM = {\n    element: null,\n    originX: 0,\n    originY: 0,\n    translateX: 0,\n    translateY: 0,\n    scale: MIN_SCALE\n  };\n  const DRAG = {\n    startX: 0,\n    startY: 0,\n    x: 0,\n    y: 0,\n    distance: 0\n  };\n\n  /**\n   * Merge default options with user options\n   *\n   * @param {Object} userOptions - Optional user options\n   * @returns {Object} - Custom options\n   */\n  const mergeOptions = userOptions => {\n    // Default options\n    const OPTIONS = {\n      selector: '.lightbox',\n      captions: true,\n      captionsSelector: 'img',\n      captionAttribute: 'alt',\n      captionText: null,\n      captionHTML: false,\n      captionToggle: true,\n      captionToggleLabel: ['Hide caption', 'Show caption'],\n      nav: 'auto',\n      navText: ['<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"15 6 9 12 15 18\" /></svg>', '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"9 6 15 12 9 18\" /></svg>'],\n      navLabel: ['Previous image', 'Next image'],\n      announcementLabel: ['Slide', 'of'],\n      close: true,\n      closeText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" /></svg>',\n      closeLabel: 'Close lightbox',\n      dialogTitle: 'Lightbox',\n      loadingIndicatorLabel: 'Image loading',\n      counter: true,\n      keyboard: true,\n      zoom: false,\n      zoomText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"16 4 20 4 20 8\" /><line x1=\"14\" y1=\"10\" x2=\"20\" y2=\"4\" /><polyline points=\"8 20 4 20 4 16\" /><line x1=\"4\" y1=\"20\" x2=\"10\" y2=\"14\" /><polyline points=\"16 20 20 20 20 16\" /><line x1=\"14\" y1=\"14\" x2=\"20\" y2=\"20\" /><polyline points=\"8 4 4 4 4 8\" /><line x1=\"4\" y1=\"4\" x2=\"10\" y2=\"10\" /></svg>',\n      docClose: true,\n      swipeClose: true,\n      hideScrollbar: true,\n      draggable: true,\n      threshold: 100,\n      theme: 'tobii--theme-default'\n    };\n    return _extends({}, OPTIONS, userOptions);\n  };\n\n  /**\n   * Init\n   *\n   */\n  const init = userOptions => {\n    // Merge user options into defaults\n    userSettings = mergeOptions(userOptions);\n\n    // Create the lightbox container\n    lightbox = document.createElement('div');\n    lightbox.setAttribute('role', 'dialog');\n    lightbox.setAttribute('aria-hidden', 'true');\n    lightbox.setAttribute('aria-modal', 'true');\n    lightbox.setAttribute('aria-label', userSettings.dialogTitle);\n    lightbox.classList.add('tobii');\n\n    // Add theme class\n    lightbox.classList.add(userSettings.theme);\n\n    // Create the previous button\n    prevButton = document.createElement('button');\n    prevButton.className = 'tobii__btn tobii__btn--previous';\n    prevButton.setAttribute('type', 'button');\n    prevButton.setAttribute('aria-label', userSettings.navLabel[0]);\n    prevButton.innerHTML = userSettings.navText[0];\n    lightbox.appendChild(prevButton);\n\n    // Create the next button\n    nextButton = document.createElement('button');\n    nextButton.className = 'tobii__btn tobii__btn--next';\n    nextButton.setAttribute('type', 'button');\n    nextButton.setAttribute('aria-label', userSettings.navLabel[1]);\n    nextButton.innerHTML = userSettings.navText[1];\n    lightbox.appendChild(nextButton);\n\n    // Create the close button\n    closeButton = document.createElement('button');\n    closeButton.className = 'tobii__btn tobii__btn--close';\n    closeButton.setAttribute('type', 'button');\n    closeButton.setAttribute('aria-label', userSettings.closeLabel);\n    closeButton.innerHTML = userSettings.closeText;\n    lightbox.appendChild(closeButton);\n\n    // Create the counter\n    counter = document.createElement('div');\n    counter.className = 'tobii__counter';\n    lightbox.appendChild(counter);\n\n    // Create the live region\n    liveRegion = document.createElement('div');\n    liveRegion.className = 'tobii__sr';\n    liveRegion.setAttribute('aria-live', 'polite');\n    liveRegion.setAttribute('aria-atomic', 'true');\n    lightbox.appendChild(liveRegion);\n\n    // Append to body\n    document.body.appendChild(lightbox);\n\n    // Init only\n    if (!userSettings.selector) return;\n\n    // Get a list of all elements within the document\n    const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector);\n    if (!LIGHTBOX_TRIGGER_ELS) {\n      throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`);\n    }\n    LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el));\n  };\n\n  /**\n   * Check dependencies\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const checkDependencies = el => {\n    // Check if there is a YouTube video and if the YouTube iframe-API is ready\n    if (document.querySelector('[data-type=\"youtube\"]') !== null && !isYouTubeDependencyLoaded) {\n      if (document.getElementById('iframe_api') === null) {\n        const TAG = document.createElement('script');\n        const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0];\n        TAG.id = 'iframe_api';\n        TAG.src = 'https://www.youtube.com/iframe_api';\n        FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG);\n      }\n      if (WAITING_ELS.indexOf(el) === -1) {\n        WAITING_ELS.push(el);\n      }\n      window.onYouTubePlayerAPIReady = () => {\n        WAITING_ELS.forEach(waitingEl => {\n          add(waitingEl);\n        });\n        isYouTubeDependencyLoaded = true;\n      };\n    } else {\n      add(el);\n    }\n  };\n\n  /**\n   * Get group name from element\n   *\n   * @param {HTMLElement} el\n   * @return {string}\n   */\n  const getGroupName = el => {\n    return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default';\n  };\n\n  /**\n   * Copy an object. (The secure way)\n   *\n   * @param {object} object\n   * @return {object}\n   */\n  const copyObject = object => {\n    return JSON.parse(JSON.stringify(object));\n  };\n\n  /**\n   * Add element\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const add = el => {\n    const newGroup = getGroupName(el);\n    if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {\n      groups[newGroup] = copyObject(GROUP_ATTS);\n\n      // Create slider\n      groups[newGroup].slider = document.createElement('div');\n      groups[newGroup].slider.className = 'tobii__slider';\n\n      // Hide slider\n      groups[newGroup].slider.setAttribute('aria-hidden', 'true');\n      lightbox.appendChild(groups[newGroup].slider);\n    }\n\n    // Check if element already exists\n    if (groups[newGroup].gallery.indexOf(el) === -1) {\n      groups[newGroup].gallery.push(el);\n      groups[newGroup].elementsLength++;\n\n      // Set zoom icon if necessary\n      if (userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false' || el.getAttribute('data-zoom') === 'true') {\n        const TOBII_ZOOM = document.createElement('div');\n        TOBII_ZOOM.className = 'tobii-zoom__icon';\n        TOBII_ZOOM.innerHTML = userSettings.zoomText;\n        el.classList.add('tobii-zoom');\n        el.appendChild(TOBII_ZOOM);\n      }\n\n      // Bind click event handler\n      el.addEventListener('click', triggerTobii);\n\n      // Create slide\n      const SLIDE_ELEMENT = document.createElement('div');\n      const SLIDE_ELEMENT_CONTENT = document.createElement('div');\n      SLIDE_ELEMENT.className = 'tobii__slide';\n      SLIDE_ELEMENT.style.position = 'absolute';\n      SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`;\n\n      // Hide slide\n      SLIDE_ELEMENT.setAttribute('aria-hidden', 'true');\n\n      // Create type elements\n      const model = getModel(el);\n      model.init(el, SLIDE_ELEMENT_CONTENT, userSettings);\n\n      // Add slide content container to slide element\n      SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT);\n\n      // Add slide element to slider\n      groups[newGroup].slider.appendChild(SLIDE_ELEMENT);\n      groups[newGroup].sliderElements.push(SLIDE_ELEMENT);\n      ++groups[newGroup].x;\n      if (isOpen() && newGroup === activeGroup) {\n        updateConfig();\n        updateLightbox();\n      }\n    } else {\n      throw new Error('Ups, element already added.');\n    }\n  };\n\n  /**\n   * Remove element\n   *\n   * @param {HTMLElement} el - Element to remove\n   */\n  const remove = el => {\n    const GROUP_NAME = getGroupName(el);\n\n    // Check if element exists\n    const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el);\n    if (galleryIndex === -1) {\n      throw new Error(`Ups, I can't find a slide for the element ${el}.`);\n    }\n    const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex];\n\n    // If the element to be removed is the currently visible slide\n    if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) {\n      if (groups[GROUP_NAME].elementsLength === 1) {\n        close();\n        throw new Error('Ups, I\\'ve closed. There are no slides more to show.');\n      } else {\n        // Navigate away before removal\n        if (groups[GROUP_NAME].currentIndex === 0) {\n          next();\n        } else {\n          previous();\n        }\n        updateConfig();\n        updateLightbox();\n      }\n    }\n    groups[GROUP_NAME].gallery.splice(galleryIndex, 1);\n    groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1);\n    groups[GROUP_NAME].elementsLength--;\n    --groups[GROUP_NAME].x;\n\n    // Remove zoom icon if necessary\n    if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) {\n      const ZOOM_ICON = el.querySelector('.tobii-zoom__icon');\n      ZOOM_ICON.parentNode.classList.remove('tobii-zoom');\n      ZOOM_ICON.parentNode.removeChild(ZOOM_ICON);\n    }\n\n    // Unbind click event handler\n    el.removeEventListener('click', triggerTobii);\n\n    // Remove slide\n    SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT);\n  };\n  const getModel = el => {\n    const type = el.getAttribute('data-type');\n    if (SUPPORTED_ELEMENTS[type] !== undefined) {\n      return SUPPORTED_ELEMENTS[type];\n    } else {\n      // unknown - use default\n      if (el.hasAttribute('data-type')) {\n        console.log('Unknown lightbox element type: ' + type);\n      }\n      return SUPPORTED_ELEMENTS.image;\n    }\n  };\n\n  /**\n   * Open Tobii\n   *\n   * @param {number} index - Index to load\n   */\n  const open = (index = 0) => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m aleady open.');\n    }\n    if (index === -1 || index >= groups[activeGroup].elementsLength) {\n      throw new Error(`Ups, I can't find slide ${index}.`);\n    }\n    document.documentElement.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open-' + activeGroup);\n    updateConfig();\n\n    // Hide close if necessary\n    if (!userSettings.close) {\n      closeButton.disabled = false;\n      closeButton.setAttribute('aria-hidden', 'true');\n    }\n\n    // Save user’s focus\n    lastFocus = document.activeElement;\n\n    // Use `history.pushState()` to make sure the \"Back\" button behavior\n    // that aligns with the user's expectations\n    const stateObj = {\n      tobii: 'close'\n    };\n    const url = window.location.href;\n    window.history.pushState(stateObj, 'Image', url);\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    bindEvents();\n\n    // Load slide\n    load(groups[activeGroup].currentIndex);\n\n    // Show slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'false');\n\n    // Show lightbox\n    lightbox.setAttribute('aria-hidden', 'false');\n    updateLightbox();\n\n    // Preload previous and next slide\n    preload(groups[activeGroup].currentIndex + 1);\n    preload(groups[activeGroup].currentIndex - 1);\n    groups[activeGroup].slider.classList.add('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const openEvent = new window.CustomEvent('open', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(openEvent);\n  };\n\n  /**\n   * Close Tobii\n   *\n   */\n  const close = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m already closed.');\n    }\n    document.documentElement.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open-' + activeGroup);\n    unbindEvents();\n\n    // Remove entry in browser history\n    if (window.history.state !== null) {\n      if (window.history.state.tobii === 'close') {\n        window.history.back();\n      }\n    }\n\n    // Reenable the user’s focus\n    lastFocus.focus();\n\n    // Don't forget to cleanup our current element\n    leave(groups[activeGroup].currentIndex);\n    cleanup(groups[activeGroup].currentIndex);\n\n    // Hide lightbox\n    lightbox.setAttribute('aria-hidden', 'true');\n\n    // Hide slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'true');\n\n    // Reset current index\n    groups[activeGroup].currentIndex = 0;\n\n    // Remove the hack to prevent animation during opening\n    groups[activeGroup].slider.classList.remove('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const closeEvent = new window.CustomEvent('close', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(closeEvent);\n  };\n\n  /**\n   * Preload slide\n   *\n   * @param {number} index - Index to preload\n   */\n  const preload = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onPreload(CONTAINER);\n  };\n\n  /**\n   * Load slide\n   * Will be called when opening the lightbox or moving index\n   *\n   * @param {number} index - Index to load\n   */\n  const load = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLoad(CONTAINER, activeGroup);\n  };\n\n  /**\n   * Select a slide\n   *\n   * @param {number} index - Index to select\n   */\n  const select = index => {\n    const currIndex = groups[activeGroup].currentIndex;\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (isOpen()) {\n      if (!index && index !== 0) {\n        throw new Error('Ups, no slide specified.');\n      }\n      if (index === groups[activeGroup].currentIndex) {\n        throw new Error(`Ups, slide ${index} is already selected.`);\n      }\n      if (index === -1 || index >= groups[activeGroup].elementsLength) {\n        throw new Error(`Ups, I can't find slide ${index}.`);\n      }\n    }\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    leave(currIndex);\n    load(index);\n    if (index < currIndex) {\n      updateLightbox('left');\n      cleanup(currIndex);\n      preload(index - 1);\n    }\n    if (index > currIndex) {\n      updateLightbox('right');\n      cleanup(currIndex);\n      preload(index + 1);\n    }\n  };\n\n  /**\n   * Select the previous slide\n   *\n   */\n  const previous = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex > 0) {\n      leave(groups[activeGroup].currentIndex);\n      load(--groups[activeGroup].currentIndex);\n      updateLightbox('left');\n      cleanup(groups[activeGroup].currentIndex + 1);\n      preload(groups[activeGroup].currentIndex - 1);\n    }\n\n    // Create and dispatch a new event\n    const previousEvent = new window.CustomEvent('previous', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(previousEvent);\n  };\n\n  /**\n   * Select the next slide\n   *\n   */\n  const next = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) {\n      leave(groups[activeGroup].currentIndex);\n      load(++groups[activeGroup].currentIndex);\n      updateLightbox('right');\n      cleanup(groups[activeGroup].currentIndex - 1);\n      preload(groups[activeGroup].currentIndex + 1);\n    }\n\n    // Create and dispatch a new event\n    const nextEvent = new window.CustomEvent('next', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(nextEvent);\n  };\n\n  /**\n   * Select a group\n   *\n   * @param {string} name - Name of the group to select\n   */\n  const selectGroup = name => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m open.');\n    }\n    if (!name) {\n      throw new Error('Ups, no group specified.');\n    }\n    if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {\n      throw new Error(`Ups, I don't have a group called \"${name}\".`);\n    }\n    activeGroup = name;\n  };\n\n  /**\n   * Leave slide\n   * Will be called before moving index\n   *\n   * @param {number} index - Index to leave\n   */\n  const leave = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLeave(CONTAINER);\n  };\n\n  /**\n   * Cleanup slide\n   * Will be called after moving index\n   *\n   * @param {number} index - Index to cleanup\n   */\n  const cleanup = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onCleanup(CONTAINER);\n    DRAG.startX = 0;\n    DRAG.startY = 0;\n    DRAG.x = 0;\n    DRAG.y = 0;\n    DRAG.distance = 0;\n    lastTapTime = 0;\n    if (isZoomed()) resetZoom();\n    TRANSFORM.element = null;\n  };\n\n  /**\n   * Update offset\n   *\n   */\n  const updateOffset = () => {\n    offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth;\n    groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`;\n  };\n\n  /**\n   * Update counter\n   *\n   */\n  const updateCounter = () => {\n    counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`;\n  };\n\n  /**\n   * Update focus\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateFocus = dir => {\n    const group = groups[activeGroup];\n    const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto';\n    const hasMultipleSlides = group.elementsLength > 1;\n    if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) {\n      setButtonState(prevButton, true, true);\n      setButtonState(nextButton, true, true);\n      if (group.currentIndex === 0) {\n        setButtonState(nextButton, false, false);\n        nextButton.focus();\n      } else if (group.currentIndex === group.elementsLength - 1) {\n        setButtonState(prevButton, false, false);\n        prevButton.focus();\n      } else {\n        setButtonState(prevButton, false, false);\n        setButtonState(nextButton, false, false);\n        if (dir === 'left') {\n          prevButton.focus();\n        } else {\n          nextButton.focus();\n        }\n      }\n    } else if (userSettings.close) {\n      closeButton.focus();\n    }\n  };\n\n  /**\n   * Resize event\n   *\n   */\n  const resizeHandler = () => {\n    updateOffset();\n  };\n\n  /**\n   * Click event handler to trigger Tobii\n   *\n   */\n  const triggerTobii = event => {\n    event.preventDefault();\n    activeGroup = getGroupName(event.currentTarget);\n    open(groups[activeGroup].gallery.indexOf(event.currentTarget));\n  };\n\n  /**\n   * Click event handler\n   *\n   */\n  const clickHandler = event => {\n    if (event.target === prevButton) {\n      previous();\n    } else if (event.target === nextButton) {\n      next();\n    } else if (event.target === closeButton || event.target.classList.contains('tobii__slide') || event.target.classList.contains('tobii') && userSettings.docClose) {\n      close();\n    }\n    event.stopPropagation();\n  };\n\n  /**\n   * Set the hidden/disabled state of a button\n   *\n   */\n  const setButtonState = (button, hidden, disabled) => {\n    button.setAttribute('aria-hidden', hidden ? 'true' : 'false');\n    button.disabled = disabled;\n  };\n\n  /**\n   * Keydown event handler\n   *\n   */\n  const keydownHandler = event => {\n    if (event.code === 'Tab') {\n      const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', ')));\n      if (FOCUSABLE.length === 0) return;\n      const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement);\n      if (event.shiftKey && FOCUSED_INDEX === 0) {\n        // SHIFT+Tab on first → jump to last\n        FOCUSABLE[FOCUSABLE.length - 1].focus();\n        event.preventDefault();\n      } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) {\n        // Tab on last → jump to first\n        FOCUSABLE[0].focus();\n        event.preventDefault();\n      }\n    } else if (event.code === 'Escape') {\n      // `ESC` Key: Close Tobii\n      event.preventDefault();\n      close();\n    } else if (event.code === 'ArrowLeft') {\n      // `PREV` Key: Show the previous slide\n      event.preventDefault();\n      previous();\n    } else if (event.code === 'ArrowRight') {\n      // `NEXT` Key: Show the next slide\n      event.preventDefault();\n      next();\n    }\n  };\n\n  /**\n   * Contextmenu event handler\n   * This is a fix for chromium based browser on mac.\n   * The 'contextmenu' terminates a mouse event sequence.\n   * https://bugs.chromium.org/p/chromium/issues/detail?id=506801\n   *\n   */\n  const contextmenuHandler = () => {\n    pointerDownCache = [];\n    updateOffset();\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n  };\n\n  /**\n   * Pointerdown event handler\n   *\n   */\n  const pointerdownHandler = event => {\n    // Prevent dragging / swiping on textareas, inputs and selects\n    if (isIgnoreElement(event.target)) {\n      return;\n    }\n    event.preventDefault();\n    event.stopPropagation();\n    DRAG.startX = DRAG.x = event.clientX;\n    DRAG.startY = DRAG.y = event.clientY;\n    DRAG.distance = 0;\n\n    // This event is cached to support 2-finger gestures\n    pointerDownCache.push(event);\n    if (pointerDownCache.length === 2) {\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      DRAG.startX = DRAG.x = x;\n      DRAG.startY = DRAG.y = y;\n      DRAG.distance = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / TRANSFORM.scale;\n    }\n  };\n\n  /**\n   * Pointermove event handler\n   *\n   */\n  const pointermoveHandler = event => {\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Find this event in the cache and update its record with this event\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache[index] = event;\n    if (pointerDownCache.length === 2) {\n      // 2-pointer horizontal pinch/zoom gesture\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      const scale = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / DRAG.distance;\n      zoomPan(event.target, clamp(scale, MIN_SCALE, MAX_SCALE), x, y, x - DRAG.x, y - DRAG.y);\n      DRAG.x = x;\n      DRAG.y = y;\n      return;\n    }\n    if (isZoomed()) {\n      const deltaX = event.clientX - DRAG.x;\n      const deltaY = event.clientY - DRAG.y;\n      pan(deltaX, deltaY);\n    }\n    DRAG.x = event.clientX;\n    DRAG.y = event.clientY;\n    if (!isZoomed()) {\n      // Drag animation\n      const deltaX = DRAG.startX - DRAG.x;\n      const deltaY = DRAG.startY - DRAG.y;\n\n      // Skip animation if drag distance is too low\n      if (distance(deltaX, deltaY) < 10) return;\n      if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) {\n        // Horizontal swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset - Math.round(deltaX)}px, 0)`;\n      } else if (userSettings.swipeClose) {\n        // Vertical swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset}px, -${Math.round(deltaY)}px)`;\n      }\n    }\n  };\n\n  /**\n   * Pointerup event handler\n   *\n   */\n  const pointerupHandler = event => {\n    // Intercept regular click handler\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Remove this event from the target's cache\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache.splice(index, 1);\n    const x = event.clientX;\n    const y = event.clientY;\n    const deltaX = DRAG.startX - x;\n    const deltaY = DRAG.startY - y;\n    const distanceX = Math.abs(deltaX);\n    const distanceY = Math.abs(deltaY);\n    if (distanceX > 8 || distanceY > 8) {\n      if (!isZoomed()) {\n        // Evaluate drag\n        if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) {\n          previous();\n        } else if (deltaX > 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) {\n          next();\n        } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) {\n          close();\n        } else {\n          updateOffset();\n        }\n      }\n    } else {\n      // Evaluate tap\n      const now = Date.now();\n      const tapLength = now - lastTapTime;\n      if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) {\n        // Double click\n        event.preventDefault();\n        lastTapTime = 0;\n        if (isZoomed()) {\n          resetZoom();\n        } else {\n          zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0);\n        }\n      } else {\n        lastTapTime = now;\n        if (isTouchDevice()) {\n          // Delayed tap on mobile\n          window.setTimeout(() => {\n            const {\n              left,\n              top,\n              bottom,\n              right,\n              width\n            } = event.target.getBoundingClientRect();\n            if (y < top || y > bottom || !lastTapTime) return;\n            if (x > left && x < left + width / 2) {\n              previous();\n            } else if (x < right && x > right - width / 2) {\n              next();\n            }\n          }, DOUBLE_TAP_TIME);\n        }\n      }\n    }\n  };\n\n  /**\n   * Wheel event handler\n   *\n   */\n  const wheelHandler = event => {\n    const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1;\n    if (!isZoomed() && !deltaScale) return;\n    event.preventDefault();\n    const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale);\n    zoomPan(event.target, clamp(newScale, MIN_SCALE, MAX_SCALE), event.clientX, event.clientY, 0, 0);\n  };\n  const clampedTranslate = (axis, translate) => {\n    // Whole clamping functionality heavily inspired\n    // by https://github.com/Neophen/pinch-zoom-pan\n    const {\n      element,\n      scale,\n      originX,\n      originY\n    } = TRANSFORM;\n    const axisIsX = axis === 'x';\n    const origin = axisIsX ? originX : originY;\n    const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight';\n    const containerSize = element.parentNode[axisKey];\n    const imageSize = element[axisKey];\n    const bounds = element.getBoundingClientRect();\n    const imageScaledSize = axisIsX ? bounds.width : bounds.height;\n    const defaultOrigin = imageSize / 2;\n    const originOffset = (origin - defaultOrigin) * (scale - 1);\n    const range = Math.max(0, Math.round(imageScaledSize) - containerSize);\n    const max = Math.round(range / 2);\n    const min = 0 - max;\n    return clamp(translate, min + originOffset, max + originOffset);\n  };\n  const clamp = (value, min, max) => Math.max(Math.min(value, max), min);\n  const isZoomed = () => TRANSFORM.scale !== MIN_SCALE;\n  const pan = (deltaX, deltaY) => {\n    if (deltaX !== 0) {\n      TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX);\n    }\n    if (deltaY !== 0) {\n      TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY);\n    }\n    const {\n      element,\n      originX,\n      originY,\n      translateX,\n      translateY,\n      scale\n    } = TRANSFORM;\n    element.style.transformOrigin = `${originX}px ${originY}px`;\n    element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;\n  };\n  const zoomPan = (el, newScale, x, y, deltaX, deltaY) => {\n    if (el.tagName !== 'IMG') return;\n    const {\n      left,\n      top\n    } = el.getBoundingClientRect();\n    const originX = x - left;\n    const originY = y - top;\n    const newOriginX = originX / TRANSFORM.scale;\n    const newOriginY = originY / TRANSFORM.scale;\n    TRANSFORM.element = el;\n    TRANSFORM.originX = newOriginX;\n    TRANSFORM.originY = newOriginY;\n    TRANSFORM.scale = newScale;\n    pan(deltaX, deltaY);\n  };\n  const distance = (dx, dy) => Math.hypot(dx, dy);\n  const midPoint = (x1, y1, x2, y2) => ({\n    x: (x1 + x2) / 2,\n    y: (y1 + y2) / 2\n  });\n  const resetZoom = () => {\n    TRANSFORM.scale = MIN_SCALE;\n    TRANSFORM.originX = 0;\n    TRANSFORM.originY = 0;\n    TRANSFORM.translateX = 0;\n    TRANSFORM.translateY = 0;\n    pan(0, 0);\n  };\n\n  /**\n   * Bind events\n   *\n   */\n  const bindEvents = () => {\n    if (userSettings.keyboard) {\n      window.addEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.addEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.addEventListener('popstate', close);\n\n    // Click event\n    on('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      on('pointerdown', pointerdownHandler);\n      on('pointermove', pointermoveHandler);\n      on('pointerup', pointerupHandler);\n      on('pointercancel', contextmenuHandler);\n      on('pointerout', contextmenuHandler);\n      on('pointerleave', contextmenuHandler);\n      on('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    on('wheel', wheelHandler);\n  };\n\n  /**\n   * Unbind events\n   *\n   */\n  const unbindEvents = () => {\n    if (userSettings.keyboard) {\n      window.removeEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.removeEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.removeEventListener('popstate', close);\n\n    // Click event\n    off('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      off('pointerdown', pointerdownHandler);\n      off('pointermove', pointermoveHandler);\n      off('pointerup', pointerupHandler);\n      off('pointercancel', contextmenuHandler);\n      off('pointerout', contextmenuHandler);\n      off('pointerleave', contextmenuHandler);\n      off('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    off('wheel', wheelHandler);\n  };\n\n  /**\n   * Update userSettings\n   *\n   */\n  const updateConfig = () => {\n    const group = groups[activeGroup];\n    const slider = group.slider;\n    if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) {\n      slider.classList.add('tobii__slider--is-draggable');\n    }\n    const hideButtons = !userSettings.nav || group.elementsLength === 1 || userSettings.nav === 'auto' && isTouchDevice();\n    setButtonState(prevButton, hideButtons, hideButtons);\n    setButtonState(nextButton, hideButtons, hideButtons);\n    const hideCounter = !userSettings.counter || group.elementsLength === 1;\n    counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false');\n  };\n\n  /**\n   * Update live region\n   *\n   */\n  const updateAnnouncement = () => {\n    const group = groups[activeGroup];\n    const currIndex = group.currentIndex;\n    const total = group.elementsLength;\n    const trigger = group.gallery[currIndex];\n    const [slide, of] = userSettings.announcementLabel;\n    let extra;\n    if (trigger.hasAttribute('data-label')) {\n      extra = trigger.getAttribute('data-label');\n    } else {\n      const img = trigger.querySelector('img');\n      extra = (img == null ? void 0 : img.alt) || '';\n    }\n    const base = `${slide} ${currIndex + 1} ${of} ${total}`;\n\n    // Announce reliably\n    liveRegion.textContent = '';\n    window.setTimeout(() => {\n      liveRegion.textContent = extra ? `${base}. ${extra}` : base;\n    }, 10);\n  };\n\n  /**\n   * Update lightbox\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateLightbox = (dir = null) => {\n    updateOffset();\n    updateCounter();\n    updateAnnouncement();\n    updateFocus(dir);\n  };\n\n  /**\n   * Reset Tobii\n   *\n   */\n  const reset = () => {\n    if (isOpen()) close();\n    Object.values(groups).forEach(group => group.gallery.forEach(remove));\n    groups = {};\n    activeGroup = null;\n    Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset());\n  };\n\n  /**\n   * Destroy Tobii\n   *\n   */\n  const destroy = () => {\n    reset();\n    lightbox.parentNode.removeChild(lightbox);\n  };\n\n  /**\n   * Check if Tobii is open\n   *\n   */\n  const isOpen = () => {\n    return lightbox.getAttribute('aria-hidden') === 'false';\n  };\n\n  /**\n   * Detect whether device is touch capable\n   *\n   */\n  const isTouchDevice = () => {\n    return 'ontouchstart' in window;\n  };\n\n  /**\n   * Checks whether element's tagName is part of array\n   *\n   */\n  const isIgnoreElement = el => {\n    return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton || el === nextButton || el === closeButton;\n  };\n\n  /**\n   * Return current index\n   *\n   */\n  const slidesIndex = () => {\n    return groups[activeGroup].currentIndex;\n  };\n\n  /**\n   * Return elements length\n   *\n   */\n  const slidesCount = () => {\n    return groups[activeGroup].elementsLength;\n  };\n\n  /**\n   * Return current group\n   *\n   */\n  const currentGroup = () => {\n    return activeGroup;\n  };\n\n  /**\n   * Bind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const on = (eventName, callback) => {\n    lightbox.addEventListener(eventName, callback);\n  };\n\n  /**\n   * Unbind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const off = (eventName, callback) => {\n    lightbox.removeEventListener(eventName, callback);\n  };\n  init(userOptions);\n  return {\n    open,\n    previous,\n    next,\n    close,\n    add: checkDependencies,\n    remove,\n    reset,\n    destroy,\n    isOpen,\n    slidesIndex,\n    select,\n    slidesCount,\n    selectGroup,\n    currentGroup,\n    on,\n    off\n  };\n}\n\nexport { Tobii as default };\n"
  },
  {
    "path": "dist/tobii.module.js",
    "content": "class ImageType {\n  constructor() {\n    this.figcaptionId = 0;\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const FIGURE = document.createElement('figure');\n    const IMAGE = document.createElement('img');\n    const THUMBNAIL = el.querySelector('img');\n    const LOADING_INDICATOR = document.createElement('div');\n\n    // Accessibility: allow setting focus programmatically on figure elements.\n    FIGURE.tabIndex = -1;\n\n    // Add role=\"group\" to figure\n    FIGURE.setAttribute('role', 'group');\n\n    // Hide figure until the image is loaded\n    FIGURE.style.opacity = '0';\n    if (THUMBNAIL) {\n      IMAGE.alt = THUMBNAIL.alt || '';\n    }\n    IMAGE.setAttribute('data-src', el.href);\n    if (el.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset'));\n    }\n    if (el.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes'));\n    }\n\n    // Add image to figure\n    FIGURE.appendChild(IMAGE);\n\n    // Create figcaption\n    let captionContent;\n    if (typeof this.userSettings.captionText === 'function') {\n      captionContent = this.userSettings.captionText(el);\n    } else if (this.userSettings.captionsSelector === 'self' && el.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = el.getAttribute(this.userSettings.captionAttribute);\n    } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL && THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute);\n    }\n    if (this.userSettings.captions && captionContent) {\n      const FIGCAPTION = document.createElement('figcaption');\n      FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`;\n      const SPAN = document.createElement('span');\n      if (this.userSettings.captionHTML) {\n        SPAN.innerHTML = captionContent;\n      } else {\n        SPAN.textContent = captionContent;\n      }\n      FIGCAPTION.appendChild(SPAN);\n      if (this.userSettings.captionToggle) {\n        const isMobile = window.innerWidth < 768;\n        const BUTTON = document.createElement('button');\n        BUTTON.className = 'caption-toggle';\n        BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0];\n        BUTTON.setAttribute('aria-controls', FIGCAPTION.id);\n        BUTTON.setAttribute('aria-expanded', !isMobile);\n        if (isMobile) {\n          FIGCAPTION.classList.add('caption-hidden');\n        }\n        SPAN.setAttribute('aria-hidden', isMobile);\n        const preventAndStopEvent = event => {\n          event.preventDefault();\n          event.stopPropagation();\n        };\n        BUTTON.addEventListener('pointerdown', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('pointerup', event => preventAndStopEvent(event));\n        BUTTON.addEventListener('click', event => {\n          preventAndStopEvent(event);\n          const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true';\n          const buttonLabel = isExpanded ? this.userSettings.captionToggleLabel[1] : this.userSettings.captionToggleLabel[0];\n          BUTTON.textContent = BUTTON.title = buttonLabel;\n          BUTTON.setAttribute('aria-expanded', !isExpanded);\n          FIGCAPTION.classList.toggle('caption-hidden');\n          SPAN.setAttribute('aria-hidden', isExpanded);\n        });\n        FIGCAPTION.appendChild(BUTTON);\n      }\n      FIGURE.appendChild(FIGCAPTION);\n      IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id);\n\n      // Add aria-label to the figure containing the caption content\n      FIGURE.setAttribute('aria-label', SPAN.textContent);\n      ++this.figcaptionId;\n    }\n\n    // Add figure to container\n    container.appendChild(FIGURE);\n\n    // Create loading indicator\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n\n    // Add loading indicator to container\n    container.appendChild(LOADING_INDICATOR);\n\n    // Register type\n    container.setAttribute('data-type', 'image');\n    container.classList.add('tobii-image');\n  }\n  onPreload(container) {\n    // Same as preload\n    this.onLoad(container);\n  }\n  onLoad(container) {\n    const IMAGE = container.querySelector('img');\n    if (!IMAGE.hasAttribute('data-src')) {\n      return;\n    }\n    const FIGURE = container.querySelector('figure');\n    const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n    const handleImageEvent = () => {\n      container.removeChild(LOADING_INDICATOR);\n      FIGURE.style.opacity = '1';\n    };\n    IMAGE.addEventListener('load', handleImageEvent);\n    IMAGE.addEventListener('error', handleImageEvent);\n    if (IMAGE.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset'));\n      IMAGE.removeAttribute('data-srcset');\n    }\n    if (IMAGE.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes'));\n      IMAGE.removeAttribute('data-sizes');\n    }\n    IMAGE.setAttribute('src', IMAGE.getAttribute('data-src'));\n    IMAGE.removeAttribute('data-src');\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    // Nothing\n  }\n  onReset() {\n    this.figcaptionId = 0;\n  }\n}\n\nclass IframeType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    container.setAttribute('data-HREF', HREF);\n    if (el.hasAttribute('data-allow')) {\n      container.setAttribute('data-allow', el.getAttribute('data-allow'));\n    }\n    if (el.hasAttribute('data-width')) {\n      container.setAttribute('data-width', `${el.getAttribute('data-width')}`);\n    }\n    if (el.hasAttribute('data-height')) {\n      container.setAttribute('data-height', `${el.getAttribute('data-height')}`);\n    }\n\n    // dont create empty iframes here - very slow\n\n    // Register type\n    container.setAttribute('data-type', 'iframe');\n    container.classList.add('tobii-iframe');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    let IFRAME = container.querySelector('iframe');\n\n    // Create loading indicator\n    const LOADING_INDICATOR = document.createElement('div');\n    LOADING_INDICATOR.className = 'tobii__loader';\n    LOADING_INDICATOR.setAttribute('role', 'progressbar');\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n    container.appendChild(LOADING_INDICATOR);\n    if (IFRAME == null) {\n      // create iframe\n      IFRAME = document.createElement('iframe');\n      const HREF = container.getAttribute('data-href');\n      IFRAME.setAttribute('frameborder', '0');\n      IFRAME.setAttribute('src', HREF);\n\n      // Set allow parameters\n      let allowValue = 'fullscreen';\n      if (HREF.includes('youtube.com')) {\n        allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';\n      } else if (HREF.includes('vimeo.com')) {\n        allowValue += '; autoplay; picture-in-picture';\n      } else if (container.hasAttribute('data-allow')) {\n        allowValue = container.getAttribute('data-allow');\n      }\n      IFRAME.setAttribute('allow', allowValue);\n      if (container.hasAttribute('data-width')) {\n        IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`;\n      }\n      if (container.hasAttribute('data-height')) {\n        IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`;\n      }\n\n      // Hide until loaded\n      IFRAME.style.opacity = '0';\n\n      // Add iframe to container\n      container.appendChild(IFRAME);\n\n      // Handle load and error\n      const removeLoader = () => {\n        IFRAME.style.opacity = '1';\n        const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n        if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR);\n      };\n      IFRAME.addEventListener('load', removeLoader);\n      IFRAME.addEventListener('error', removeLoader);\n    } else {\n      // was already created\n      IFRAME.setAttribute('src', container.getAttribute('data-href'));\n    }\n  }\n  onLeave(container) {\n    // Nothing\n  }\n  onCleanup(container) {\n    const IFRAME = container.querySelector('iframe');\n    IFRAME.removeAttribute('src');\n    IFRAME.style.opacity = '0';\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\nclass HtmlType {\n  constructor() {\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n    const TARGET = document.querySelector(TARGET_SELECTOR);\n    if (!TARGET) {\n      throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`);\n    }\n\n    // Add content to container\n    container.appendChild(TARGET);\n\n    // Register type\n    container.setAttribute('data-type', 'html');\n    container.classList.add('tobii-html');\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container, group) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) {\n        // Continue where video was stopped\n        VIDEO.currentTime = VIDEO.getAttribute('data-time');\n      }\n\n      // Start playback (and loading if necessary)\n      VIDEO.play();\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      // Start playback (and loading if necessary)\n      audio.play();\n    }\n    container.classList.add('tobii-group-' + group);\n  }\n  onLeave(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (!VIDEO.paused) {\n        // Stop if video is playing\n        VIDEO.pause();\n      }\n\n      // Backup currentTime (needed for revisit)\n      if (VIDEO.readyState > 0) {\n        VIDEO.setAttribute('data-time', VIDEO.currentTime);\n      }\n    }\n    const audio = container.querySelector('audio');\n    if (audio) {\n      if (!audio.paused) {\n        // Stop if is playing\n        audio.pause();\n      }\n    }\n  }\n  onCleanup(container) {\n    const VIDEO = container.querySelector('video');\n    if (VIDEO) {\n      if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) {\n        // Some data has been loaded but not the whole package.\n        // In order to save bandwidth, stop downloading as soon as possible.\n        const VIDEO_CLONE = VIDEO.cloneNode(true);\n        this._removeSources(VIDEO);\n        VIDEO.load();\n        VIDEO.parentNode.removeChild(VIDEO);\n        container.appendChild(VIDEO_CLONE);\n      }\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n\n  /**\n   * Remove all `src` attributes\n   *\n   * @param {HTMLElement} el - Element to remove all `src` attributes\n   */\n  _removeSources(el) {\n    const SOURCES = el.querySelectorAll('src');\n    if (SOURCES) {\n      SOURCES.forEach(source => {\n        source.removeAttribute('src');\n      });\n    }\n  }\n}\n\nclass YoutubeType {\n  constructor() {\n    this.playerId = 0;\n    this.PLAYER = [];\n    this.userSettings = null;\n  }\n  init(el, container, userSettings) {\n    this.userSettings = userSettings;\n    const IFRAME_PLACEHOLDER = document.createElement('div');\n\n    // Add iframePlaceholder to container\n    container.appendChild(IFRAME_PLACEHOLDER);\n    this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {\n      host: 'https://www.youtube-nocookie.com',\n      height: el.getAttribute('data-height') || '360',\n      width: el.getAttribute('data-width') || '640',\n      videoId: el.getAttribute('data-id'),\n      playerVars: {\n        controls: el.getAttribute('data-controls') || 1,\n        rel: 0,\n        playsinline: 1\n      }\n    });\n\n    // Set player ID\n    container.setAttribute('data-player', this.playerId);\n\n    // Register type\n    container.setAttribute('data-type', 'youtube');\n    container.classList.add('tobii-youtube');\n    this.playerId++;\n  }\n  onPreload(container) {\n    // Nothing\n  }\n  onLoad(container) {\n    this.PLAYER[container.getAttribute('data-player')].playVideo();\n  }\n  onLeave(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onCleanup(container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n    }\n  }\n  onReset() {\n    // Nothing\n  }\n}\n\n/**\n * Tobii\n *\n * @author midzer\n * @version 3.2.0\n * @url https://github.com/midzer/tobii\n *\n * MIT License\n */\nfunction Tobii(userOptions) {\n  /**\n   * Global variables\n   *\n   */\n  const SUPPORTED_ELEMENTS = {\n    image: new ImageType(),\n    // default\n    html: new HtmlType(),\n    iframe: new IframeType(),\n    youtube: new YoutubeType()\n  };\n  const FOCUSABLE_ELEMENTS = ['a[href]:not([tabindex^=\"-\"]):not([inert])', 'area[href]:not([tabindex^=\"-\"]):not([inert])', 'input:not([disabled]):not([inert])', 'select:not([disabled]):not([inert])', 'textarea:not([disabled]):not([inert])', 'button:not([disabled]):not([inert])', 'iframe:not([tabindex^=\"-\"]):not([inert])', 'audio:not([tabindex^=\"-\"]):not([inert])', 'video:not([tabindex^=\"-\"]):not([inert])', '[contenteditable]:not([tabindex^=\"-\"]):not([inert])', '[tabindex]:not([tabindex^=\"-\"]):not([inert])'];\n  let userSettings = {};\n  const WAITING_ELS = [];\n  const GROUP_ATTS = {\n    gallery: [],\n    slider: null,\n    sliderElements: [],\n    elementsLength: 0,\n    currentIndex: 0,\n    x: 0\n  };\n  let lightbox = null;\n  let prevButton = null;\n  let nextButton = null;\n  let closeButton = null;\n  let counter = null;\n  let lastFocus = null;\n  let offset = null;\n  let isYouTubeDependencyLoaded = false;\n  let groups = {};\n  let activeGroup = null;\n  let pointerDownCache = [];\n  let lastTapTime = 0;\n  let liveRegion = null;\n  const MIN_SCALE = 1;\n  const MAX_SCALE = 4;\n  const DOUBLE_TAP_TIME = 500; // milliseconds\n  const SCALE_SENSITIVITY = 10;\n  const TRANSFORM = {\n    element: null,\n    originX: 0,\n    originY: 0,\n    translateX: 0,\n    translateY: 0,\n    scale: MIN_SCALE\n  };\n  const DRAG = {\n    startX: 0,\n    startY: 0,\n    x: 0,\n    y: 0,\n    distance: 0\n  };\n\n  /**\n   * Merge default options with user options\n   *\n   * @param {Object} userOptions - Optional user options\n   * @returns {Object} - Custom options\n   */\n  const mergeOptions = userOptions => {\n    // Default options\n    const OPTIONS = {\n      selector: '.lightbox',\n      captions: true,\n      captionsSelector: 'img',\n      captionAttribute: 'alt',\n      captionText: null,\n      captionHTML: false,\n      captionToggle: true,\n      captionToggleLabel: ['Hide caption', 'Show caption'],\n      nav: 'auto',\n      navText: ['<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"15 6 9 12 15 18\" /></svg>', '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"9 6 15 12 9 18\" /></svg>'],\n      navLabel: ['Previous image', 'Next image'],\n      announcementLabel: ['Slide', 'of'],\n      close: true,\n      closeText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" /></svg>',\n      closeLabel: 'Close lightbox',\n      dialogTitle: 'Lightbox',\n      loadingIndicatorLabel: 'Image loading',\n      counter: true,\n      keyboard: true,\n      zoom: false,\n      zoomText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"16 4 20 4 20 8\" /><line x1=\"14\" y1=\"10\" x2=\"20\" y2=\"4\" /><polyline points=\"8 20 4 20 4 16\" /><line x1=\"4\" y1=\"20\" x2=\"10\" y2=\"14\" /><polyline points=\"16 20 20 20 20 16\" /><line x1=\"14\" y1=\"14\" x2=\"20\" y2=\"20\" /><polyline points=\"8 4 4 4 4 8\" /><line x1=\"4\" y1=\"4\" x2=\"10\" y2=\"10\" /></svg>',\n      docClose: true,\n      swipeClose: true,\n      hideScrollbar: true,\n      draggable: true,\n      threshold: 100,\n      theme: 'tobii--theme-default'\n    };\n    return {\n      ...OPTIONS,\n      ...userOptions\n    };\n  };\n\n  /**\n   * Init\n   *\n   */\n  const init = userOptions => {\n    // Merge user options into defaults\n    userSettings = mergeOptions(userOptions);\n\n    // Create the lightbox container\n    lightbox = document.createElement('div');\n    lightbox.setAttribute('role', 'dialog');\n    lightbox.setAttribute('aria-hidden', 'true');\n    lightbox.setAttribute('aria-modal', 'true');\n    lightbox.setAttribute('aria-label', userSettings.dialogTitle);\n    lightbox.classList.add('tobii');\n\n    // Add theme class\n    lightbox.classList.add(userSettings.theme);\n\n    // Create the previous button\n    prevButton = document.createElement('button');\n    prevButton.className = 'tobii__btn tobii__btn--previous';\n    prevButton.setAttribute('type', 'button');\n    prevButton.setAttribute('aria-label', userSettings.navLabel[0]);\n    prevButton.innerHTML = userSettings.navText[0];\n    lightbox.appendChild(prevButton);\n\n    // Create the next button\n    nextButton = document.createElement('button');\n    nextButton.className = 'tobii__btn tobii__btn--next';\n    nextButton.setAttribute('type', 'button');\n    nextButton.setAttribute('aria-label', userSettings.navLabel[1]);\n    nextButton.innerHTML = userSettings.navText[1];\n    lightbox.appendChild(nextButton);\n\n    // Create the close button\n    closeButton = document.createElement('button');\n    closeButton.className = 'tobii__btn tobii__btn--close';\n    closeButton.setAttribute('type', 'button');\n    closeButton.setAttribute('aria-label', userSettings.closeLabel);\n    closeButton.innerHTML = userSettings.closeText;\n    lightbox.appendChild(closeButton);\n\n    // Create the counter\n    counter = document.createElement('div');\n    counter.className = 'tobii__counter';\n    lightbox.appendChild(counter);\n\n    // Create the live region\n    liveRegion = document.createElement('div');\n    liveRegion.className = 'tobii__sr';\n    liveRegion.setAttribute('aria-live', 'polite');\n    liveRegion.setAttribute('aria-atomic', 'true');\n    lightbox.appendChild(liveRegion);\n\n    // Append to body\n    document.body.appendChild(lightbox);\n\n    // Init only\n    if (!userSettings.selector) return;\n\n    // Get a list of all elements within the document\n    const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector);\n    if (!LIGHTBOX_TRIGGER_ELS) {\n      throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`);\n    }\n    LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el));\n  };\n\n  /**\n   * Check dependencies\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const checkDependencies = el => {\n    // Check if there is a YouTube video and if the YouTube iframe-API is ready\n    if (document.querySelector('[data-type=\"youtube\"]') !== null && !isYouTubeDependencyLoaded) {\n      if (document.getElementById('iframe_api') === null) {\n        const TAG = document.createElement('script');\n        const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0];\n        TAG.id = 'iframe_api';\n        TAG.src = 'https://www.youtube.com/iframe_api';\n        FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG);\n      }\n      if (WAITING_ELS.indexOf(el) === -1) {\n        WAITING_ELS.push(el);\n      }\n      window.onYouTubePlayerAPIReady = () => {\n        WAITING_ELS.forEach(waitingEl => {\n          add(waitingEl);\n        });\n        isYouTubeDependencyLoaded = true;\n      };\n    } else {\n      add(el);\n    }\n  };\n\n  /**\n   * Get group name from element\n   *\n   * @param {HTMLElement} el\n   * @return {string}\n   */\n  const getGroupName = el => {\n    return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default';\n  };\n\n  /**\n   * Copy an object. (The secure way)\n   *\n   * @param {object} object\n   * @return {object}\n   */\n  const copyObject = object => {\n    return JSON.parse(JSON.stringify(object));\n  };\n\n  /**\n   * Add element\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const add = el => {\n    const newGroup = getGroupName(el);\n    if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {\n      groups[newGroup] = copyObject(GROUP_ATTS);\n\n      // Create slider\n      groups[newGroup].slider = document.createElement('div');\n      groups[newGroup].slider.className = 'tobii__slider';\n\n      // Hide slider\n      groups[newGroup].slider.setAttribute('aria-hidden', 'true');\n      lightbox.appendChild(groups[newGroup].slider);\n    }\n\n    // Check if element already exists\n    if (groups[newGroup].gallery.indexOf(el) === -1) {\n      groups[newGroup].gallery.push(el);\n      groups[newGroup].elementsLength++;\n\n      // Set zoom icon if necessary\n      if (userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false' || el.getAttribute('data-zoom') === 'true') {\n        const TOBII_ZOOM = document.createElement('div');\n        TOBII_ZOOM.className = 'tobii-zoom__icon';\n        TOBII_ZOOM.innerHTML = userSettings.zoomText;\n        el.classList.add('tobii-zoom');\n        el.appendChild(TOBII_ZOOM);\n      }\n\n      // Bind click event handler\n      el.addEventListener('click', triggerTobii);\n\n      // Create slide\n      const SLIDE_ELEMENT = document.createElement('div');\n      const SLIDE_ELEMENT_CONTENT = document.createElement('div');\n      SLIDE_ELEMENT.className = 'tobii__slide';\n      SLIDE_ELEMENT.style.position = 'absolute';\n      SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`;\n\n      // Hide slide\n      SLIDE_ELEMENT.setAttribute('aria-hidden', 'true');\n\n      // Create type elements\n      const model = getModel(el);\n      model.init(el, SLIDE_ELEMENT_CONTENT, userSettings);\n\n      // Add slide content container to slide element\n      SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT);\n\n      // Add slide element to slider\n      groups[newGroup].slider.appendChild(SLIDE_ELEMENT);\n      groups[newGroup].sliderElements.push(SLIDE_ELEMENT);\n      ++groups[newGroup].x;\n      if (isOpen() && newGroup === activeGroup) {\n        updateConfig();\n        updateLightbox();\n      }\n    } else {\n      throw new Error('Ups, element already added.');\n    }\n  };\n\n  /**\n   * Remove element\n   *\n   * @param {HTMLElement} el - Element to remove\n   */\n  const remove = el => {\n    const GROUP_NAME = getGroupName(el);\n\n    // Check if element exists\n    const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el);\n    if (galleryIndex === -1) {\n      throw new Error(`Ups, I can't find a slide for the element ${el}.`);\n    }\n    const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex];\n\n    // If the element to be removed is the currently visible slide\n    if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) {\n      if (groups[GROUP_NAME].elementsLength === 1) {\n        close();\n        throw new Error('Ups, I\\'ve closed. There are no slides more to show.');\n      } else {\n        // Navigate away before removal\n        if (groups[GROUP_NAME].currentIndex === 0) {\n          next();\n        } else {\n          previous();\n        }\n        updateConfig();\n        updateLightbox();\n      }\n    }\n    groups[GROUP_NAME].gallery.splice(galleryIndex, 1);\n    groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1);\n    groups[GROUP_NAME].elementsLength--;\n    --groups[GROUP_NAME].x;\n\n    // Remove zoom icon if necessary\n    if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) {\n      const ZOOM_ICON = el.querySelector('.tobii-zoom__icon');\n      ZOOM_ICON.parentNode.classList.remove('tobii-zoom');\n      ZOOM_ICON.parentNode.removeChild(ZOOM_ICON);\n    }\n\n    // Unbind click event handler\n    el.removeEventListener('click', triggerTobii);\n\n    // Remove slide\n    SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT);\n  };\n  const getModel = el => {\n    const type = el.getAttribute('data-type');\n    if (SUPPORTED_ELEMENTS[type] !== undefined) {\n      return SUPPORTED_ELEMENTS[type];\n    } else {\n      // unknown - use default\n      if (el.hasAttribute('data-type')) {\n        console.log('Unknown lightbox element type: ' + type);\n      }\n      return SUPPORTED_ELEMENTS.image;\n    }\n  };\n\n  /**\n   * Open Tobii\n   *\n   * @param {number} index - Index to load\n   */\n  const open = (index = 0) => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m aleady open.');\n    }\n    if (index === -1 || index >= groups[activeGroup].elementsLength) {\n      throw new Error(`Ups, I can't find slide ${index}.`);\n    }\n    document.documentElement.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open');\n    document.body.classList.add('tobii-is-open-' + activeGroup);\n    updateConfig();\n\n    // Hide close if necessary\n    if (!userSettings.close) {\n      closeButton.disabled = false;\n      closeButton.setAttribute('aria-hidden', 'true');\n    }\n\n    // Save user’s focus\n    lastFocus = document.activeElement;\n\n    // Use `history.pushState()` to make sure the \"Back\" button behavior\n    // that aligns with the user's expectations\n    const stateObj = {\n      tobii: 'close'\n    };\n    const url = window.location.href;\n    window.history.pushState(stateObj, 'Image', url);\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    bindEvents();\n\n    // Load slide\n    load(groups[activeGroup].currentIndex);\n\n    // Show slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'false');\n\n    // Show lightbox\n    lightbox.setAttribute('aria-hidden', 'false');\n    updateLightbox();\n\n    // Preload previous and next slide\n    preload(groups[activeGroup].currentIndex + 1);\n    preload(groups[activeGroup].currentIndex - 1);\n    groups[activeGroup].slider.classList.add('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const openEvent = new window.CustomEvent('open', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(openEvent);\n  };\n\n  /**\n   * Close Tobii\n   *\n   */\n  const close = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m already closed.');\n    }\n    document.documentElement.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open');\n    document.body.classList.remove('tobii-is-open-' + activeGroup);\n    unbindEvents();\n\n    // Remove entry in browser history\n    if (window.history.state !== null) {\n      if (window.history.state.tobii === 'close') {\n        window.history.back();\n      }\n    }\n\n    // Reenable the user’s focus\n    lastFocus.focus();\n\n    // Don't forget to cleanup our current element\n    leave(groups[activeGroup].currentIndex);\n    cleanup(groups[activeGroup].currentIndex);\n\n    // Hide lightbox\n    lightbox.setAttribute('aria-hidden', 'true');\n\n    // Hide slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'true');\n\n    // Reset current index\n    groups[activeGroup].currentIndex = 0;\n\n    // Remove the hack to prevent animation during opening\n    groups[activeGroup].slider.classList.remove('tobii__slider--animate');\n\n    // Create and dispatch a new event\n    const closeEvent = new window.CustomEvent('close', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(closeEvent);\n  };\n\n  /**\n   * Preload slide\n   *\n   * @param {number} index - Index to preload\n   */\n  const preload = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onPreload(CONTAINER);\n  };\n\n  /**\n   * Load slide\n   * Will be called when opening the lightbox or moving index\n   *\n   * @param {number} index - Index to load\n   */\n  const load = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLoad(CONTAINER, activeGroup);\n  };\n\n  /**\n   * Select a slide\n   *\n   * @param {number} index - Index to select\n   */\n  const select = index => {\n    const currIndex = groups[activeGroup].currentIndex;\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (isOpen()) {\n      if (!index && index !== 0) {\n        throw new Error('Ups, no slide specified.');\n      }\n      if (index === groups[activeGroup].currentIndex) {\n        throw new Error(`Ups, slide ${index} is already selected.`);\n      }\n      if (index === -1 || index >= groups[activeGroup].elementsLength) {\n        throw new Error(`Ups, I can't find slide ${index}.`);\n      }\n    }\n\n    // Set current index\n    groups[activeGroup].currentIndex = index;\n    leave(currIndex);\n    load(index);\n    if (index < currIndex) {\n      updateLightbox('left');\n      cleanup(currIndex);\n      preload(index - 1);\n    }\n    if (index > currIndex) {\n      updateLightbox('right');\n      cleanup(currIndex);\n      preload(index + 1);\n    }\n  };\n\n  /**\n   * Select the previous slide\n   *\n   */\n  const previous = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex > 0) {\n      leave(groups[activeGroup].currentIndex);\n      load(--groups[activeGroup].currentIndex);\n      updateLightbox('left');\n      cleanup(groups[activeGroup].currentIndex + 1);\n      preload(groups[activeGroup].currentIndex - 1);\n    }\n\n    // Create and dispatch a new event\n    const previousEvent = new window.CustomEvent('previous', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(previousEvent);\n  };\n\n  /**\n   * Select the next slide\n   *\n   */\n  const next = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.');\n    }\n    if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) {\n      leave(groups[activeGroup].currentIndex);\n      load(++groups[activeGroup].currentIndex);\n      updateLightbox('right');\n      cleanup(groups[activeGroup].currentIndex - 1);\n      preload(groups[activeGroup].currentIndex + 1);\n    }\n\n    // Create and dispatch a new event\n    const nextEvent = new window.CustomEvent('next', {\n      detail: {\n        group: activeGroup\n      }\n    });\n    lightbox.dispatchEvent(nextEvent);\n  };\n\n  /**\n   * Select a group\n   *\n   * @param {string} name - Name of the group to select\n   */\n  const selectGroup = name => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m open.');\n    }\n    if (!name) {\n      throw new Error('Ups, no group specified.');\n    }\n    if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {\n      throw new Error(`Ups, I don't have a group called \"${name}\".`);\n    }\n    activeGroup = name;\n  };\n\n  /**\n   * Leave slide\n   * Will be called before moving index\n   *\n   * @param {number} index - Index to leave\n   */\n  const leave = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onLeave(CONTAINER);\n  };\n\n  /**\n   * Cleanup slide\n   * Will be called after moving index\n   *\n   * @param {number} index - Index to cleanup\n   */\n  const cleanup = index => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return;\n    }\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n    const model = getModel(CONTAINER);\n    model.onCleanup(CONTAINER);\n    DRAG.startX = 0;\n    DRAG.startY = 0;\n    DRAG.x = 0;\n    DRAG.y = 0;\n    DRAG.distance = 0;\n    lastTapTime = 0;\n    if (isZoomed()) resetZoom();\n    TRANSFORM.element = null;\n  };\n\n  /**\n   * Update offset\n   *\n   */\n  const updateOffset = () => {\n    offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth;\n    groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`;\n  };\n\n  /**\n   * Update counter\n   *\n   */\n  const updateCounter = () => {\n    counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`;\n  };\n\n  /**\n   * Update focus\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateFocus = dir => {\n    const group = groups[activeGroup];\n    const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto';\n    const hasMultipleSlides = group.elementsLength > 1;\n    if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) {\n      setButtonState(prevButton, true, true);\n      setButtonState(nextButton, true, true);\n      if (group.currentIndex === 0) {\n        setButtonState(nextButton, false, false);\n        nextButton.focus();\n      } else if (group.currentIndex === group.elementsLength - 1) {\n        setButtonState(prevButton, false, false);\n        prevButton.focus();\n      } else {\n        setButtonState(prevButton, false, false);\n        setButtonState(nextButton, false, false);\n        if (dir === 'left') {\n          prevButton.focus();\n        } else {\n          nextButton.focus();\n        }\n      }\n    } else if (userSettings.close) {\n      closeButton.focus();\n    }\n  };\n\n  /**\n   * Resize event\n   *\n   */\n  const resizeHandler = () => {\n    updateOffset();\n  };\n\n  /**\n   * Click event handler to trigger Tobii\n   *\n   */\n  const triggerTobii = event => {\n    event.preventDefault();\n    activeGroup = getGroupName(event.currentTarget);\n    open(groups[activeGroup].gallery.indexOf(event.currentTarget));\n  };\n\n  /**\n   * Click event handler\n   *\n   */\n  const clickHandler = event => {\n    if (event.target === prevButton) {\n      previous();\n    } else if (event.target === nextButton) {\n      next();\n    } else if (event.target === closeButton || event.target.classList.contains('tobii__slide') || event.target.classList.contains('tobii') && userSettings.docClose) {\n      close();\n    }\n    event.stopPropagation();\n  };\n\n  /**\n   * Set the hidden/disabled state of a button\n   *\n   */\n  const setButtonState = (button, hidden, disabled) => {\n    button.setAttribute('aria-hidden', hidden ? 'true' : 'false');\n    button.disabled = disabled;\n  };\n\n  /**\n   * Keydown event handler\n   *\n   */\n  const keydownHandler = event => {\n    if (event.code === 'Tab') {\n      const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', ')));\n      if (FOCUSABLE.length === 0) return;\n      const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement);\n      if (event.shiftKey && FOCUSED_INDEX === 0) {\n        // SHIFT+Tab on first → jump to last\n        FOCUSABLE[FOCUSABLE.length - 1].focus();\n        event.preventDefault();\n      } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) {\n        // Tab on last → jump to first\n        FOCUSABLE[0].focus();\n        event.preventDefault();\n      }\n    } else if (event.code === 'Escape') {\n      // `ESC` Key: Close Tobii\n      event.preventDefault();\n      close();\n    } else if (event.code === 'ArrowLeft') {\n      // `PREV` Key: Show the previous slide\n      event.preventDefault();\n      previous();\n    } else if (event.code === 'ArrowRight') {\n      // `NEXT` Key: Show the next slide\n      event.preventDefault();\n      next();\n    }\n  };\n\n  /**\n   * Contextmenu event handler\n   * This is a fix for chromium based browser on mac.\n   * The 'contextmenu' terminates a mouse event sequence.\n   * https://bugs.chromium.org/p/chromium/issues/detail?id=506801\n   *\n   */\n  const contextmenuHandler = () => {\n    pointerDownCache = [];\n    updateOffset();\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n  };\n\n  /**\n   * Pointerdown event handler\n   *\n   */\n  const pointerdownHandler = event => {\n    // Prevent dragging / swiping on textareas, inputs and selects\n    if (isIgnoreElement(event.target)) {\n      return;\n    }\n    event.preventDefault();\n    event.stopPropagation();\n    DRAG.startX = DRAG.x = event.clientX;\n    DRAG.startY = DRAG.y = event.clientY;\n    DRAG.distance = 0;\n\n    // This event is cached to support 2-finger gestures\n    pointerDownCache.push(event);\n    if (pointerDownCache.length === 2) {\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      DRAG.startX = DRAG.x = x;\n      DRAG.startY = DRAG.y = y;\n      DRAG.distance = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / TRANSFORM.scale;\n    }\n  };\n\n  /**\n   * Pointermove event handler\n   *\n   */\n  const pointermoveHandler = event => {\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Find this event in the cache and update its record with this event\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache[index] = event;\n    if (pointerDownCache.length === 2) {\n      // 2-pointer horizontal pinch/zoom gesture\n      const {\n        x,\n        y\n      } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n      const scale = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / DRAG.distance;\n      zoomPan(event.target, clamp(scale, MIN_SCALE, MAX_SCALE), x, y, x - DRAG.x, y - DRAG.y);\n      DRAG.x = x;\n      DRAG.y = y;\n      return;\n    }\n    if (isZoomed()) {\n      const deltaX = event.clientX - DRAG.x;\n      const deltaY = event.clientY - DRAG.y;\n      pan(deltaX, deltaY);\n    }\n    DRAG.x = event.clientX;\n    DRAG.y = event.clientY;\n    if (!isZoomed()) {\n      // Drag animation\n      const deltaX = DRAG.startX - DRAG.x;\n      const deltaY = DRAG.startY - DRAG.y;\n\n      // Skip animation if drag distance is too low\n      if (distance(deltaX, deltaY) < 10) return;\n      if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) {\n        // Horizontal swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset - Math.round(deltaX)}px, 0)`;\n      } else if (userSettings.swipeClose) {\n        // Vertical swipe\n        groups[activeGroup].slider.style.transform = `translate(${offset}px, -${Math.round(deltaY)}px)`;\n      }\n    }\n  };\n\n  /**\n   * Pointerup event handler\n   *\n   */\n  const pointerupHandler = event => {\n    // Intercept regular click handler\n    if (!pointerDownCache.length) return;\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n    // Remove this event from the target's cache\n    const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n    pointerDownCache.splice(index, 1);\n    const x = event.clientX;\n    const y = event.clientY;\n    const deltaX = DRAG.startX - x;\n    const deltaY = DRAG.startY - y;\n    const distanceX = Math.abs(deltaX);\n    const distanceY = Math.abs(deltaY);\n    if (distanceX > 8 || distanceY > 8) {\n      if (!isZoomed()) {\n        // Evaluate drag\n        if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) {\n          previous();\n        } else if (deltaX > 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) {\n          next();\n        } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) {\n          close();\n        } else {\n          updateOffset();\n        }\n      }\n    } else {\n      // Evaluate tap\n      const now = Date.now();\n      const tapLength = now - lastTapTime;\n      if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) {\n        // Double click\n        event.preventDefault();\n        lastTapTime = 0;\n        if (isZoomed()) {\n          resetZoom();\n        } else {\n          zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0);\n        }\n      } else {\n        lastTapTime = now;\n        if (isTouchDevice()) {\n          // Delayed tap on mobile\n          window.setTimeout(() => {\n            const {\n              left,\n              top,\n              bottom,\n              right,\n              width\n            } = event.target.getBoundingClientRect();\n            if (y < top || y > bottom || !lastTapTime) return;\n            if (x > left && x < left + width / 2) {\n              previous();\n            } else if (x < right && x > right - width / 2) {\n              next();\n            }\n          }, DOUBLE_TAP_TIME);\n        }\n      }\n    }\n  };\n\n  /**\n   * Wheel event handler\n   *\n   */\n  const wheelHandler = event => {\n    const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1;\n    if (!isZoomed() && !deltaScale) return;\n    event.preventDefault();\n    const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale);\n    zoomPan(event.target, clamp(newScale, MIN_SCALE, MAX_SCALE), event.clientX, event.clientY, 0, 0);\n  };\n  const clampedTranslate = (axis, translate) => {\n    // Whole clamping functionality heavily inspired\n    // by https://github.com/Neophen/pinch-zoom-pan\n    const {\n      element,\n      scale,\n      originX,\n      originY\n    } = TRANSFORM;\n    const axisIsX = axis === 'x';\n    const origin = axisIsX ? originX : originY;\n    const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight';\n    const containerSize = element.parentNode[axisKey];\n    const imageSize = element[axisKey];\n    const bounds = element.getBoundingClientRect();\n    const imageScaledSize = axisIsX ? bounds.width : bounds.height;\n    const defaultOrigin = imageSize / 2;\n    const originOffset = (origin - defaultOrigin) * (scale - 1);\n    const range = Math.max(0, Math.round(imageScaledSize) - containerSize);\n    const max = Math.round(range / 2);\n    const min = 0 - max;\n    return clamp(translate, min + originOffset, max + originOffset);\n  };\n  const clamp = (value, min, max) => Math.max(Math.min(value, max), min);\n  const isZoomed = () => TRANSFORM.scale !== MIN_SCALE;\n  const pan = (deltaX, deltaY) => {\n    if (deltaX !== 0) {\n      TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX);\n    }\n    if (deltaY !== 0) {\n      TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY);\n    }\n    const {\n      element,\n      originX,\n      originY,\n      translateX,\n      translateY,\n      scale\n    } = TRANSFORM;\n    element.style.transformOrigin = `${originX}px ${originY}px`;\n    element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;\n  };\n  const zoomPan = (el, newScale, x, y, deltaX, deltaY) => {\n    if (el.tagName !== 'IMG') return;\n    const {\n      left,\n      top\n    } = el.getBoundingClientRect();\n    const originX = x - left;\n    const originY = y - top;\n    const newOriginX = originX / TRANSFORM.scale;\n    const newOriginY = originY / TRANSFORM.scale;\n    TRANSFORM.element = el;\n    TRANSFORM.originX = newOriginX;\n    TRANSFORM.originY = newOriginY;\n    TRANSFORM.scale = newScale;\n    pan(deltaX, deltaY);\n  };\n  const distance = (dx, dy) => Math.hypot(dx, dy);\n  const midPoint = (x1, y1, x2, y2) => ({\n    x: (x1 + x2) / 2,\n    y: (y1 + y2) / 2\n  });\n  const resetZoom = () => {\n    TRANSFORM.scale = MIN_SCALE;\n    TRANSFORM.originX = 0;\n    TRANSFORM.originY = 0;\n    TRANSFORM.translateX = 0;\n    TRANSFORM.translateY = 0;\n    pan(0, 0);\n  };\n\n  /**\n   * Bind events\n   *\n   */\n  const bindEvents = () => {\n    if (userSettings.keyboard) {\n      window.addEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.addEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.addEventListener('popstate', close);\n\n    // Click event\n    on('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      on('pointerdown', pointerdownHandler);\n      on('pointermove', pointermoveHandler);\n      on('pointerup', pointerupHandler);\n      on('pointercancel', contextmenuHandler);\n      on('pointerout', contextmenuHandler);\n      on('pointerleave', contextmenuHandler);\n      on('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    on('wheel', wheelHandler);\n  };\n\n  /**\n   * Unbind events\n   *\n   */\n  const unbindEvents = () => {\n    if (userSettings.keyboard) {\n      window.removeEventListener('keydown', keydownHandler);\n    }\n\n    // Resize event\n    window.removeEventListener('resize', resizeHandler);\n\n    // Popstate event\n    window.removeEventListener('popstate', close);\n\n    // Click event\n    off('click', clickHandler);\n    if (userSettings.draggable) {\n      // Pointer events\n      off('pointerdown', pointerdownHandler);\n      off('pointermove', pointermoveHandler);\n      off('pointerup', pointerupHandler);\n      off('pointercancel', contextmenuHandler);\n      off('pointerout', contextmenuHandler);\n      off('pointerleave', contextmenuHandler);\n      off('contextmenu', contextmenuHandler);\n    }\n\n    // Wheel event\n    off('wheel', wheelHandler);\n  };\n\n  /**\n   * Update userSettings\n   *\n   */\n  const updateConfig = () => {\n    const group = groups[activeGroup];\n    const slider = group.slider;\n    if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) {\n      slider.classList.add('tobii__slider--is-draggable');\n    }\n    const hideButtons = !userSettings.nav || group.elementsLength === 1 || userSettings.nav === 'auto' && isTouchDevice();\n    setButtonState(prevButton, hideButtons, hideButtons);\n    setButtonState(nextButton, hideButtons, hideButtons);\n    const hideCounter = !userSettings.counter || group.elementsLength === 1;\n    counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false');\n  };\n\n  /**\n   * Update live region\n   *\n   */\n  const updateAnnouncement = () => {\n    const group = groups[activeGroup];\n    const currIndex = group.currentIndex;\n    const total = group.elementsLength;\n    const trigger = group.gallery[currIndex];\n    const [slide, of] = userSettings.announcementLabel;\n    let extra;\n    if (trigger.hasAttribute('data-label')) {\n      extra = trigger.getAttribute('data-label');\n    } else {\n      const img = trigger.querySelector('img');\n      extra = img?.alt || '';\n    }\n    const base = `${slide} ${currIndex + 1} ${of} ${total}`;\n\n    // Announce reliably\n    liveRegion.textContent = '';\n    window.setTimeout(() => {\n      liveRegion.textContent = extra ? `${base}. ${extra}` : base;\n    }, 10);\n  };\n\n  /**\n   * Update lightbox\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateLightbox = (dir = null) => {\n    updateOffset();\n    updateCounter();\n    updateAnnouncement();\n    updateFocus(dir);\n  };\n\n  /**\n   * Reset Tobii\n   *\n   */\n  const reset = () => {\n    if (isOpen()) close();\n    Object.values(groups).forEach(group => group.gallery.forEach(remove));\n    groups = {};\n    activeGroup = null;\n    Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset());\n  };\n\n  /**\n   * Destroy Tobii\n   *\n   */\n  const destroy = () => {\n    reset();\n    lightbox.parentNode.removeChild(lightbox);\n  };\n\n  /**\n   * Check if Tobii is open\n   *\n   */\n  const isOpen = () => {\n    return lightbox.getAttribute('aria-hidden') === 'false';\n  };\n\n  /**\n   * Detect whether device is touch capable\n   *\n   */\n  const isTouchDevice = () => {\n    return 'ontouchstart' in window;\n  };\n\n  /**\n   * Checks whether element's tagName is part of array\n   *\n   */\n  const isIgnoreElement = el => {\n    return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton || el === nextButton || el === closeButton;\n  };\n\n  /**\n   * Return current index\n   *\n   */\n  const slidesIndex = () => {\n    return groups[activeGroup].currentIndex;\n  };\n\n  /**\n   * Return elements length\n   *\n   */\n  const slidesCount = () => {\n    return groups[activeGroup].elementsLength;\n  };\n\n  /**\n   * Return current group\n   *\n   */\n  const currentGroup = () => {\n    return activeGroup;\n  };\n\n  /**\n   * Bind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const on = (eventName, callback) => {\n    lightbox.addEventListener(eventName, callback);\n  };\n\n  /**\n   * Unbind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const off = (eventName, callback) => {\n    lightbox.removeEventListener(eventName, callback);\n  };\n  init(userOptions);\n  return {\n    open,\n    previous,\n    next,\n    close,\n    add: checkDependencies,\n    remove,\n    reset,\n    destroy,\n    isOpen,\n    slidesIndex,\n    select,\n    slidesCount,\n    selectGroup,\n    currentGroup,\n    on,\n    off\n  };\n}\n\nexport { Tobii as default };\n"
  },
  {
    "path": "dist/tobii.umd.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = global || self, global.Tobii = factory());\n})(this, (function () {\n  class ImageType {\n    constructor() {\n      this.figcaptionId = 0;\n      this.userSettings = null;\n    }\n    init(el, container, userSettings) {\n      this.userSettings = userSettings;\n      const FIGURE = document.createElement('figure');\n      const IMAGE = document.createElement('img');\n      const THUMBNAIL = el.querySelector('img');\n      const LOADING_INDICATOR = document.createElement('div');\n\n      // Accessibility: allow setting focus programmatically on figure elements.\n      FIGURE.tabIndex = -1;\n\n      // Add role=\"group\" to figure\n      FIGURE.setAttribute('role', 'group');\n\n      // Hide figure until the image is loaded\n      FIGURE.style.opacity = '0';\n      if (THUMBNAIL) {\n        IMAGE.alt = THUMBNAIL.alt || '';\n      }\n      IMAGE.setAttribute('data-src', el.href);\n      if (el.hasAttribute('data-srcset')) {\n        IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset'));\n      }\n      if (el.hasAttribute('data-sizes')) {\n        IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes'));\n      }\n\n      // Add image to figure\n      FIGURE.appendChild(IMAGE);\n\n      // Create figcaption\n      let captionContent;\n      if (typeof this.userSettings.captionText === 'function') {\n        captionContent = this.userSettings.captionText(el);\n      } else if (this.userSettings.captionsSelector === 'self' && el.hasAttribute(this.userSettings.captionAttribute)) {\n        captionContent = el.getAttribute(this.userSettings.captionAttribute);\n      } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL && THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) {\n        captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute);\n      }\n      if (this.userSettings.captions && captionContent) {\n        const FIGCAPTION = document.createElement('figcaption');\n        FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`;\n        const SPAN = document.createElement('span');\n        if (this.userSettings.captionHTML) {\n          SPAN.innerHTML = captionContent;\n        } else {\n          SPAN.textContent = captionContent;\n        }\n        FIGCAPTION.appendChild(SPAN);\n        if (this.userSettings.captionToggle) {\n          const isMobile = window.innerWidth < 768;\n          const BUTTON = document.createElement('button');\n          BUTTON.className = 'caption-toggle';\n          BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0];\n          BUTTON.setAttribute('aria-controls', FIGCAPTION.id);\n          BUTTON.setAttribute('aria-expanded', !isMobile);\n          if (isMobile) {\n            FIGCAPTION.classList.add('caption-hidden');\n          }\n          SPAN.setAttribute('aria-hidden', isMobile);\n          const preventAndStopEvent = event => {\n            event.preventDefault();\n            event.stopPropagation();\n          };\n          BUTTON.addEventListener('pointerdown', event => preventAndStopEvent(event));\n          BUTTON.addEventListener('pointerup', event => preventAndStopEvent(event));\n          BUTTON.addEventListener('click', event => {\n            preventAndStopEvent(event);\n            const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true';\n            const buttonLabel = isExpanded ? this.userSettings.captionToggleLabel[1] : this.userSettings.captionToggleLabel[0];\n            BUTTON.textContent = BUTTON.title = buttonLabel;\n            BUTTON.setAttribute('aria-expanded', !isExpanded);\n            FIGCAPTION.classList.toggle('caption-hidden');\n            SPAN.setAttribute('aria-hidden', isExpanded);\n          });\n          FIGCAPTION.appendChild(BUTTON);\n        }\n        FIGURE.appendChild(FIGCAPTION);\n        IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id);\n\n        // Add aria-label to the figure containing the caption content\n        FIGURE.setAttribute('aria-label', SPAN.textContent);\n        ++this.figcaptionId;\n      }\n\n      // Add figure to container\n      container.appendChild(FIGURE);\n\n      // Create loading indicator\n      LOADING_INDICATOR.className = 'tobii__loader';\n      LOADING_INDICATOR.setAttribute('role', 'progressbar');\n      LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n\n      // Add loading indicator to container\n      container.appendChild(LOADING_INDICATOR);\n\n      // Register type\n      container.setAttribute('data-type', 'image');\n      container.classList.add('tobii-image');\n    }\n    onPreload(container) {\n      // Same as preload\n      this.onLoad(container);\n    }\n    onLoad(container) {\n      const IMAGE = container.querySelector('img');\n      if (!IMAGE.hasAttribute('data-src')) {\n        return;\n      }\n      const FIGURE = container.querySelector('figure');\n      const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n      const handleImageEvent = () => {\n        container.removeChild(LOADING_INDICATOR);\n        FIGURE.style.opacity = '1';\n      };\n      IMAGE.addEventListener('load', handleImageEvent);\n      IMAGE.addEventListener('error', handleImageEvent);\n      if (IMAGE.hasAttribute('data-srcset')) {\n        IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset'));\n        IMAGE.removeAttribute('data-srcset');\n      }\n      if (IMAGE.hasAttribute('data-sizes')) {\n        IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes'));\n        IMAGE.removeAttribute('data-sizes');\n      }\n      IMAGE.setAttribute('src', IMAGE.getAttribute('data-src'));\n      IMAGE.removeAttribute('data-src');\n    }\n    onLeave(container) {\n      // Nothing\n    }\n    onCleanup(container) {\n      // Nothing\n    }\n    onReset() {\n      this.figcaptionId = 0;\n    }\n  }\n\n  class IframeType {\n    constructor() {\n      this.userSettings = null;\n    }\n    init(el, container, userSettings) {\n      this.userSettings = userSettings;\n      const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n      container.setAttribute('data-HREF', HREF);\n      if (el.hasAttribute('data-allow')) {\n        container.setAttribute('data-allow', el.getAttribute('data-allow'));\n      }\n      if (el.hasAttribute('data-width')) {\n        container.setAttribute('data-width', `${el.getAttribute('data-width')}`);\n      }\n      if (el.hasAttribute('data-height')) {\n        container.setAttribute('data-height', `${el.getAttribute('data-height')}`);\n      }\n\n      // dont create empty iframes here - very slow\n\n      // Register type\n      container.setAttribute('data-type', 'iframe');\n      container.classList.add('tobii-iframe');\n    }\n    onPreload(container) {\n      // Nothing\n    }\n    onLoad(container) {\n      let IFRAME = container.querySelector('iframe');\n\n      // Create loading indicator\n      const LOADING_INDICATOR = document.createElement('div');\n      LOADING_INDICATOR.className = 'tobii__loader';\n      LOADING_INDICATOR.setAttribute('role', 'progressbar');\n      LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel);\n      container.appendChild(LOADING_INDICATOR);\n      if (IFRAME == null) {\n        // create iframe\n        IFRAME = document.createElement('iframe');\n        const HREF = container.getAttribute('data-href');\n        IFRAME.setAttribute('frameborder', '0');\n        IFRAME.setAttribute('src', HREF);\n\n        // Set allow parameters\n        let allowValue = 'fullscreen';\n        if (HREF.includes('youtube.com')) {\n          allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';\n        } else if (HREF.includes('vimeo.com')) {\n          allowValue += '; autoplay; picture-in-picture';\n        } else if (container.hasAttribute('data-allow')) {\n          allowValue = container.getAttribute('data-allow');\n        }\n        IFRAME.setAttribute('allow', allowValue);\n        if (container.hasAttribute('data-width')) {\n          IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`;\n        }\n        if (container.hasAttribute('data-height')) {\n          IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`;\n        }\n\n        // Hide until loaded\n        IFRAME.style.opacity = '0';\n\n        // Add iframe to container\n        container.appendChild(IFRAME);\n\n        // Handle load and error\n        const removeLoader = () => {\n          IFRAME.style.opacity = '1';\n          const LOADING_INDICATOR = container.querySelector('.tobii__loader');\n          if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR);\n        };\n        IFRAME.addEventListener('load', removeLoader);\n        IFRAME.addEventListener('error', removeLoader);\n      } else {\n        // was already created\n        IFRAME.setAttribute('src', container.getAttribute('data-href'));\n      }\n    }\n    onLeave(container) {\n      // Nothing\n    }\n    onCleanup(container) {\n      const IFRAME = container.querySelector('iframe');\n      IFRAME.removeAttribute('src');\n      IFRAME.style.opacity = '0';\n    }\n    onReset() {\n      // Nothing\n    }\n  }\n\n  class HtmlType {\n    constructor() {\n      this.userSettings = null;\n    }\n    init(el, container, userSettings) {\n      this.userSettings = userSettings;\n      const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href');\n      const TARGET = document.querySelector(TARGET_SELECTOR);\n      if (!TARGET) {\n        throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`);\n      }\n\n      // Add content to container\n      container.appendChild(TARGET);\n\n      // Register type\n      container.setAttribute('data-type', 'html');\n      container.classList.add('tobii-html');\n    }\n    onPreload(container) {\n      // Nothing\n    }\n    onLoad(container, group) {\n      const VIDEO = container.querySelector('video');\n      if (VIDEO) {\n        if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) {\n          // Continue where video was stopped\n          VIDEO.currentTime = VIDEO.getAttribute('data-time');\n        }\n\n        // Start playback (and loading if necessary)\n        VIDEO.play();\n      }\n      const audio = container.querySelector('audio');\n      if (audio) {\n        // Start playback (and loading if necessary)\n        audio.play();\n      }\n      container.classList.add('tobii-group-' + group);\n    }\n    onLeave(container) {\n      const VIDEO = container.querySelector('video');\n      if (VIDEO) {\n        if (!VIDEO.paused) {\n          // Stop if video is playing\n          VIDEO.pause();\n        }\n\n        // Backup currentTime (needed for revisit)\n        if (VIDEO.readyState > 0) {\n          VIDEO.setAttribute('data-time', VIDEO.currentTime);\n        }\n      }\n      const audio = container.querySelector('audio');\n      if (audio) {\n        if (!audio.paused) {\n          // Stop if is playing\n          audio.pause();\n        }\n      }\n    }\n    onCleanup(container) {\n      const VIDEO = container.querySelector('video');\n      if (VIDEO) {\n        if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) {\n          // Some data has been loaded but not the whole package.\n          // In order to save bandwidth, stop downloading as soon as possible.\n          const VIDEO_CLONE = VIDEO.cloneNode(true);\n          this._removeSources(VIDEO);\n          VIDEO.load();\n          VIDEO.parentNode.removeChild(VIDEO);\n          container.appendChild(VIDEO_CLONE);\n        }\n      }\n    }\n    onReset() {\n      // Nothing\n    }\n\n    /**\n     * Remove all `src` attributes\n     *\n     * @param {HTMLElement} el - Element to remove all `src` attributes\n     */\n    _removeSources(el) {\n      const SOURCES = el.querySelectorAll('src');\n      if (SOURCES) {\n        SOURCES.forEach(source => {\n          source.removeAttribute('src');\n        });\n      }\n    }\n  }\n\n  class YoutubeType {\n    constructor() {\n      this.playerId = 0;\n      this.PLAYER = [];\n      this.userSettings = null;\n    }\n    init(el, container, userSettings) {\n      this.userSettings = userSettings;\n      const IFRAME_PLACEHOLDER = document.createElement('div');\n\n      // Add iframePlaceholder to container\n      container.appendChild(IFRAME_PLACEHOLDER);\n      this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {\n        host: 'https://www.youtube-nocookie.com',\n        height: el.getAttribute('data-height') || '360',\n        width: el.getAttribute('data-width') || '640',\n        videoId: el.getAttribute('data-id'),\n        playerVars: {\n          controls: el.getAttribute('data-controls') || 1,\n          rel: 0,\n          playsinline: 1\n        }\n      });\n\n      // Set player ID\n      container.setAttribute('data-player', this.playerId);\n\n      // Register type\n      container.setAttribute('data-type', 'youtube');\n      container.classList.add('tobii-youtube');\n      this.playerId++;\n    }\n    onPreload(container) {\n      // Nothing\n    }\n    onLoad(container) {\n      this.PLAYER[container.getAttribute('data-player')].playVideo();\n    }\n    onLeave(container) {\n      if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n        this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n      }\n    }\n    onCleanup(container) {\n      if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n        this.PLAYER[container.getAttribute('data-player')].pauseVideo();\n      }\n    }\n    onReset() {\n      // Nothing\n    }\n  }\n\n  /**\n   * Tobii\n   *\n   * @author midzer\n   * @version 3.2.0\n   * @url https://github.com/midzer/tobii\n   *\n   * MIT License\n   */\n  function Tobii(userOptions) {\n    /**\n     * Global variables\n     *\n     */\n    const SUPPORTED_ELEMENTS = {\n      image: new ImageType(),\n      // default\n      html: new HtmlType(),\n      iframe: new IframeType(),\n      youtube: new YoutubeType()\n    };\n    const FOCUSABLE_ELEMENTS = ['a[href]:not([tabindex^=\"-\"]):not([inert])', 'area[href]:not([tabindex^=\"-\"]):not([inert])', 'input:not([disabled]):not([inert])', 'select:not([disabled]):not([inert])', 'textarea:not([disabled]):not([inert])', 'button:not([disabled]):not([inert])', 'iframe:not([tabindex^=\"-\"]):not([inert])', 'audio:not([tabindex^=\"-\"]):not([inert])', 'video:not([tabindex^=\"-\"]):not([inert])', '[contenteditable]:not([tabindex^=\"-\"]):not([inert])', '[tabindex]:not([tabindex^=\"-\"]):not([inert])'];\n    let userSettings = {};\n    const WAITING_ELS = [];\n    const GROUP_ATTS = {\n      gallery: [],\n      slider: null,\n      sliderElements: [],\n      elementsLength: 0,\n      currentIndex: 0,\n      x: 0\n    };\n    let lightbox = null;\n    let prevButton = null;\n    let nextButton = null;\n    let closeButton = null;\n    let counter = null;\n    let lastFocus = null;\n    let offset = null;\n    let isYouTubeDependencyLoaded = false;\n    let groups = {};\n    let activeGroup = null;\n    let pointerDownCache = [];\n    let lastTapTime = 0;\n    let liveRegion = null;\n    const MIN_SCALE = 1;\n    const MAX_SCALE = 4;\n    const DOUBLE_TAP_TIME = 500; // milliseconds\n    const SCALE_SENSITIVITY = 10;\n    const TRANSFORM = {\n      element: null,\n      originX: 0,\n      originY: 0,\n      translateX: 0,\n      translateY: 0,\n      scale: MIN_SCALE\n    };\n    const DRAG = {\n      startX: 0,\n      startY: 0,\n      x: 0,\n      y: 0,\n      distance: 0\n    };\n\n    /**\n     * Merge default options with user options\n     *\n     * @param {Object} userOptions - Optional user options\n     * @returns {Object} - Custom options\n     */\n    const mergeOptions = userOptions => {\n      // Default options\n      const OPTIONS = {\n        selector: '.lightbox',\n        captions: true,\n        captionsSelector: 'img',\n        captionAttribute: 'alt',\n        captionText: null,\n        captionHTML: false,\n        captionToggle: true,\n        captionToggleLabel: ['Hide caption', 'Show caption'],\n        nav: 'auto',\n        navText: ['<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"15 6 9 12 15 18\" /></svg>', '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"9 6 15 12 9 18\" /></svg>'],\n        navLabel: ['Previous image', 'Next image'],\n        announcementLabel: ['Slide', 'of'],\n        close: true,\n        closeText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" /></svg>',\n        closeLabel: 'Close lightbox',\n        dialogTitle: 'Lightbox',\n        loadingIndicatorLabel: 'Image loading',\n        counter: true,\n        keyboard: true,\n        zoom: false,\n        zoomText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"16 4 20 4 20 8\" /><line x1=\"14\" y1=\"10\" x2=\"20\" y2=\"4\" /><polyline points=\"8 20 4 20 4 16\" /><line x1=\"4\" y1=\"20\" x2=\"10\" y2=\"14\" /><polyline points=\"16 20 20 20 20 16\" /><line x1=\"14\" y1=\"14\" x2=\"20\" y2=\"20\" /><polyline points=\"8 4 4 4 4 8\" /><line x1=\"4\" y1=\"4\" x2=\"10\" y2=\"10\" /></svg>',\n        docClose: true,\n        swipeClose: true,\n        hideScrollbar: true,\n        draggable: true,\n        threshold: 100,\n        theme: 'tobii--theme-default'\n      };\n      return {\n        ...OPTIONS,\n        ...userOptions\n      };\n    };\n\n    /**\n     * Init\n     *\n     */\n    const init = userOptions => {\n      // Merge user options into defaults\n      userSettings = mergeOptions(userOptions);\n\n      // Create the lightbox container\n      lightbox = document.createElement('div');\n      lightbox.setAttribute('role', 'dialog');\n      lightbox.setAttribute('aria-hidden', 'true');\n      lightbox.setAttribute('aria-modal', 'true');\n      lightbox.setAttribute('aria-label', userSettings.dialogTitle);\n      lightbox.classList.add('tobii');\n\n      // Add theme class\n      lightbox.classList.add(userSettings.theme);\n\n      // Create the previous button\n      prevButton = document.createElement('button');\n      prevButton.className = 'tobii__btn tobii__btn--previous';\n      prevButton.setAttribute('type', 'button');\n      prevButton.setAttribute('aria-label', userSettings.navLabel[0]);\n      prevButton.innerHTML = userSettings.navText[0];\n      lightbox.appendChild(prevButton);\n\n      // Create the next button\n      nextButton = document.createElement('button');\n      nextButton.className = 'tobii__btn tobii__btn--next';\n      nextButton.setAttribute('type', 'button');\n      nextButton.setAttribute('aria-label', userSettings.navLabel[1]);\n      nextButton.innerHTML = userSettings.navText[1];\n      lightbox.appendChild(nextButton);\n\n      // Create the close button\n      closeButton = document.createElement('button');\n      closeButton.className = 'tobii__btn tobii__btn--close';\n      closeButton.setAttribute('type', 'button');\n      closeButton.setAttribute('aria-label', userSettings.closeLabel);\n      closeButton.innerHTML = userSettings.closeText;\n      lightbox.appendChild(closeButton);\n\n      // Create the counter\n      counter = document.createElement('div');\n      counter.className = 'tobii__counter';\n      lightbox.appendChild(counter);\n\n      // Create the live region\n      liveRegion = document.createElement('div');\n      liveRegion.className = 'tobii__sr';\n      liveRegion.setAttribute('aria-live', 'polite');\n      liveRegion.setAttribute('aria-atomic', 'true');\n      lightbox.appendChild(liveRegion);\n\n      // Append to body\n      document.body.appendChild(lightbox);\n\n      // Init only\n      if (!userSettings.selector) return;\n\n      // Get a list of all elements within the document\n      const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector);\n      if (!LIGHTBOX_TRIGGER_ELS) {\n        throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`);\n      }\n      LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el));\n    };\n\n    /**\n     * Check dependencies\n     *\n     * @param {HTMLElement} el - Element to add\n     */\n    const checkDependencies = el => {\n      // Check if there is a YouTube video and if the YouTube iframe-API is ready\n      if (document.querySelector('[data-type=\"youtube\"]') !== null && !isYouTubeDependencyLoaded) {\n        if (document.getElementById('iframe_api') === null) {\n          const TAG = document.createElement('script');\n          const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0];\n          TAG.id = 'iframe_api';\n          TAG.src = 'https://www.youtube.com/iframe_api';\n          FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG);\n        }\n        if (WAITING_ELS.indexOf(el) === -1) {\n          WAITING_ELS.push(el);\n        }\n        window.onYouTubePlayerAPIReady = () => {\n          WAITING_ELS.forEach(waitingEl => {\n            add(waitingEl);\n          });\n          isYouTubeDependencyLoaded = true;\n        };\n      } else {\n        add(el);\n      }\n    };\n\n    /**\n     * Get group name from element\n     *\n     * @param {HTMLElement} el\n     * @return {string}\n     */\n    const getGroupName = el => {\n      return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default';\n    };\n\n    /**\n     * Copy an object. (The secure way)\n     *\n     * @param {object} object\n     * @return {object}\n     */\n    const copyObject = object => {\n      return JSON.parse(JSON.stringify(object));\n    };\n\n    /**\n     * Add element\n     *\n     * @param {HTMLElement} el - Element to add\n     */\n    const add = el => {\n      const newGroup = getGroupName(el);\n      if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {\n        groups[newGroup] = copyObject(GROUP_ATTS);\n\n        // Create slider\n        groups[newGroup].slider = document.createElement('div');\n        groups[newGroup].slider.className = 'tobii__slider';\n\n        // Hide slider\n        groups[newGroup].slider.setAttribute('aria-hidden', 'true');\n        lightbox.appendChild(groups[newGroup].slider);\n      }\n\n      // Check if element already exists\n      if (groups[newGroup].gallery.indexOf(el) === -1) {\n        groups[newGroup].gallery.push(el);\n        groups[newGroup].elementsLength++;\n\n        // Set zoom icon if necessary\n        if (userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false' || el.getAttribute('data-zoom') === 'true') {\n          const TOBII_ZOOM = document.createElement('div');\n          TOBII_ZOOM.className = 'tobii-zoom__icon';\n          TOBII_ZOOM.innerHTML = userSettings.zoomText;\n          el.classList.add('tobii-zoom');\n          el.appendChild(TOBII_ZOOM);\n        }\n\n        // Bind click event handler\n        el.addEventListener('click', triggerTobii);\n\n        // Create slide\n        const SLIDE_ELEMENT = document.createElement('div');\n        const SLIDE_ELEMENT_CONTENT = document.createElement('div');\n        SLIDE_ELEMENT.className = 'tobii__slide';\n        SLIDE_ELEMENT.style.position = 'absolute';\n        SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`;\n\n        // Hide slide\n        SLIDE_ELEMENT.setAttribute('aria-hidden', 'true');\n\n        // Create type elements\n        const model = getModel(el);\n        model.init(el, SLIDE_ELEMENT_CONTENT, userSettings);\n\n        // Add slide content container to slide element\n        SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT);\n\n        // Add slide element to slider\n        groups[newGroup].slider.appendChild(SLIDE_ELEMENT);\n        groups[newGroup].sliderElements.push(SLIDE_ELEMENT);\n        ++groups[newGroup].x;\n        if (isOpen() && newGroup === activeGroup) {\n          updateConfig();\n          updateLightbox();\n        }\n      } else {\n        throw new Error('Ups, element already added.');\n      }\n    };\n\n    /**\n     * Remove element\n     *\n     * @param {HTMLElement} el - Element to remove\n     */\n    const remove = el => {\n      const GROUP_NAME = getGroupName(el);\n\n      // Check if element exists\n      const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el);\n      if (galleryIndex === -1) {\n        throw new Error(`Ups, I can't find a slide for the element ${el}.`);\n      }\n      const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex];\n\n      // If the element to be removed is the currently visible slide\n      if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) {\n        if (groups[GROUP_NAME].elementsLength === 1) {\n          close();\n          throw new Error('Ups, I\\'ve closed. There are no slides more to show.');\n        } else {\n          // Navigate away before removal\n          if (groups[GROUP_NAME].currentIndex === 0) {\n            next();\n          } else {\n            previous();\n          }\n          updateConfig();\n          updateLightbox();\n        }\n      }\n      groups[GROUP_NAME].gallery.splice(galleryIndex, 1);\n      groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1);\n      groups[GROUP_NAME].elementsLength--;\n      --groups[GROUP_NAME].x;\n\n      // Remove zoom icon if necessary\n      if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) {\n        const ZOOM_ICON = el.querySelector('.tobii-zoom__icon');\n        ZOOM_ICON.parentNode.classList.remove('tobii-zoom');\n        ZOOM_ICON.parentNode.removeChild(ZOOM_ICON);\n      }\n\n      // Unbind click event handler\n      el.removeEventListener('click', triggerTobii);\n\n      // Remove slide\n      SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT);\n    };\n    const getModel = el => {\n      const type = el.getAttribute('data-type');\n      if (SUPPORTED_ELEMENTS[type] !== undefined) {\n        return SUPPORTED_ELEMENTS[type];\n      } else {\n        // unknown - use default\n        if (el.hasAttribute('data-type')) {\n          console.log('Unknown lightbox element type: ' + type);\n        }\n        return SUPPORTED_ELEMENTS.image;\n      }\n    };\n\n    /**\n     * Open Tobii\n     *\n     * @param {number} index - Index to load\n     */\n    const open = (index = 0) => {\n      if (isOpen()) {\n        throw new Error('Ups, I\\'m aleady open.');\n      }\n      if (index === -1 || index >= groups[activeGroup].elementsLength) {\n        throw new Error(`Ups, I can't find slide ${index}.`);\n      }\n      document.documentElement.classList.add('tobii-is-open');\n      document.body.classList.add('tobii-is-open');\n      document.body.classList.add('tobii-is-open-' + activeGroup);\n      updateConfig();\n\n      // Hide close if necessary\n      if (!userSettings.close) {\n        closeButton.disabled = false;\n        closeButton.setAttribute('aria-hidden', 'true');\n      }\n\n      // Save user’s focus\n      lastFocus = document.activeElement;\n\n      // Use `history.pushState()` to make sure the \"Back\" button behavior\n      // that aligns with the user's expectations\n      const stateObj = {\n        tobii: 'close'\n      };\n      const url = window.location.href;\n      window.history.pushState(stateObj, 'Image', url);\n\n      // Set current index\n      groups[activeGroup].currentIndex = index;\n      bindEvents();\n\n      // Load slide\n      load(groups[activeGroup].currentIndex);\n\n      // Show slider\n      groups[activeGroup].slider.setAttribute('aria-hidden', 'false');\n\n      // Show lightbox\n      lightbox.setAttribute('aria-hidden', 'false');\n      updateLightbox();\n\n      // Preload previous and next slide\n      preload(groups[activeGroup].currentIndex + 1);\n      preload(groups[activeGroup].currentIndex - 1);\n      groups[activeGroup].slider.classList.add('tobii__slider--animate');\n\n      // Create and dispatch a new event\n      const openEvent = new window.CustomEvent('open', {\n        detail: {\n          group: activeGroup\n        }\n      });\n      lightbox.dispatchEvent(openEvent);\n    };\n\n    /**\n     * Close Tobii\n     *\n     */\n    const close = () => {\n      if (!isOpen()) {\n        throw new Error('Ups, I\\'m already closed.');\n      }\n      document.documentElement.classList.remove('tobii-is-open');\n      document.body.classList.remove('tobii-is-open');\n      document.body.classList.remove('tobii-is-open-' + activeGroup);\n      unbindEvents();\n\n      // Remove entry in browser history\n      if (window.history.state !== null) {\n        if (window.history.state.tobii === 'close') {\n          window.history.back();\n        }\n      }\n\n      // Reenable the user’s focus\n      lastFocus.focus();\n\n      // Don't forget to cleanup our current element\n      leave(groups[activeGroup].currentIndex);\n      cleanup(groups[activeGroup].currentIndex);\n\n      // Hide lightbox\n      lightbox.setAttribute('aria-hidden', 'true');\n\n      // Hide slider\n      groups[activeGroup].slider.setAttribute('aria-hidden', 'true');\n\n      // Reset current index\n      groups[activeGroup].currentIndex = 0;\n\n      // Remove the hack to prevent animation during opening\n      groups[activeGroup].slider.classList.remove('tobii__slider--animate');\n\n      // Create and dispatch a new event\n      const closeEvent = new window.CustomEvent('close', {\n        detail: {\n          group: activeGroup\n        }\n      });\n      lightbox.dispatchEvent(closeEvent);\n    };\n\n    /**\n     * Preload slide\n     *\n     * @param {number} index - Index to preload\n     */\n    const preload = index => {\n      if (groups[activeGroup].sliderElements[index] === undefined) {\n        return;\n      }\n      const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n      const model = getModel(CONTAINER);\n      model.onPreload(CONTAINER);\n    };\n\n    /**\n     * Load slide\n     * Will be called when opening the lightbox or moving index\n     *\n     * @param {number} index - Index to load\n     */\n    const load = index => {\n      if (groups[activeGroup].sliderElements[index] === undefined) {\n        return;\n      }\n      const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n      const model = getModel(CONTAINER);\n      model.onLoad(CONTAINER, activeGroup);\n    };\n\n    /**\n     * Select a slide\n     *\n     * @param {number} index - Index to select\n     */\n    const select = index => {\n      const currIndex = groups[activeGroup].currentIndex;\n      if (!isOpen()) {\n        throw new Error('Ups, I\\'m closed.');\n      }\n      if (isOpen()) {\n        if (!index && index !== 0) {\n          throw new Error('Ups, no slide specified.');\n        }\n        if (index === groups[activeGroup].currentIndex) {\n          throw new Error(`Ups, slide ${index} is already selected.`);\n        }\n        if (index === -1 || index >= groups[activeGroup].elementsLength) {\n          throw new Error(`Ups, I can't find slide ${index}.`);\n        }\n      }\n\n      // Set current index\n      groups[activeGroup].currentIndex = index;\n      leave(currIndex);\n      load(index);\n      if (index < currIndex) {\n        updateLightbox('left');\n        cleanup(currIndex);\n        preload(index - 1);\n      }\n      if (index > currIndex) {\n        updateLightbox('right');\n        cleanup(currIndex);\n        preload(index + 1);\n      }\n    };\n\n    /**\n     * Select the previous slide\n     *\n     */\n    const previous = () => {\n      if (!isOpen()) {\n        throw new Error('Ups, I\\'m closed.');\n      }\n      if (groups[activeGroup].currentIndex > 0) {\n        leave(groups[activeGroup].currentIndex);\n        load(--groups[activeGroup].currentIndex);\n        updateLightbox('left');\n        cleanup(groups[activeGroup].currentIndex + 1);\n        preload(groups[activeGroup].currentIndex - 1);\n      }\n\n      // Create and dispatch a new event\n      const previousEvent = new window.CustomEvent('previous', {\n        detail: {\n          group: activeGroup\n        }\n      });\n      lightbox.dispatchEvent(previousEvent);\n    };\n\n    /**\n     * Select the next slide\n     *\n     */\n    const next = () => {\n      if (!isOpen()) {\n        throw new Error('Ups, I\\'m closed.');\n      }\n      if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) {\n        leave(groups[activeGroup].currentIndex);\n        load(++groups[activeGroup].currentIndex);\n        updateLightbox('right');\n        cleanup(groups[activeGroup].currentIndex - 1);\n        preload(groups[activeGroup].currentIndex + 1);\n      }\n\n      // Create and dispatch a new event\n      const nextEvent = new window.CustomEvent('next', {\n        detail: {\n          group: activeGroup\n        }\n      });\n      lightbox.dispatchEvent(nextEvent);\n    };\n\n    /**\n     * Select a group\n     *\n     * @param {string} name - Name of the group to select\n     */\n    const selectGroup = name => {\n      if (isOpen()) {\n        throw new Error('Ups, I\\'m open.');\n      }\n      if (!name) {\n        throw new Error('Ups, no group specified.');\n      }\n      if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {\n        throw new Error(`Ups, I don't have a group called \"${name}\".`);\n      }\n      activeGroup = name;\n    };\n\n    /**\n     * Leave slide\n     * Will be called before moving index\n     *\n     * @param {number} index - Index to leave\n     */\n    const leave = index => {\n      if (groups[activeGroup].sliderElements[index] === undefined) {\n        return;\n      }\n      const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n      const model = getModel(CONTAINER);\n      model.onLeave(CONTAINER);\n    };\n\n    /**\n     * Cleanup slide\n     * Will be called after moving index\n     *\n     * @param {number} index - Index to cleanup\n     */\n    const cleanup = index => {\n      if (groups[activeGroup].sliderElements[index] === undefined) {\n        return;\n      }\n      const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]');\n      const model = getModel(CONTAINER);\n      model.onCleanup(CONTAINER);\n      DRAG.startX = 0;\n      DRAG.startY = 0;\n      DRAG.x = 0;\n      DRAG.y = 0;\n      DRAG.distance = 0;\n      lastTapTime = 0;\n      if (isZoomed()) resetZoom();\n      TRANSFORM.element = null;\n    };\n\n    /**\n     * Update offset\n     *\n     */\n    const updateOffset = () => {\n      offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth;\n      groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`;\n    };\n\n    /**\n     * Update counter\n     *\n     */\n    const updateCounter = () => {\n      counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`;\n    };\n\n    /**\n     * Update focus\n     *\n     * @param {string|null} dir - Current slide direction\n     */\n    const updateFocus = dir => {\n      const group = groups[activeGroup];\n      const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto';\n      const hasMultipleSlides = group.elementsLength > 1;\n      if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) {\n        setButtonState(prevButton, true, true);\n        setButtonState(nextButton, true, true);\n        if (group.currentIndex === 0) {\n          setButtonState(nextButton, false, false);\n          nextButton.focus();\n        } else if (group.currentIndex === group.elementsLength - 1) {\n          setButtonState(prevButton, false, false);\n          prevButton.focus();\n        } else {\n          setButtonState(prevButton, false, false);\n          setButtonState(nextButton, false, false);\n          if (dir === 'left') {\n            prevButton.focus();\n          } else {\n            nextButton.focus();\n          }\n        }\n      } else if (userSettings.close) {\n        closeButton.focus();\n      }\n    };\n\n    /**\n     * Resize event\n     *\n     */\n    const resizeHandler = () => {\n      updateOffset();\n    };\n\n    /**\n     * Click event handler to trigger Tobii\n     *\n     */\n    const triggerTobii = event => {\n      event.preventDefault();\n      activeGroup = getGroupName(event.currentTarget);\n      open(groups[activeGroup].gallery.indexOf(event.currentTarget));\n    };\n\n    /**\n     * Click event handler\n     *\n     */\n    const clickHandler = event => {\n      if (event.target === prevButton) {\n        previous();\n      } else if (event.target === nextButton) {\n        next();\n      } else if (event.target === closeButton || event.target.classList.contains('tobii__slide') || event.target.classList.contains('tobii') && userSettings.docClose) {\n        close();\n      }\n      event.stopPropagation();\n    };\n\n    /**\n     * Set the hidden/disabled state of a button\n     *\n     */\n    const setButtonState = (button, hidden, disabled) => {\n      button.setAttribute('aria-hidden', hidden ? 'true' : 'false');\n      button.disabled = disabled;\n    };\n\n    /**\n     * Keydown event handler\n     *\n     */\n    const keydownHandler = event => {\n      if (event.code === 'Tab') {\n        const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', ')));\n        if (FOCUSABLE.length === 0) return;\n        const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement);\n        if (event.shiftKey && FOCUSED_INDEX === 0) {\n          // SHIFT+Tab on first → jump to last\n          FOCUSABLE[FOCUSABLE.length - 1].focus();\n          event.preventDefault();\n        } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) {\n          // Tab on last → jump to first\n          FOCUSABLE[0].focus();\n          event.preventDefault();\n        }\n      } else if (event.code === 'Escape') {\n        // `ESC` Key: Close Tobii\n        event.preventDefault();\n        close();\n      } else if (event.code === 'ArrowLeft') {\n        // `PREV` Key: Show the previous slide\n        event.preventDefault();\n        previous();\n      } else if (event.code === 'ArrowRight') {\n        // `NEXT` Key: Show the next slide\n        event.preventDefault();\n        next();\n      }\n    };\n\n    /**\n     * Contextmenu event handler\n     * This is a fix for chromium based browser on mac.\n     * The 'contextmenu' terminates a mouse event sequence.\n     * https://bugs.chromium.org/p/chromium/issues/detail?id=506801\n     *\n     */\n    const contextmenuHandler = () => {\n      pointerDownCache = [];\n      updateOffset();\n      groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n    };\n\n    /**\n     * Pointerdown event handler\n     *\n     */\n    const pointerdownHandler = event => {\n      // Prevent dragging / swiping on textareas, inputs and selects\n      if (isIgnoreElement(event.target)) {\n        return;\n      }\n      event.preventDefault();\n      event.stopPropagation();\n      DRAG.startX = DRAG.x = event.clientX;\n      DRAG.startY = DRAG.y = event.clientY;\n      DRAG.distance = 0;\n\n      // This event is cached to support 2-finger gestures\n      pointerDownCache.push(event);\n      if (pointerDownCache.length === 2) {\n        const {\n          x,\n          y\n        } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n        DRAG.startX = DRAG.x = x;\n        DRAG.startY = DRAG.y = y;\n        DRAG.distance = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / TRANSFORM.scale;\n      }\n    };\n\n    /**\n     * Pointermove event handler\n     *\n     */\n    const pointermoveHandler = event => {\n      if (!pointerDownCache.length) return;\n      groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n      // Find this event in the cache and update its record with this event\n      const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n      pointerDownCache[index] = event;\n      if (pointerDownCache.length === 2) {\n        // 2-pointer horizontal pinch/zoom gesture\n        const {\n          x,\n          y\n        } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY);\n        const scale = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / DRAG.distance;\n        zoomPan(event.target, clamp(scale, MIN_SCALE, MAX_SCALE), x, y, x - DRAG.x, y - DRAG.y);\n        DRAG.x = x;\n        DRAG.y = y;\n        return;\n      }\n      if (isZoomed()) {\n        const deltaX = event.clientX - DRAG.x;\n        const deltaY = event.clientY - DRAG.y;\n        pan(deltaX, deltaY);\n      }\n      DRAG.x = event.clientX;\n      DRAG.y = event.clientY;\n      if (!isZoomed()) {\n        // Drag animation\n        const deltaX = DRAG.startX - DRAG.x;\n        const deltaY = DRAG.startY - DRAG.y;\n\n        // Skip animation if drag distance is too low\n        if (distance(deltaX, deltaY) < 10) return;\n        if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) {\n          // Horizontal swipe\n          groups[activeGroup].slider.style.transform = `translate(${offset - Math.round(deltaX)}px, 0)`;\n        } else if (userSettings.swipeClose) {\n          // Vertical swipe\n          groups[activeGroup].slider.style.transform = `translate(${offset}px, -${Math.round(deltaY)}px)`;\n        }\n      }\n    };\n\n    /**\n     * Pointerup event handler\n     *\n     */\n    const pointerupHandler = event => {\n      // Intercept regular click handler\n      if (!pointerDownCache.length) return;\n      groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'));\n\n      // Remove this event from the target's cache\n      const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId);\n      pointerDownCache.splice(index, 1);\n      const x = event.clientX;\n      const y = event.clientY;\n      const deltaX = DRAG.startX - x;\n      const deltaY = DRAG.startY - y;\n      const distanceX = Math.abs(deltaX);\n      const distanceY = Math.abs(deltaY);\n      if (distanceX > 8 || distanceY > 8) {\n        if (!isZoomed()) {\n          // Evaluate drag\n          if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) {\n            previous();\n          } else if (deltaX > 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) {\n            next();\n          } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) {\n            close();\n          } else {\n            updateOffset();\n          }\n        }\n      } else {\n        // Evaluate tap\n        const now = Date.now();\n        const tapLength = now - lastTapTime;\n        if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) {\n          // Double click\n          event.preventDefault();\n          lastTapTime = 0;\n          if (isZoomed()) {\n            resetZoom();\n          } else {\n            zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0);\n          }\n        } else {\n          lastTapTime = now;\n          if (isTouchDevice()) {\n            // Delayed tap on mobile\n            window.setTimeout(() => {\n              const {\n                left,\n                top,\n                bottom,\n                right,\n                width\n              } = event.target.getBoundingClientRect();\n              if (y < top || y > bottom || !lastTapTime) return;\n              if (x > left && x < left + width / 2) {\n                previous();\n              } else if (x < right && x > right - width / 2) {\n                next();\n              }\n            }, DOUBLE_TAP_TIME);\n          }\n        }\n      }\n    };\n\n    /**\n     * Wheel event handler\n     *\n     */\n    const wheelHandler = event => {\n      const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1;\n      if (!isZoomed() && !deltaScale) return;\n      event.preventDefault();\n      const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale);\n      zoomPan(event.target, clamp(newScale, MIN_SCALE, MAX_SCALE), event.clientX, event.clientY, 0, 0);\n    };\n    const clampedTranslate = (axis, translate) => {\n      // Whole clamping functionality heavily inspired\n      // by https://github.com/Neophen/pinch-zoom-pan\n      const {\n        element,\n        scale,\n        originX,\n        originY\n      } = TRANSFORM;\n      const axisIsX = axis === 'x';\n      const origin = axisIsX ? originX : originY;\n      const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight';\n      const containerSize = element.parentNode[axisKey];\n      const imageSize = element[axisKey];\n      const bounds = element.getBoundingClientRect();\n      const imageScaledSize = axisIsX ? bounds.width : bounds.height;\n      const defaultOrigin = imageSize / 2;\n      const originOffset = (origin - defaultOrigin) * (scale - 1);\n      const range = Math.max(0, Math.round(imageScaledSize) - containerSize);\n      const max = Math.round(range / 2);\n      const min = 0 - max;\n      return clamp(translate, min + originOffset, max + originOffset);\n    };\n    const clamp = (value, min, max) => Math.max(Math.min(value, max), min);\n    const isZoomed = () => TRANSFORM.scale !== MIN_SCALE;\n    const pan = (deltaX, deltaY) => {\n      if (deltaX !== 0) {\n        TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX);\n      }\n      if (deltaY !== 0) {\n        TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY);\n      }\n      const {\n        element,\n        originX,\n        originY,\n        translateX,\n        translateY,\n        scale\n      } = TRANSFORM;\n      element.style.transformOrigin = `${originX}px ${originY}px`;\n      element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;\n    };\n    const zoomPan = (el, newScale, x, y, deltaX, deltaY) => {\n      if (el.tagName !== 'IMG') return;\n      const {\n        left,\n        top\n      } = el.getBoundingClientRect();\n      const originX = x - left;\n      const originY = y - top;\n      const newOriginX = originX / TRANSFORM.scale;\n      const newOriginY = originY / TRANSFORM.scale;\n      TRANSFORM.element = el;\n      TRANSFORM.originX = newOriginX;\n      TRANSFORM.originY = newOriginY;\n      TRANSFORM.scale = newScale;\n      pan(deltaX, deltaY);\n    };\n    const distance = (dx, dy) => Math.hypot(dx, dy);\n    const midPoint = (x1, y1, x2, y2) => ({\n      x: (x1 + x2) / 2,\n      y: (y1 + y2) / 2\n    });\n    const resetZoom = () => {\n      TRANSFORM.scale = MIN_SCALE;\n      TRANSFORM.originX = 0;\n      TRANSFORM.originY = 0;\n      TRANSFORM.translateX = 0;\n      TRANSFORM.translateY = 0;\n      pan(0, 0);\n    };\n\n    /**\n     * Bind events\n     *\n     */\n    const bindEvents = () => {\n      if (userSettings.keyboard) {\n        window.addEventListener('keydown', keydownHandler);\n      }\n\n      // Resize event\n      window.addEventListener('resize', resizeHandler);\n\n      // Popstate event\n      window.addEventListener('popstate', close);\n\n      // Click event\n      on('click', clickHandler);\n      if (userSettings.draggable) {\n        // Pointer events\n        on('pointerdown', pointerdownHandler);\n        on('pointermove', pointermoveHandler);\n        on('pointerup', pointerupHandler);\n        on('pointercancel', contextmenuHandler);\n        on('pointerout', contextmenuHandler);\n        on('pointerleave', contextmenuHandler);\n        on('contextmenu', contextmenuHandler);\n      }\n\n      // Wheel event\n      on('wheel', wheelHandler);\n    };\n\n    /**\n     * Unbind events\n     *\n     */\n    const unbindEvents = () => {\n      if (userSettings.keyboard) {\n        window.removeEventListener('keydown', keydownHandler);\n      }\n\n      // Resize event\n      window.removeEventListener('resize', resizeHandler);\n\n      // Popstate event\n      window.removeEventListener('popstate', close);\n\n      // Click event\n      off('click', clickHandler);\n      if (userSettings.draggable) {\n        // Pointer events\n        off('pointerdown', pointerdownHandler);\n        off('pointermove', pointermoveHandler);\n        off('pointerup', pointerupHandler);\n        off('pointercancel', contextmenuHandler);\n        off('pointerout', contextmenuHandler);\n        off('pointerleave', contextmenuHandler);\n        off('contextmenu', contextmenuHandler);\n      }\n\n      // Wheel event\n      off('wheel', wheelHandler);\n    };\n\n    /**\n     * Update userSettings\n     *\n     */\n    const updateConfig = () => {\n      const group = groups[activeGroup];\n      const slider = group.slider;\n      if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) {\n        slider.classList.add('tobii__slider--is-draggable');\n      }\n      const hideButtons = !userSettings.nav || group.elementsLength === 1 || userSettings.nav === 'auto' && isTouchDevice();\n      setButtonState(prevButton, hideButtons, hideButtons);\n      setButtonState(nextButton, hideButtons, hideButtons);\n      const hideCounter = !userSettings.counter || group.elementsLength === 1;\n      counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false');\n    };\n\n    /**\n     * Update live region\n     *\n     */\n    const updateAnnouncement = () => {\n      const group = groups[activeGroup];\n      const currIndex = group.currentIndex;\n      const total = group.elementsLength;\n      const trigger = group.gallery[currIndex];\n      const [slide, of] = userSettings.announcementLabel;\n      let extra;\n      if (trigger.hasAttribute('data-label')) {\n        extra = trigger.getAttribute('data-label');\n      } else {\n        const img = trigger.querySelector('img');\n        extra = img?.alt || '';\n      }\n      const base = `${slide} ${currIndex + 1} ${of} ${total}`;\n\n      // Announce reliably\n      liveRegion.textContent = '';\n      window.setTimeout(() => {\n        liveRegion.textContent = extra ? `${base}. ${extra}` : base;\n      }, 10);\n    };\n\n    /**\n     * Update lightbox\n     *\n     * @param {string|null} dir - Current slide direction\n     */\n    const updateLightbox = (dir = null) => {\n      updateOffset();\n      updateCounter();\n      updateAnnouncement();\n      updateFocus(dir);\n    };\n\n    /**\n     * Reset Tobii\n     *\n     */\n    const reset = () => {\n      if (isOpen()) close();\n      Object.values(groups).forEach(group => group.gallery.forEach(remove));\n      groups = {};\n      activeGroup = null;\n      Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset());\n    };\n\n    /**\n     * Destroy Tobii\n     *\n     */\n    const destroy = () => {\n      reset();\n      lightbox.parentNode.removeChild(lightbox);\n    };\n\n    /**\n     * Check if Tobii is open\n     *\n     */\n    const isOpen = () => {\n      return lightbox.getAttribute('aria-hidden') === 'false';\n    };\n\n    /**\n     * Detect whether device is touch capable\n     *\n     */\n    const isTouchDevice = () => {\n      return 'ontouchstart' in window;\n    };\n\n    /**\n     * Checks whether element's tagName is part of array\n     *\n     */\n    const isIgnoreElement = el => {\n      return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton || el === nextButton || el === closeButton;\n    };\n\n    /**\n     * Return current index\n     *\n     */\n    const slidesIndex = () => {\n      return groups[activeGroup].currentIndex;\n    };\n\n    /**\n     * Return elements length\n     *\n     */\n    const slidesCount = () => {\n      return groups[activeGroup].elementsLength;\n    };\n\n    /**\n     * Return current group\n     *\n     */\n    const currentGroup = () => {\n      return activeGroup;\n    };\n\n    /**\n     * Bind events\n     * @param {String} eventName\n     * @param {function} callback - callback to call\n     *\n     */\n    const on = (eventName, callback) => {\n      lightbox.addEventListener(eventName, callback);\n    };\n\n    /**\n     * Unbind events\n     * @param {String} eventName\n     * @param {function} callback - callback to call\n     *\n     */\n    const off = (eventName, callback) => {\n      lightbox.removeEventListener(eventName, callback);\n    };\n    init(userOptions);\n    return {\n      open,\n      previous,\n      next,\n      close,\n      add: checkDependencies,\n      remove,\n      reset,\n      destroy,\n      isOpen,\n      slidesIndex,\n      select,\n      slidesCount,\n      selectGroup,\n      currentGroup,\n      on,\n      off\n    };\n  }\n\n  return Tobii;\n\n}));\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import neostandard from 'neostandard'\n\nexport default neostandard({\n  env: ['browser']\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@midzer/tobii\",\n  \"version\": \"3.2.0\",\n  \"description\": \"An accessible, open-source lightbox with no dependencies.\",\n  \"main\": \"./dist/tobii.js\",\n  \"module\": \"./dist/tobii.module.js\",\n  \"umd:main\": \"./dist/tobii.umd.js\",\n  \"unpkg\": \"./dist/tobii.umd.js\",\n  \"source\": \"./src/js/index.js\",\n  \"exports\": {\n    \".\": {\n      \"browser\": \"./dist/tobii.module.js\",\n      \"umd\": \"./dist/tobii.umd.js\",\n      \"import\": \"./dist/tobii.modern.js\",\n      \"require\": \"./dist/tobii.js\"\n    },\n    \"./package.json\": \"./package.json\",\n    \"./\": \"./\"\n  },\n  \"devDependencies\": {\n    \"cross-env\": \"^10.1.0\",\n    \"eslint\": \"9.39.2\",\n    \"microbundle\": \"^0.15.1\",\n    \"neostandard\": \"^0.12.2\",\n    \"rimraf\": \"4.4.1\",\n    \"sass\": \"^1.98.0\",\n    \"stylelint\": \"^16.26.1\"\n  },\n  \"browserslist\": {\n    \"browser\": [\n      \"last 2 versions\",\n      \"not <= 1%\"\n    ],\n    \"main\": [\n      \"last 2 versions\",\n      \"not <= 1%\"\n    ]\n  },\n  \"scripts\": {\n    \"build\": \"npm run distclean && npm run build:main && npm run build:browser\",\n    \"build:main\": \"cross-env BROWSERSLIST_ENV=main microbundle build --raw --no-compress --no-sourcemap --name Tobii\",\n    \"build:browser\": \"cross-env BROWSERSLIST_ENV=browser microbundle build --raw -f iife src/js/browser.js -o dist/tobii.min.js --no-sourcemap --name Tobii\",\n    \"postbuild:browser\": \"node add-banner.js\",\n    \"distclean\": \"rimraf dist\",\n    \"clean\": \"rimraf dist && rimraf node_modules\",\n    \"dev\": \"microbundle watch --raw --format cjs\",\n    \"dev-modern\": \"microbundle watch --raw --format esm\",\n    \"lint\": \"eslint src\",\n    \"test\": \"npm run lint\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/midzer/tobii.git\"\n  },\n  \"files\": [\n    \"src\",\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"keywords\": [\n    \"lightbox\",\n    \"accessible\",\n    \"a11y\",\n    \"javascript\",\n    \"vanilla\",\n    \"scss\",\n    \"css\"\n  ],\n  \"author\": \"midzer\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/midzer/tobii/issues\"\n  },\n  \"homepage\": \"https://midzer.github.io/tobii/demo/\"\n}\n"
  },
  {
    "path": "src/js/browser.js",
    "content": "import '../scss/tobii.scss'\nimport Tobii from './index'\n\nif (typeof module < 'u') {\n  module.exports = Tobii\n} else {\n  self.Tobii = Tobii\n}\n"
  },
  {
    "path": "src/js/index.js",
    "content": "/**\n * Tobii\n *\n * @author midzer\n * @version 3.2.0\n * @url https://github.com/midzer/tobii\n *\n * MIT License\n */\n\nimport ImageType from './types/image'\nimport IframeType from './types/iframe'\nimport HtmlType from './types/html'\nimport YoutubeType from './types/youtube'\n\nexport default function Tobii (userOptions) {\n  /**\n   * Global variables\n   *\n   */\n  const SUPPORTED_ELEMENTS = {\n    image: new ImageType(), // default\n    html: new HtmlType(),\n    iframe: new IframeType(),\n    youtube: new YoutubeType()\n  }\n  const FOCUSABLE_ELEMENTS = [\n    'a[href]:not([tabindex^=\"-\"]):not([inert])',\n    'area[href]:not([tabindex^=\"-\"]):not([inert])',\n    'input:not([disabled]):not([inert])',\n    'select:not([disabled]):not([inert])',\n    'textarea:not([disabled]):not([inert])',\n    'button:not([disabled]):not([inert])',\n    'iframe:not([tabindex^=\"-\"]):not([inert])',\n    'audio:not([tabindex^=\"-\"]):not([inert])',\n    'video:not([tabindex^=\"-\"]):not([inert])',\n    '[contenteditable]:not([tabindex^=\"-\"]):not([inert])',\n    '[tabindex]:not([tabindex^=\"-\"]):not([inert])'\n  ]\n  let userSettings = {}\n  const WAITING_ELS = []\n  const GROUP_ATTS = {\n    gallery: [],\n    slider: null,\n    sliderElements: [],\n    elementsLength: 0,\n    currentIndex: 0,\n    x: 0\n  }\n  let lightbox = null\n  let prevButton = null\n  let nextButton = null\n  let closeButton = null\n  let counter = null\n  let lastFocus = null\n  let offset = null\n  let isYouTubeDependencyLoaded = false\n  let groups = {}\n  let activeGroup = null\n  let pointerDownCache = []\n  let lastTapTime = 0\n  let liveRegion = null\n  const MIN_SCALE = 1\n  const MAX_SCALE = 4\n  const DOUBLE_TAP_TIME = 500 // milliseconds\n  const SCALE_SENSITIVITY = 10\n  const TRANSFORM = {\n    element: null,\n    originX: 0,\n    originY: 0,\n    translateX: 0,\n    translateY: 0,\n    scale: MIN_SCALE\n  }\n  const DRAG = {\n    startX: 0,\n    startY: 0,\n    x: 0,\n    y: 0,\n    distance: 0\n  }\n\n  /**\n   * Merge default options with user options\n   *\n   * @param {Object} userOptions - Optional user options\n   * @returns {Object} - Custom options\n   */\n  const mergeOptions = (userOptions) => {\n    // Default options\n    const OPTIONS = {\n      selector: '.lightbox',\n      captions: true,\n      captionsSelector: 'img',\n      captionAttribute: 'alt',\n      captionText: null,\n      captionHTML: false,\n      captionToggle: true,\n      captionToggleLabel: [\n        'Hide caption',\n        'Show caption'\n      ],\n      nav: 'auto',\n      navText: [\n        '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"15 6 9 12 15 18\" /></svg>',\n        '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"9 6 15 12 9 18\" /></svg>'\n      ],\n      navLabel: [\n        'Previous image',\n        'Next image'\n      ],\n      announcementLabel: ['Slide', 'of'],\n      close: true,\n      closeText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" /><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" /></svg>',\n      closeLabel: 'Close lightbox',\n      dialogTitle: 'Lightbox',\n      loadingIndicatorLabel: 'Image loading',\n      counter: true,\n      keyboard: true,\n      zoom: false,\n      zoomText: '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path stroke=\"none\" d=\"M0 0h24v24H0z\"/><polyline points=\"16 4 20 4 20 8\" /><line x1=\"14\" y1=\"10\" x2=\"20\" y2=\"4\" /><polyline points=\"8 20 4 20 4 16\" /><line x1=\"4\" y1=\"20\" x2=\"10\" y2=\"14\" /><polyline points=\"16 20 20 20 20 16\" /><line x1=\"14\" y1=\"14\" x2=\"20\" y2=\"20\" /><polyline points=\"8 4 4 4 4 8\" /><line x1=\"4\" y1=\"4\" x2=\"10\" y2=\"10\" /></svg>',\n      docClose: true,\n      swipeClose: true,\n      hideScrollbar: true,\n      draggable: true,\n      threshold: 100,\n      theme: 'tobii--theme-default'\n    }\n\n    return {\n      ...OPTIONS, ...userOptions\n    }\n  }\n\n  /**\n   * Init\n   *\n   */\n  const init = (userOptions) => {\n    // Merge user options into defaults\n    userSettings = mergeOptions(userOptions)\n\n    // Create the lightbox container\n    lightbox = document.createElement('div')\n    lightbox.setAttribute('role', 'dialog')\n    lightbox.setAttribute('aria-hidden', 'true')\n    lightbox.setAttribute('aria-modal', 'true')\n    lightbox.setAttribute('aria-label', userSettings.dialogTitle)\n    lightbox.classList.add('tobii')\n\n    // Add theme class\n    lightbox.classList.add(userSettings.theme)\n\n    // Create the previous button\n    prevButton = document.createElement('button')\n    prevButton.className = 'tobii__btn tobii__btn--previous'\n    prevButton.setAttribute('type', 'button')\n    prevButton.setAttribute('aria-label', userSettings.navLabel[0])\n    prevButton.innerHTML = userSettings.navText[0]\n    lightbox.appendChild(prevButton)\n\n    // Create the next button\n    nextButton = document.createElement('button')\n    nextButton.className = 'tobii__btn tobii__btn--next'\n    nextButton.setAttribute('type', 'button')\n    nextButton.setAttribute('aria-label', userSettings.navLabel[1])\n    nextButton.innerHTML = userSettings.navText[1]\n    lightbox.appendChild(nextButton)\n\n    // Create the close button\n    closeButton = document.createElement('button')\n    closeButton.className = 'tobii__btn tobii__btn--close'\n    closeButton.setAttribute('type', 'button')\n    closeButton.setAttribute('aria-label', userSettings.closeLabel)\n    closeButton.innerHTML = userSettings.closeText\n    lightbox.appendChild(closeButton)\n\n    // Create the counter\n    counter = document.createElement('div')\n    counter.className = 'tobii__counter'\n    lightbox.appendChild(counter)\n\n    // Create the live region\n    liveRegion = document.createElement('div')\n    liveRegion.className = 'tobii__sr'\n    liveRegion.setAttribute('aria-live', 'polite')\n    liveRegion.setAttribute('aria-atomic', 'true')\n    lightbox.appendChild(liveRegion)\n\n    // Append to body\n    document.body.appendChild(lightbox)\n\n    // Init only\n    if (!userSettings.selector) return\n\n    // Get a list of all elements within the document\n    const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector)\n\n    if (!LIGHTBOX_TRIGGER_ELS) {\n      throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`)\n    }\n\n    LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el))\n  }\n\n  /**\n   * Check dependencies\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const checkDependencies = (el) => {\n    // Check if there is a YouTube video and if the YouTube iframe-API is ready\n    if (document.querySelector('[data-type=\"youtube\"]') !== null && !isYouTubeDependencyLoaded) {\n      if (document.getElementById('iframe_api') === null) {\n        const TAG = document.createElement('script')\n        const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0]\n\n        TAG.id = 'iframe_api'\n        TAG.src = 'https://www.youtube.com/iframe_api'\n\n        FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG)\n      }\n\n      if (WAITING_ELS.indexOf(el) === -1) {\n        WAITING_ELS.push(el)\n      }\n\n      window.onYouTubePlayerAPIReady = () => {\n        WAITING_ELS.forEach((waitingEl) => {\n          add(waitingEl)\n        })\n\n        isYouTubeDependencyLoaded = true\n      }\n    } else {\n      add(el)\n    }\n  }\n\n  /**\n   * Get group name from element\n   *\n   * @param {HTMLElement} el\n   * @return {string}\n   */\n  const getGroupName = (el) => {\n    return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default'\n  }\n\n  /**\n   * Copy an object. (The secure way)\n   *\n   * @param {object} object\n   * @return {object}\n   */\n  const copyObject = (object) => {\n    return JSON.parse(JSON.stringify(object))\n  }\n\n  /**\n   * Add element\n   *\n   * @param {HTMLElement} el - Element to add\n   */\n  const add = (el) => {\n    const newGroup = getGroupName(el)\n\n    if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {\n      groups[newGroup] = copyObject(GROUP_ATTS)\n\n      // Create slider\n      groups[newGroup].slider = document.createElement('div')\n      groups[newGroup].slider.className = 'tobii__slider'\n\n      // Hide slider\n      groups[newGroup].slider.setAttribute('aria-hidden', 'true')\n\n      lightbox.appendChild(groups[newGroup].slider)\n    }\n\n    // Check if element already exists\n    if (groups[newGroup].gallery.indexOf(el) === -1) {\n      groups[newGroup].gallery.push(el)\n      groups[newGroup].elementsLength++\n\n      // Set zoom icon if necessary\n      if ((userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false') ||\n        el.getAttribute('data-zoom') === 'true') {\n        const TOBII_ZOOM = document.createElement('div')\n\n        TOBII_ZOOM.className = 'tobii-zoom__icon'\n        TOBII_ZOOM.innerHTML = userSettings.zoomText\n\n        el.classList.add('tobii-zoom')\n        el.appendChild(TOBII_ZOOM)\n      }\n\n      // Bind click event handler\n      el.addEventListener('click', triggerTobii)\n\n      // Create slide\n      const SLIDE_ELEMENT = document.createElement('div')\n      const SLIDE_ELEMENT_CONTENT = document.createElement('div')\n\n      SLIDE_ELEMENT.className = 'tobii__slide'\n      SLIDE_ELEMENT.style.position = 'absolute'\n      SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`\n\n      // Hide slide\n      SLIDE_ELEMENT.setAttribute('aria-hidden', 'true')\n\n      // Create type elements\n      const model = getModel(el)\n      model.init(el, SLIDE_ELEMENT_CONTENT, userSettings)\n\n      // Add slide content container to slide element\n      SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT)\n\n      // Add slide element to slider\n      groups[newGroup].slider.appendChild(SLIDE_ELEMENT)\n      groups[newGroup].sliderElements.push(SLIDE_ELEMENT)\n\n      ++groups[newGroup].x\n\n      if (isOpen() && newGroup === activeGroup) {\n        updateConfig()\n        updateLightbox()\n      }\n    } else {\n      throw new Error('Ups, element already added.')\n    }\n  }\n\n  /**\n   * Remove element\n   *\n   * @param {HTMLElement} el - Element to remove\n   */\n  const remove = (el) => {\n    const GROUP_NAME = getGroupName(el)\n\n    // Check if element exists\n    const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el)\n    if (galleryIndex === -1) {\n      throw new Error(`Ups, I can't find a slide for the element ${el}.`)\n    }\n\n    const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex]\n\n    // If the element to be removed is the currently visible slide\n    if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) {\n      if (groups[GROUP_NAME].elementsLength === 1) {\n        close()\n        throw new Error('Ups, I\\'ve closed. There are no slides more to show.')\n      } else {\n        // Navigate away before removal\n        if (groups[GROUP_NAME].currentIndex === 0) {\n          next()\n        } else {\n          previous()\n        }\n        updateConfig()\n        updateLightbox()\n      }\n    }\n\n    groups[GROUP_NAME].gallery.splice(galleryIndex, 1)\n    groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1)\n    groups[GROUP_NAME].elementsLength--\n    --groups[GROUP_NAME].x\n\n    // Remove zoom icon if necessary\n    if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) {\n      const ZOOM_ICON = el.querySelector('.tobii-zoom__icon')\n      ZOOM_ICON.parentNode.classList.remove('tobii-zoom')\n      ZOOM_ICON.parentNode.removeChild(ZOOM_ICON)\n    }\n\n    // Unbind click event handler\n    el.removeEventListener('click', triggerTobii)\n\n    // Remove slide\n    SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT)\n  }\n\n  const getModel = (el) => {\n    const type = el.getAttribute('data-type')\n    if (SUPPORTED_ELEMENTS[type] !== undefined) {\n      return SUPPORTED_ELEMENTS[type]\n    } else {\n      // unknown - use default\n      if (el.hasAttribute('data-type')) {\n        console.log('Unknown lightbox element type: ' + type)\n      }\n      return SUPPORTED_ELEMENTS.image\n    }\n  }\n\n  /**\n   * Open Tobii\n   *\n   * @param {number} index - Index to load\n   */\n  const open = (index = 0) => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m aleady open.')\n    }\n\n    if (index === -1 || index >= groups[activeGroup].elementsLength) {\n      throw new Error(`Ups, I can't find slide ${index}.`)\n    }\n\n    document.documentElement.classList.add('tobii-is-open')\n    document.body.classList.add('tobii-is-open')\n    document.body.classList.add('tobii-is-open-' + activeGroup)\n\n    updateConfig()\n\n    // Hide close if necessary\n    if (!userSettings.close) {\n      closeButton.disabled = false\n      closeButton.setAttribute('aria-hidden', 'true')\n    }\n\n    // Save user’s focus\n    lastFocus = document.activeElement\n\n    // Use `history.pushState()` to make sure the \"Back\" button behavior\n    // that aligns with the user's expectations\n    const stateObj = {\n      tobii: 'close'\n    }\n    const url = window.location.href\n\n    window.history.pushState(stateObj, 'Image', url)\n\n    // Set current index\n    groups[activeGroup].currentIndex = index\n\n    bindEvents()\n\n    // Load slide\n    load(groups[activeGroup].currentIndex)\n\n    // Show slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'false')\n\n    // Show lightbox\n    lightbox.setAttribute('aria-hidden', 'false')\n\n    updateLightbox()\n\n    // Preload previous and next slide\n    preload(groups[activeGroup].currentIndex + 1)\n    preload(groups[activeGroup].currentIndex - 1)\n\n    groups[activeGroup].slider.classList.add('tobii__slider--animate')\n\n    // Create and dispatch a new event\n    const openEvent = new window.CustomEvent('open', {\n      detail: {\n        group: activeGroup\n      }\n    })\n\n    lightbox.dispatchEvent(openEvent)\n  }\n\n  /**\n   * Close Tobii\n   *\n   */\n  const close = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m already closed.')\n    }\n\n    document.documentElement.classList.remove('tobii-is-open')\n    document.body.classList.remove('tobii-is-open')\n    document.body.classList.remove('tobii-is-open-' + activeGroup)\n\n    unbindEvents()\n\n    // Remove entry in browser history\n    if (window.history.state !== null) {\n      if (window.history.state.tobii === 'close') {\n        window.history.back()\n      }\n    }\n\n    // Reenable the user’s focus\n    lastFocus.focus()\n\n    // Don't forget to cleanup our current element\n    leave(groups[activeGroup].currentIndex)\n    cleanup(groups[activeGroup].currentIndex)\n\n    // Hide lightbox\n    lightbox.setAttribute('aria-hidden', 'true')\n\n    // Hide slider\n    groups[activeGroup].slider.setAttribute('aria-hidden', 'true')\n\n    // Reset current index\n    groups[activeGroup].currentIndex = 0\n\n    // Remove the hack to prevent animation during opening\n    groups[activeGroup].slider.classList.remove('tobii__slider--animate')\n\n    // Create and dispatch a new event\n    const closeEvent = new window.CustomEvent('close', {\n      detail: {\n        group: activeGroup\n      }\n    })\n    lightbox.dispatchEvent(closeEvent)\n  }\n\n  /**\n   * Preload slide\n   *\n   * @param {number} index - Index to preload\n   */\n  const preload = (index) => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return\n    }\n\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]')\n    const model = getModel(CONTAINER)\n\n    model.onPreload(CONTAINER)\n  }\n\n  /**\n   * Load slide\n   * Will be called when opening the lightbox or moving index\n   *\n   * @param {number} index - Index to load\n   */\n  const load = (index) => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return\n    }\n\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]')\n    const model = getModel(CONTAINER)\n\n    model.onLoad(CONTAINER, activeGroup)\n  }\n\n  /**\n   * Select a slide\n   *\n   * @param {number} index - Index to select\n   */\n  const select = (index) => {\n    const currIndex = groups[activeGroup].currentIndex\n\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.')\n    }\n\n    if (isOpen()) {\n      if (!index && index !== 0) {\n        throw new Error('Ups, no slide specified.')\n      }\n\n      if (index === groups[activeGroup].currentIndex) {\n        throw new Error(`Ups, slide ${index} is already selected.`)\n      }\n\n      if (index === -1 || index >= groups[activeGroup].elementsLength) {\n        throw new Error(`Ups, I can't find slide ${index}.`)\n      }\n    }\n\n    // Set current index\n    groups[activeGroup].currentIndex = index\n\n    leave(currIndex)\n    load(index)\n\n    if (index < currIndex) {\n      updateLightbox('left')\n      cleanup(currIndex)\n      preload(index - 1)\n    }\n\n    if (index > currIndex) {\n      updateLightbox('right')\n      cleanup(currIndex)\n      preload(index + 1)\n    }\n  }\n\n  /**\n   * Select the previous slide\n   *\n   */\n  const previous = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.')\n    }\n\n    if (groups[activeGroup].currentIndex > 0) {\n      leave(groups[activeGroup].currentIndex)\n      load(--groups[activeGroup].currentIndex)\n      updateLightbox('left')\n      cleanup(groups[activeGroup].currentIndex + 1)\n      preload(groups[activeGroup].currentIndex - 1)\n    }\n\n    // Create and dispatch a new event\n    const previousEvent = new window.CustomEvent('previous', {\n      detail: {\n        group: activeGroup\n      }\n    })\n\n    lightbox.dispatchEvent(previousEvent)\n  }\n\n  /**\n   * Select the next slide\n   *\n   */\n  const next = () => {\n    if (!isOpen()) {\n      throw new Error('Ups, I\\'m closed.')\n    }\n\n    if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) {\n      leave(groups[activeGroup].currentIndex)\n      load(++groups[activeGroup].currentIndex)\n      updateLightbox('right')\n      cleanup(groups[activeGroup].currentIndex - 1)\n      preload(groups[activeGroup].currentIndex + 1)\n    }\n\n    // Create and dispatch a new event\n    const nextEvent = new window.CustomEvent('next', {\n      detail: {\n        group: activeGroup\n      }\n    })\n\n    lightbox.dispatchEvent(nextEvent)\n  }\n\n  /**\n   * Select a group\n   *\n   * @param {string} name - Name of the group to select\n   */\n  const selectGroup = (name) => {\n    if (isOpen()) {\n      throw new Error('Ups, I\\'m open.')\n    }\n\n    if (!name) {\n      throw new Error('Ups, no group specified.')\n    }\n\n    if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {\n      throw new Error(`Ups, I don't have a group called \"${name}\".`)\n    }\n\n    activeGroup = name\n  }\n\n  /**\n   * Leave slide\n   * Will be called before moving index\n   *\n   * @param {number} index - Index to leave\n   */\n  const leave = (index) => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return\n    }\n\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]')\n    const model = getModel(CONTAINER)\n\n    model.onLeave(CONTAINER)\n  }\n\n  /**\n   * Cleanup slide\n   * Will be called after moving index\n   *\n   * @param {number} index - Index to cleanup\n   */\n  const cleanup = (index) => {\n    if (groups[activeGroup].sliderElements[index] === undefined) {\n      return\n    }\n\n    const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]')\n    const model = getModel(CONTAINER)\n\n    model.onCleanup(CONTAINER)\n\n    DRAG.startX = 0\n    DRAG.startY = 0\n    DRAG.x = 0\n    DRAG.y = 0\n    DRAG.distance = 0\n\n    lastTapTime = 0\n\n    if (isZoomed()) resetZoom()\n\n    TRANSFORM.element = null\n  }\n\n  /**\n   * Update offset\n   *\n   */\n  const updateOffset = () => {\n    offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth\n\n    groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`\n  }\n\n  /**\n   * Update counter\n   *\n   */\n  const updateCounter = () => {\n    counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`\n  }\n\n  /**\n   * Update focus\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateFocus = (dir) => {\n    const group = groups[activeGroup]\n    const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto'\n    const hasMultipleSlides = group.elementsLength > 1\n\n    if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) {\n      setButtonState(prevButton, true, true)\n      setButtonState(nextButton, true, true)\n\n      if (group.currentIndex === 0) {\n        setButtonState(nextButton, false, false)\n        nextButton.focus()\n      } else if (group.currentIndex === group.elementsLength - 1) {\n        setButtonState(prevButton, false, false)\n        prevButton.focus()\n      } else {\n        setButtonState(prevButton, false, false)\n        setButtonState(nextButton, false, false)\n        if (dir === 'left') {\n          prevButton.focus()\n        } else {\n          nextButton.focus()\n        }\n      }\n    } else if (userSettings.close) {\n      closeButton.focus()\n    }\n  }\n\n  /**\n   * Resize event\n   *\n   */\n  const resizeHandler = () => {\n    updateOffset()\n  }\n\n  /**\n   * Click event handler to trigger Tobii\n   *\n   */\n  const triggerTobii = (event) => {\n    event.preventDefault()\n\n    activeGroup = getGroupName(event.currentTarget)\n\n    open(groups[activeGroup].gallery.indexOf(event.currentTarget))\n  }\n\n  /**\n   * Click event handler\n   *\n   */\n  const clickHandler = (event) => {\n    if (event.target === prevButton) {\n      previous()\n    } else if (event.target === nextButton) {\n      next()\n    } else if (event.target === closeButton ||\n      (event.target.classList.contains('tobii__slide') || (event.target.classList.contains('tobii') && userSettings.docClose))) {\n      close()\n    }\n\n    event.stopPropagation()\n  }\n\n  /**\n   * Set the hidden/disabled state of a button\n   *\n   */\n  const setButtonState = (button, hidden, disabled) => {\n    button.setAttribute('aria-hidden', hidden ? 'true' : 'false')\n    button.disabled = disabled\n  }\n\n  /**\n   * Keydown event handler\n   *\n   */\n  const keydownHandler = (event) => {\n    if (event.code === 'Tab') {\n      const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', ')))\n\n      if (FOCUSABLE.length === 0) return\n\n      const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement)\n\n      if (event.shiftKey && FOCUSED_INDEX === 0) {\n        // SHIFT+Tab on first → jump to last\n        FOCUSABLE[FOCUSABLE.length - 1].focus()\n        event.preventDefault()\n      } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) {\n        // Tab on last → jump to first\n        FOCUSABLE[0].focus()\n        event.preventDefault()\n      }\n    } else if (event.code === 'Escape') {\n      // `ESC` Key: Close Tobii\n      event.preventDefault()\n      close()\n    } else if (event.code === 'ArrowLeft') {\n      // `PREV` Key: Show the previous slide\n      event.preventDefault()\n      previous()\n    } else if (event.code === 'ArrowRight') {\n      // `NEXT` Key: Show the next slide\n      event.preventDefault()\n      next()\n    }\n  }\n\n  /**\n   * Contextmenu event handler\n   * This is a fix for chromium based browser on mac.\n   * The 'contextmenu' terminates a mouse event sequence.\n   * https://bugs.chromium.org/p/chromium/issues/detail?id=506801\n   *\n   */\n  const contextmenuHandler = () => {\n    pointerDownCache = []\n    updateOffset()\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'))\n  }\n\n  /**\n   * Pointerdown event handler\n   *\n   */\n  const pointerdownHandler = (event) => {\n    // Prevent dragging / swiping on textareas, inputs and selects\n    if (isIgnoreElement(event.target)) {\n      return\n    }\n\n    event.preventDefault()\n    event.stopPropagation()\n\n    DRAG.startX = DRAG.x = event.clientX\n    DRAG.startY = DRAG.y = event.clientY\n    DRAG.distance = 0\n\n    // This event is cached to support 2-finger gestures\n    pointerDownCache.push(event)\n\n    if (pointerDownCache.length === 2) {\n      const { x, y } = midPoint(\n        pointerDownCache[0].clientX, pointerDownCache[0].clientY,\n        pointerDownCache[1].clientX, pointerDownCache[1].clientY\n      )\n\n      DRAG.startX = DRAG.x = x\n      DRAG.startY = DRAG.y = y\n      DRAG.distance = distance(\n        pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY\n      ) / TRANSFORM.scale\n    }\n  }\n\n  /**\n   * Pointermove event handler\n   *\n   */\n  const pointermoveHandler = (event) => {\n    if (!pointerDownCache.length) return\n\n    groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'))\n\n    // Find this event in the cache and update its record with this event\n    const index = pointerDownCache.findIndex(\n      (cachedEv) => cachedEv.pointerId === event.pointerId\n    )\n    pointerDownCache[index] = event\n\n    if (pointerDownCache.length === 2) {\n      // 2-pointer horizontal pinch/zoom gesture\n      const { x, y } = midPoint(\n        pointerDownCache[0].clientX, pointerDownCache[0].clientY,\n        pointerDownCache[1].clientX, pointerDownCache[1].clientY\n      )\n      const scale = distance(\n        pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY\n      ) / DRAG.distance\n\n      zoomPan(\n        event.target,\n        clamp(scale, MIN_SCALE, MAX_SCALE),\n        x, y,\n        x - DRAG.x, y - DRAG.y\n      )\n\n      DRAG.x = x\n      DRAG.y = y\n\n      return\n    }\n\n    if (isZoomed()) {\n      const deltaX = event.clientX - DRAG.x\n      const deltaY = event.clientY - DRAG.y\n\n      pan(deltaX, deltaY)\n    }\n\n    DRAG.x = event.clientX\n    DRAG.y = event.clientY\n\n    if (!isZoomed()) {\n      // Drag animation\n      const deltaX = DRAG.startX - DRAG.x\n      const deltaY = DRAG.startY - DRAG.y\n\n      // Skip animation if drag distance is too low\n      if (distance(deltaX, deltaY) < 10) return\n\n      if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) {\n        // Horizontal swipe\n        groups[activeGroup].slider.style.transform =\n          `translate(${offset - Math.round(deltaX)}px, 0)`\n      } else if (userSettings.swipeClose) {\n        // Vertical swipe\n        groups[activeGroup].slider.style.transform =\n        `translate(${offset}px, -${Math.round(deltaY)}px)`\n      }\n    }\n  }\n\n  /**\n   * Pointerup event handler\n   *\n   */\n  const pointerupHandler = (event) => {\n    // Intercept regular click handler\n    if (!pointerDownCache.length) return\n\n    groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging'))\n\n    // Remove this event from the target's cache\n    const index = pointerDownCache.findIndex(\n      (cachedEv) => cachedEv.pointerId === event.pointerId\n    )\n    pointerDownCache.splice(index, 1)\n\n    const x = event.clientX\n    const y = event.clientY\n    const deltaX = DRAG.startX - x\n    const deltaY = DRAG.startY - y\n    const distanceX = Math.abs(deltaX)\n    const distanceY = Math.abs(deltaY)\n    if (distanceX > 8 || distanceY > 8) {\n      if (!isZoomed()) {\n        // Evaluate drag\n        if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) {\n          previous()\n        } else if (deltaX > 0 && distanceX > userSettings.threshold &&\n          groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) {\n          next()\n        } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) {\n          close()\n        } else {\n          updateOffset()\n        }\n      }\n    } else {\n      // Evaluate tap\n      const now = Date.now()\n      const tapLength = now - lastTapTime\n      if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) {\n        // Double click\n        event.preventDefault()\n        lastTapTime = 0\n        if (isZoomed()) {\n          resetZoom()\n        } else {\n          zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0)\n        }\n      } else {\n        lastTapTime = now\n        if (isTouchDevice()) {\n          // Delayed tap on mobile\n          window.setTimeout(() => {\n            const { left, top, bottom, right, width } = event.target.getBoundingClientRect()\n            if (y < top || y > bottom || !lastTapTime) return\n            if (x > left && x < left + width / 2) {\n              previous()\n            } else if (x < right && x > right - width / 2) {\n              next()\n            }\n          }, DOUBLE_TAP_TIME)\n        }\n      }\n    }\n  }\n\n  /**\n   * Wheel event handler\n   *\n   */\n  const wheelHandler = (event) => {\n    const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1\n    if (!isZoomed() && !deltaScale) return\n    event.preventDefault()\n\n    const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale)\n    zoomPan(\n      event.target,\n      clamp(newScale, MIN_SCALE, MAX_SCALE),\n      event.clientX, event.clientY,\n      0, 0\n    )\n  }\n\n  const clampedTranslate = (axis, translate) => {\n    // Whole clamping functionality heavily inspired\n    // by https://github.com/Neophen/pinch-zoom-pan\n    const { element, scale, originX, originY } = TRANSFORM\n    const axisIsX = axis === 'x'\n    const origin = axisIsX ? originX : originY\n    const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight'\n\n    const containerSize = element.parentNode[axisKey]\n    const imageSize = element[axisKey]\n    const bounds = element.getBoundingClientRect()\n\n    const imageScaledSize = axisIsX ? bounds.width : bounds.height\n\n    const defaultOrigin = imageSize / 2\n    const originOffset = (origin - defaultOrigin) * (scale - 1)\n\n    const range = Math.max(0, Math.round(imageScaledSize) - containerSize)\n\n    const max = Math.round(range / 2)\n    const min = 0 - max\n\n    return clamp(translate, min + originOffset, max + originOffset)\n  }\n\n  const clamp = (value, min, max) => Math.max(Math.min(value, max), min)\n\n  const isZoomed = () => TRANSFORM.scale !== MIN_SCALE\n\n  const pan = (deltaX, deltaY) => {\n    if (deltaX !== 0) {\n      TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX)\n    }\n    if (deltaY !== 0) {\n      TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY)\n    }\n\n    const { element, originX, originY, translateX, translateY, scale } = TRANSFORM\n    element.style.transformOrigin = `${originX}px ${originY}px`\n    element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`\n  }\n\n  const zoomPan = (el, newScale, x, y, deltaX, deltaY) => {\n    if (el.tagName !== 'IMG') return\n\n    const { left, top } = el.getBoundingClientRect()\n    const originX = x - left\n    const originY = y - top\n    const newOriginX = originX / TRANSFORM.scale\n    const newOriginY = originY / TRANSFORM.scale\n\n    TRANSFORM.element = el\n    TRANSFORM.originX = newOriginX\n    TRANSFORM.originY = newOriginY\n    TRANSFORM.scale = newScale\n\n    pan(deltaX, deltaY)\n  }\n\n  const distance = (dx, dy) => Math.hypot(dx, dy)\n\n  const midPoint = (x1, y1, x2, y2) => ({\n    x: (x1 + x2) / 2,\n    y: (y1 + y2) / 2\n  })\n\n  const resetZoom = () => {\n    TRANSFORM.scale = MIN_SCALE\n    TRANSFORM.originX = 0\n    TRANSFORM.originY = 0\n    TRANSFORM.translateX = 0\n    TRANSFORM.translateY = 0\n\n    pan(0, 0)\n  }\n\n  /**\n   * Bind events\n   *\n   */\n  const bindEvents = () => {\n    if (userSettings.keyboard) {\n      window.addEventListener('keydown', keydownHandler)\n    }\n\n    // Resize event\n    window.addEventListener('resize', resizeHandler)\n\n    // Popstate event\n    window.addEventListener('popstate', close)\n\n    // Click event\n    on('click', clickHandler)\n\n    if (userSettings.draggable) {\n      // Pointer events\n      on('pointerdown', pointerdownHandler)\n      on('pointermove', pointermoveHandler)\n      on('pointerup', pointerupHandler)\n      on('pointercancel', contextmenuHandler)\n      on('pointerout', contextmenuHandler)\n      on('pointerleave', contextmenuHandler)\n      on('contextmenu', contextmenuHandler)\n    }\n\n    // Wheel event\n    on('wheel', wheelHandler)\n  }\n\n  /**\n   * Unbind events\n   *\n   */\n  const unbindEvents = () => {\n    if (userSettings.keyboard) {\n      window.removeEventListener('keydown', keydownHandler)\n    }\n\n    // Resize event\n    window.removeEventListener('resize', resizeHandler)\n\n    // Popstate event\n    window.removeEventListener('popstate', close)\n\n    // Click event\n    off('click', clickHandler)\n\n    if (userSettings.draggable) {\n      // Pointer events\n      off('pointerdown', pointerdownHandler)\n      off('pointermove', pointermoveHandler)\n      off('pointerup', pointerupHandler)\n      off('pointercancel', contextmenuHandler)\n      off('pointerout', contextmenuHandler)\n      off('pointerleave', contextmenuHandler)\n      off('contextmenu', contextmenuHandler)\n    }\n\n    // Wheel event\n    off('wheel', wheelHandler)\n  }\n\n  /**\n   * Update userSettings\n   *\n   */\n  const updateConfig = () => {\n    const group = groups[activeGroup]\n    const slider = group.slider\n\n    if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) {\n      slider.classList.add('tobii__slider--is-draggable')\n    }\n\n    const hideButtons = (\n      !userSettings.nav ||\n      group.elementsLength === 1 ||\n      (userSettings.nav === 'auto' && isTouchDevice())\n    )\n    setButtonState(prevButton, hideButtons, hideButtons)\n    setButtonState(nextButton, hideButtons, hideButtons)\n\n    const hideCounter = !userSettings.counter || group.elementsLength === 1\n    counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false')\n  }\n\n  /**\n   * Update live region\n   *\n   */\n  const updateAnnouncement = () => {\n    const group = groups[activeGroup]\n    const currIndex = group.currentIndex\n    const total = group.elementsLength\n    const trigger = group.gallery[currIndex]\n    const [slide, of] = userSettings.announcementLabel\n\n    let extra\n    if (trigger.hasAttribute('data-label')) {\n      extra = trigger.getAttribute('data-label')\n    } else {\n      const img = trigger.querySelector('img')\n      extra = img?.alt || ''\n    }\n\n    const base = `${slide} ${currIndex + 1} ${of} ${total}`\n\n    // Announce reliably\n    liveRegion.textContent = ''\n    window.setTimeout(() => {\n      liveRegion.textContent = extra ? `${base}. ${extra}` : base\n    }, 10)\n  }\n\n  /**\n   * Update lightbox\n   *\n   * @param {string|null} dir - Current slide direction\n   */\n  const updateLightbox = (dir = null) => {\n    updateOffset()\n    updateCounter()\n    updateAnnouncement()\n    updateFocus(dir)\n  }\n\n  /**\n   * Reset Tobii\n   *\n   */\n  const reset = () => {\n    if (isOpen()) close()\n\n    Object.values(groups).forEach(group =>\n      group.gallery.forEach(remove)\n    )\n\n    groups = {}\n    activeGroup = null\n\n    Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset())\n  }\n\n  /**\n   * Destroy Tobii\n   *\n   */\n  const destroy = () => {\n    reset()\n\n    lightbox.parentNode.removeChild(lightbox)\n  }\n\n  /**\n   * Check if Tobii is open\n   *\n   */\n  const isOpen = () => {\n    return lightbox.getAttribute('aria-hidden') === 'false'\n  }\n\n  /**\n   * Detect whether device is touch capable\n   *\n   */\n  const isTouchDevice = () => {\n    return 'ontouchstart' in window\n  }\n\n  /**\n   * Checks whether element's tagName is part of array\n   *\n   */\n  const isIgnoreElement = (el) => {\n    return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton ||\n      el === nextButton || el === closeButton\n  }\n\n  /**\n   * Return current index\n   *\n   */\n  const slidesIndex = () => {\n    return groups[activeGroup].currentIndex\n  }\n\n  /**\n   * Return elements length\n   *\n   */\n  const slidesCount = () => {\n    return groups[activeGroup].elementsLength\n  }\n\n  /**\n   * Return current group\n   *\n   */\n  const currentGroup = () => {\n    return activeGroup\n  }\n\n  /**\n   * Bind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const on = (eventName, callback) => {\n    lightbox.addEventListener(eventName, callback)\n  }\n\n  /**\n   * Unbind events\n   * @param {String} eventName\n   * @param {function} callback - callback to call\n   *\n   */\n  const off = (eventName, callback) => {\n    lightbox.removeEventListener(eventName, callback)\n  }\n\n  init(userOptions)\n\n  return {\n    open,\n    previous,\n    next,\n    close,\n    add: checkDependencies,\n    remove,\n    reset,\n    destroy,\n    isOpen,\n    slidesIndex,\n    select,\n    slidesCount,\n    selectGroup,\n    currentGroup,\n    on,\n    off\n  }\n}\n"
  },
  {
    "path": "src/js/types/html.js",
    "content": "class HtmlType {\n  constructor () {\n    this.userSettings = null\n  }\n\n  init (el, container, userSettings) {\n    this.userSettings = userSettings\n\n    const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href')\n    const TARGET = document.querySelector(TARGET_SELECTOR)\n\n    if (!TARGET) {\n      throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`)\n    }\n\n    // Add content to container\n    container.appendChild(TARGET)\n\n    // Register type\n    container.setAttribute('data-type', 'html')\n    container.classList.add('tobii-html')\n  }\n\n  onPreload (container) {\n    // Nothing\n  }\n\n  onLoad (container, group) {\n    const VIDEO = container.querySelector('video')\n\n    if (VIDEO) {\n      if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) {\n        // Continue where video was stopped\n        VIDEO.currentTime = VIDEO.getAttribute('data-time')\n      }\n\n      // Start playback (and loading if necessary)\n      VIDEO.play()\n    }\n\n    const audio = container.querySelector('audio')\n    if (audio) {\n      // Start playback (and loading if necessary)\n      audio.play()\n    }\n\n    container.classList.add('tobii-group-' + group)\n  }\n\n  onLeave (container) {\n    const VIDEO = container.querySelector('video')\n\n    if (VIDEO) {\n      if (!VIDEO.paused) {\n        // Stop if video is playing\n        VIDEO.pause()\n      }\n\n      // Backup currentTime (needed for revisit)\n      if (VIDEO.readyState > 0) {\n        VIDEO.setAttribute('data-time', VIDEO.currentTime)\n      }\n    }\n\n    const audio = container.querySelector('audio')\n\n    if (audio) {\n      if (!audio.paused) {\n        // Stop if is playing\n        audio.pause()\n      }\n    }\n  }\n\n  onCleanup (container) {\n    const VIDEO = container.querySelector('video')\n\n    if (VIDEO) {\n      if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) {\n        // Some data has been loaded but not the whole package.\n        // In order to save bandwidth, stop downloading as soon as possible.\n        const VIDEO_CLONE = VIDEO.cloneNode(true)\n\n        this._removeSources(VIDEO)\n        VIDEO.load()\n\n        VIDEO.parentNode.removeChild(VIDEO)\n\n        container.appendChild(VIDEO_CLONE)\n      }\n    }\n  }\n\n  onReset () {\n    // Nothing\n  }\n\n  /**\n   * Remove all `src` attributes\n   *\n   * @param {HTMLElement} el - Element to remove all `src` attributes\n   */\n  _removeSources (el) {\n    const SOURCES = el.querySelectorAll('src')\n\n    if (SOURCES) {\n      SOURCES.forEach((source) => {\n        source.removeAttribute('src')\n      })\n    }\n  }\n}\n\nexport default HtmlType\n"
  },
  {
    "path": "src/js/types/iframe.js",
    "content": "class IframeType {\n  constructor () {\n    this.userSettings = null\n  }\n\n  init (el, container, userSettings) {\n    this.userSettings = userSettings\n\n    const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href')\n\n    container.setAttribute('data-HREF', HREF)\n    if (el.hasAttribute('data-allow')) {\n      container.setAttribute('data-allow', el.getAttribute('data-allow'))\n    }\n    if (el.hasAttribute('data-width')) {\n      container.setAttribute('data-width', `${el.getAttribute('data-width')}`)\n    }\n    if (el.hasAttribute('data-height')) {\n      container.setAttribute('data-height', `${el.getAttribute('data-height')}`)\n    }\n\n    // dont create empty iframes here - very slow\n\n    // Register type\n    container.setAttribute('data-type', 'iframe')\n    container.classList.add('tobii-iframe')\n  }\n\n  onPreload (container) {\n    // Nothing\n  }\n\n  onLoad (container) {\n    let IFRAME = container.querySelector('iframe')\n\n    // Create loading indicator\n    const LOADING_INDICATOR = document.createElement('div')\n    LOADING_INDICATOR.className = 'tobii__loader'\n    LOADING_INDICATOR.setAttribute('role', 'progressbar')\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel)\n    container.appendChild(LOADING_INDICATOR)\n\n    if (IFRAME == null) {\n      // create iframe\n      IFRAME = document.createElement('iframe')\n      const HREF = container.getAttribute('data-href')\n\n      IFRAME.setAttribute('frameborder', '0')\n      IFRAME.setAttribute('src', HREF)\n\n      // Set allow parameters\n      let allowValue = 'fullscreen'\n\n      if (HREF.includes('youtube.com')) {\n        allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'\n      } else if (HREF.includes('vimeo.com')) {\n        allowValue += '; autoplay; picture-in-picture'\n      } else if (container.hasAttribute('data-allow')) {\n        allowValue = container.getAttribute('data-allow')\n      }\n\n      IFRAME.setAttribute('allow', allowValue)\n\n      if (container.hasAttribute('data-width')) {\n        IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`\n      }\n\n      if (container.hasAttribute('data-height')) {\n        IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`\n      }\n\n      // Hide until loaded\n      IFRAME.style.opacity = '0'\n\n      // Add iframe to container\n      container.appendChild(IFRAME)\n\n      // Handle load and error\n      const removeLoader = () => {\n        IFRAME.style.opacity = '1'\n        const LOADING_INDICATOR = container.querySelector('.tobii__loader')\n        if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR)\n      }\n\n      IFRAME.addEventListener('load', removeLoader)\n      IFRAME.addEventListener('error', removeLoader)\n    } else {\n      // was already created\n      IFRAME.setAttribute('src', container.getAttribute('data-href'))\n    }\n  }\n\n  onLeave (container) {\n    // Nothing\n  }\n\n  onCleanup (container) {\n    const IFRAME = container.querySelector('iframe')\n    IFRAME.removeAttribute('src')\n    IFRAME.style.opacity = '0'\n  }\n\n  onReset () {\n    // Nothing\n  }\n}\n\nexport default IframeType\n"
  },
  {
    "path": "src/js/types/image.js",
    "content": "class ImageType {\n  constructor () {\n    this.figcaptionId = 0\n    this.userSettings = null\n  }\n\n  init (el, container, userSettings) {\n    this.userSettings = userSettings\n\n    const FIGURE = document.createElement('figure')\n    const IMAGE = document.createElement('img')\n    const THUMBNAIL = el.querySelector('img')\n    const LOADING_INDICATOR = document.createElement('div')\n\n    // Accessibility: allow setting focus programmatically on figure elements.\n    FIGURE.tabIndex = -1\n\n    // Add role=\"group\" to figure\n    FIGURE.setAttribute('role', 'group')\n\n    // Hide figure until the image is loaded\n    FIGURE.style.opacity = '0'\n\n    if (THUMBNAIL) {\n      IMAGE.alt = THUMBNAIL.alt || ''\n    }\n\n    IMAGE.setAttribute('data-src', el.href)\n\n    if (el.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset'))\n    }\n\n    if (el.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes'))\n    }\n\n    // Add image to figure\n    FIGURE.appendChild(IMAGE)\n\n    // Create figcaption\n    let captionContent\n    if (typeof this.userSettings.captionText === 'function') {\n      captionContent = this.userSettings.captionText(el)\n    } else if (this.userSettings.captionsSelector === 'self' &&\n      el.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = el.getAttribute(this.userSettings.captionAttribute)\n    } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL &&\n      THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) {\n      captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute)\n    }\n    if (this.userSettings.captions && captionContent) {\n      const FIGCAPTION = document.createElement('figcaption')\n      FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`\n\n      const SPAN = document.createElement('span')\n      if (this.userSettings.captionHTML) {\n        SPAN.innerHTML = captionContent\n      } else {\n        SPAN.textContent = captionContent\n      }\n      FIGCAPTION.appendChild(SPAN)\n\n      if (this.userSettings.captionToggle) {\n        const isMobile = window.innerWidth < 768\n        const BUTTON = document.createElement('button')\n        BUTTON.className = 'caption-toggle'\n        BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0]\n        BUTTON.setAttribute('aria-controls', FIGCAPTION.id)\n        BUTTON.setAttribute('aria-expanded', !isMobile)\n        if (isMobile) {\n          FIGCAPTION.classList.add('caption-hidden')\n        }\n        SPAN.setAttribute('aria-hidden', isMobile)\n\n        const preventAndStopEvent = (event) => {\n          event.preventDefault()\n          event.stopPropagation()\n        }\n        BUTTON.addEventListener('pointerdown', (event) => preventAndStopEvent(event))\n        BUTTON.addEventListener('pointerup', (event) => preventAndStopEvent(event))\n        BUTTON.addEventListener('click', (event) => {\n          preventAndStopEvent(event)\n          const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true'\n          const buttonLabel = isExpanded\n            ? this.userSettings.captionToggleLabel[1]\n            : this.userSettings.captionToggleLabel[0]\n          BUTTON.textContent = BUTTON.title = buttonLabel\n          BUTTON.setAttribute('aria-expanded', !isExpanded)\n          FIGCAPTION.classList.toggle('caption-hidden')\n          SPAN.setAttribute('aria-hidden', isExpanded)\n        })\n\n        FIGCAPTION.appendChild(BUTTON)\n      }\n\n      FIGURE.appendChild(FIGCAPTION)\n\n      IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id)\n\n      // Add aria-label to the figure containing the caption content\n      FIGURE.setAttribute('aria-label', SPAN.textContent)\n\n      ++this.figcaptionId\n    }\n\n    // Add figure to container\n    container.appendChild(FIGURE)\n\n    // Create loading indicator\n    LOADING_INDICATOR.className = 'tobii__loader'\n    LOADING_INDICATOR.setAttribute('role', 'progressbar')\n    LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel)\n\n    // Add loading indicator to container\n    container.appendChild(LOADING_INDICATOR)\n\n    // Register type\n    container.setAttribute('data-type', 'image')\n    container.classList.add('tobii-image')\n  }\n\n  onPreload (container) {\n    // Same as preload\n    this.onLoad(container)\n  }\n\n  onLoad (container) {\n    const IMAGE = container.querySelector('img')\n\n    if (!IMAGE.hasAttribute('data-src')) {\n      return\n    }\n\n    const FIGURE = container.querySelector('figure')\n    const LOADING_INDICATOR = container.querySelector('.tobii__loader')\n\n    const handleImageEvent = () => {\n      container.removeChild(LOADING_INDICATOR)\n      FIGURE.style.opacity = '1'\n    }\n\n    IMAGE.addEventListener('load', handleImageEvent)\n    IMAGE.addEventListener('error', handleImageEvent)\n\n    if (IMAGE.hasAttribute('data-srcset')) {\n      IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset'))\n      IMAGE.removeAttribute('data-srcset')\n    }\n\n    if (IMAGE.hasAttribute('data-sizes')) {\n      IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes'))\n      IMAGE.removeAttribute('data-sizes')\n    }\n\n    IMAGE.setAttribute('src', IMAGE.getAttribute('data-src'))\n    IMAGE.removeAttribute('data-src')\n  }\n\n  onLeave (container) {\n    // Nothing\n  }\n\n  onCleanup (container) {\n    // Nothing\n  }\n\n  onReset () {\n    this.figcaptionId = 0\n  }\n}\n\nexport default ImageType\n"
  },
  {
    "path": "src/js/types/youtube.js",
    "content": "class YoutubeType {\n  constructor () {\n    this.playerId = 0\n    this.PLAYER = []\n    this.userSettings = null\n  }\n\n  init (el, container, userSettings) {\n    this.userSettings = userSettings\n\n    const IFRAME_PLACEHOLDER = document.createElement('div')\n\n    // Add iframePlaceholder to container\n    container.appendChild(IFRAME_PLACEHOLDER)\n\n    this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {\n      host: 'https://www.youtube-nocookie.com',\n      height: el.getAttribute('data-height') || '360',\n      width: el.getAttribute('data-width') || '640',\n      videoId: el.getAttribute('data-id'),\n      playerVars: {\n        controls: el.getAttribute('data-controls') || 1,\n        rel: 0,\n        playsinline: 1\n      }\n    })\n\n    // Set player ID\n    container.setAttribute('data-player', this.playerId)\n\n    // Register type\n    container.setAttribute('data-type', 'youtube')\n    container.classList.add('tobii-youtube')\n\n    this.playerId++\n  }\n\n  onPreload (container) {\n    // Nothing\n  }\n\n  onLoad (container) {\n    this.PLAYER[container.getAttribute('data-player')].playVideo()\n  }\n\n  onLeave (container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo()\n    }\n  }\n\n  onCleanup (container) {\n    if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) {\n      this.PLAYER[container.getAttribute('data-player')].pauseVideo()\n    }\n  }\n\n  onReset () {\n    // Nothing\n  }\n}\n\nexport default YoutubeType\n"
  },
  {
    "path": "src/scss/_variables.scss",
    "content": ":root {\n  --tobii-base-font-size: 1rem; /* also update --tobii-slide-max-height */\n\n  --tobii-transition-duration: 0.3s;\n  --tobii-transition-timing-function: cubic-bezier(0.19, 1, 0.22, 1);\n\n  --tobii-zoom-icon-background: hsla(210, 38%, 16%, 0.94);\n  --tobii-zoom-icon-color: #ffffff;\n\n  --tobii-lightbox-background: rgba(0,0,0,0.85);\n  --tobii-lightbox-z-index: 1337;\n\n  --tobii-caption-background: rgba(0,0,0,0.8);\n  --tobii-caption-color: #eeeeee;\n\n  --tobii-counter-background: transparent;\n  --tobii-counter-color: #ffffff;\n\n  --tobii-button-background: transparent;\n  --tobii-button-navigation-background: rgba(0,0,0,0.5);\n  --tobii-button-color: #ffffff;\n\n  --tobii-loader-color: #ffffff;\n\n  --tobii-slide-max-height: calc(100vh - 3.125em);\n  --tobii-slide-max-width: 100vw;\n}\n"
  },
  {
    "path": "src/scss/tobii.scss",
    "content": "@import 'variables';\n\n/**\n * Lightbox link\n *\n */\n\n.tobii-zoom {\n  border: 0;\n  box-shadow: none;\n  display: inline-block;\n  position: relative;\n  text-decoration: none;\n\n\n  & img {\n    display: block;\n  }\n\n  &__icon {\n    align-items: center;\n    background-color: var(--tobii-zoom-icon-background);\n    top: 0.5em;\n    color: var(--tobii-zoom-icon-color);\n    display: flex;\n    justify-content: center;\n    line-height: 1;\n    position: absolute;\n    right: 0.5em;\n    width: 1.78em;\n    height: 1.78em;\n\n\n    & svg {\n      fill: none;\n      height: 1.5em;\n      pointer-events: none;\n      stroke-linecap: round;\n      stroke-linejoin: round;\n      stroke-width: 1.5;\n      stroke: currentColor;\n      width: 1.5em;\n    }\n  }\n}\n\n// Hide scrollbar if lightbox is displayed\nbody.tobii-is-open {\n  overflow-y: hidden;\n}\n\n// Prevent background scrolling on Safari iOS devices.\n.tobii-is-open,\n.tobii-image {\n  touch-action: none;\n}\n\n/**\n * Lightbox\n *\n */\n\n.tobii {\n  background-color: var(--tobii-lightbox-background);\n  bottom: 0;\n  box-sizing: border-box;\n  contain: strict;\n  font-size: var(--tobii-base-font-size);\n  left: 0;\n  line-height: 1.5;\n  overflow: hidden;\n  position: fixed;\n  right: 0;\n  top: 0;\n  z-index: var(--tobii-lightbox-z-index);\n\n\n  &[aria-hidden='true'] {\n    display: none;\n  }\n\n  & *,\n  & *::before,\n  & *::after {\n    box-sizing: inherit;\n  }\n}\n\n\n/**\n * Slider\n *\n */\n\n.tobii__slider {\n  bottom: 0;\n  left: 0;\n  position: absolute;\n  right: 0;\n  top: 3.125em;\n  will-change: transform;\n\n  &[aria-hidden='true'] {\n    display: none;\n  }\n\n  @media screen and (prefers-reduced-motion: no-preference) {\n    &--animate:not(&--is-dragging) {\n      transition-duration: var(--tobii-transition-duration);\n      transition-property: transform;\n      transition-timing-function: var(--tobii-transition-timing-function);\n    }\n  }\n\n  &--is-draggable [data-type] {\n    cursor: grab;\n  }\n\n  &--is-dragging [data-type] {\n    cursor: grabbing;\n  }\n\n  &--is-moving [data-type] {\n    cursor: move;\n  }\n}\n\n/**\n * Slide\n *\n */\n\n.tobii__slide {\n  align-items: center;\n  display: flex;\n  height: 100%;\n  justify-content: center;\n  width: 100%;\n\n  & [data-type] {\n    max-height: 100%;\n    max-width: var(--tobii-slide-max-width);\n    overflow: hidden;\n    overscroll-behavior: contain;\n  }\n\n  & iframe,\n  & video {\n    display: block !important;\n  }\n\n  & figure {\n    margin: 0;\n    position: relative;\n\n\n    & > img {\n      display: block;\n      height: auto;\n      max-height: var(--tobii-slide-max-height);\n      max-width: var(--tobii-slide-max-width);\n      width: auto;\n    }\n\n    & > figcaption {\n      background-color: var(--tobii-caption-background);\n      bottom: 0;\n      color: var(--tobii-caption-color);\n      padding: 0.25em 0.5em;\n      position: absolute;\n      white-space: pre-wrap;\n      width: 100%;\n      font-size: 1.125em;\n      transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;\n\n      &.caption-hidden {\n        color: transparent;\n        background-color: transparent;\n\n        & > button.caption-toggle {\n          &::after {\n            opacity: 0.6;\n          }\n\n          &:hover::after {\n            opacity: 1;\n          }\n        }\n      }\n\n      & > button.caption-toggle {\n        position: absolute;\n        z-index: 2;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        margin: 0;\n        padding: 0 3em 0 0;\n        cursor: pointer;\n        color: transparent;\n        border: none;\n        background: transparent;\n\n        &::after {\n          position: absolute;\n          right: 0.25em;\n          bottom: 0.25em;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          width: 1.75em;\n          height: 1.75em;\n          content: 'ⓘ';\n          transition: opacity 0.3s ease-in-out;\n          pointer-events: none;\n          opacity: 0;\n          color: var(--tobii-caption-color);\n          border-radius: 50%;\n          background-color: var(--tobii-caption-background);\n          font-size: medium;\n          font-weight: bold;\n          line-height: 1;\n        }\n      }\n    }\n  }\n\n  & [data-type='html'] {\n    overflow-y: auto;\n\n    & video {\n      cursor: auto;\n      max-height: var(--tobii-slide-max-height);\n      max-width: var(--tobii-slide-max-width);\n    }\n\n    & audio {\n      max-width: 100%;\n    }\n  }\n\n  & [data-type='iframe'] {\n\n    /* Fix iframe scrolling on iOS */\n    -webkit-overflow-scrolling: touch;\n    transform: translate(0, 0);\n\n\n    & iframe {\n      height: var(--tobii-slide-max-height);\n      width: var(--tobii-slide-max-width);\n    }\n  }\n}\n\n/**\n * Buttons\n *\n */\n\n.tobii__btn {\n  appearance: none;\n  background-color: var(--tobii-button-background);\n  border: 1px solid transparent;\n  color: var(--tobii-button-color);\n  cursor: pointer;\n  font: inherit;\n  line-height: 1;\n  margin: 0;\n  opacity: 0.5;\n  padding: 0;\n  position: absolute;\n  touch-action: manipulation;\n  will-change: opacity;\n  z-index: 1;\n\n  &:hover {\n    opacity: 1;\n  }\n\n  @media screen and (prefers-reduced-motion: no-preference) {\n    transition-duration: var(--tobii-transition-duration);\n    transition-property: opacity, transform;\n    transition-timing-function: var(--tobii-transition-timing-function);\n    will-change: opacity, transform;\n  }\n\n\n  & svg {\n    fill: none;\n    height: 3.75em;\n    pointer-events: none;\n    stroke-linecap: round;\n    stroke-linejoin: round;\n    stroke-width: 1;\n    stroke: currentColor;\n    width: 3.75em;\n  }\n\n  &--previous,\n  &--next {\n    top: 50%;\n    transform: translateY(-50%);\n    background-color: var(--tobii-button-navigation-background);\n    border-radius: 5px;\n  }\n\n  &--previous {\n    left: 1em;\n  }\n\n  &--next {\n    right: 1em;\n  }\n\n  &--close {\n    right: 0.25em;\n    top: 0.25em;\n    width: 2.5em;\n    height: 2.5em;\n    opacity: 0.75;\n\n    & svg {\n      transform: scale(1.4);\n      width: 100%;\n      height: 100%;\n    }\n  }\n\n  &:disabled,\n  &[aria-hidden='true'] {\n    visibility: hidden;\n    cursor: default;\n  }\n}\n\n/**\n * Counter\n *\n */\n\n.tobii__counter {\n  background-color: var(--tobii-counter-background);\n  color: var(--tobii-counter-color);\n  font-size: 1.25em;\n  left: 0.875em;\n  line-height: 1;\n  position: absolute;\n  top: 0.875em;\n  z-index: 1;\n  opacity: 0.8;\n\n  &[aria-hidden='true'] {\n    display: none;\n  }\n\n  & p {\n    display: inline;\n  }\n}\n\n/**\n * Loader\n *\n */\n\n.tobii__loader {\n  display: inline-block;\n  height: 6em;\n  left: 50%;\n  position: absolute;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  width: 6em;\n\n\n  &::before {\n    animation: spin 1s infinite;\n    border-radius: 100%;\n    border: 2px solid #949ba3;\n    border-top-color: var(--tobii-loader-color);\n    bottom: 0;\n    content: '';\n    left: 0;\n    position: absolute;\n    right: 0;\n    top: 0;\n    z-index: 1;\n  }\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.tobii__slide .tobii-html {\n  background: #ffffff;\n  padding: 10px 20px;\n  max-width: 800px;\n  font-size: 1.125em;\n}\n\n/**\n * Screen Reader\n *\n */\n\n.tobii__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n"
  }
]