chameleon-backgrounds 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1 @@
1
- {
2
- "version": 3,
3
- "file": "chameleon-backgrounds.js",
4
- "mappings": "AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;UCVA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,2CAA2C,0CAA0C;WACrF,MAAM;WACN,2CAA2C,gCAAgC;WAC3E;WACA,KAAK,yBAAyB;WAC9B;WACA,GAAG;WACH;WACA;WACA,0CAA0C,wCAAwC;WAClF;WACA;WACA;WACA,E;;;;;WCtBA,wF;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D,E;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,aAAa,QAAQ;AACrB,cAAc,QAAQ;AACtB,cAAc,QAAQ;AACtB,cAAc,QAAQ;AACtB;AACA,aAAa,QAAQ;AACrB,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,6DAA6D;AAC3E,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC;;AAEA;AACA,aAAa,kBAAkB;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH,aAAa,SAAS;AACtB;;AAEA,aAAa,kBAAkB;AAC/B;;AAEA,aAAa,aAAa;AAC1B;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,aAAa;AAC1B;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,SAAS;AACtB;;AAEA,aAAa,2BAA2B;AACxC;;AAEA,aAAa,SAAS;AACtB;;AAEA;AACA;AACA,aAAa,2BAA2B;AACxC;AACA,0BAA0B;AAC1B;AACA,sBAAsB;AACtB;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,+CAA+C,UAAU;;AAEzD;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;AACA,+EAA+E,UAAU;AACzF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA,aAAa;AACb;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA,MAAM;AACN;AACA;AACA,QAAQ;AACR;AACA;AACA,UAAU;AACV,8EAA8E,YAAY;AAC1F;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,4CAA4C,UAAU;AACtD,yDAAyD,wCAAwC;AACjG;;AAEA;AACA,oEAAoE,2BAA2B;AAC/F,MAAM;AACN;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,8BAA8B,IAAI;AAClC;;AAEA;AACA;AACA;AACA;;AAEA;AACA,qDAAqD,IAAI;;AAEzD;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;;AAEA;AACA;AACA,8DAA8D,oBAAoB;AAClF,aAAa,6BAA6B;AAC1C,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,eAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,0EAA0E,UAAU;AACpF,uDAAuD,SAAS;;AAEhE;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA,0DAA0D,SAAS;AACnE;AACA;AACA;AACA;AACA,WAAW;;AAEX,UAAU;AACV;AACA,wDAAwD,SAAS;AACjE;AACA;;AAEA,oEAAoE,UAAU;AAC9E;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,mBAAmB;AACnB;;AAEA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,eAAe;AACf;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA,OAAO;AACP,KAAK;AACL;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;;AAEA;AACA,sBAAsB;;AAEtB;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,wCAAwC;;AAExC;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,UAAU;AACV;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA,aAAa,6BAA6B;AAC1C;AACA;AACA;;AAEA;AACA,gEAAgE,UAAU;AAC1E;AACA;AACA;;AAEA;AACA;AACA;AACA,OAAO;AACP,MAAM;AACN;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,aAAa,oBAAoB;AACjC,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA,+EAA+E,GAAG;AAClF,wBAAwB,aAAa;AACrC;AACA,mEAAmE,GAAG;AACtE;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,WAAW;AACnC;AACA;AACA;AACA;;AAEA,iEAAe,oBAAoB,EAAC;AACJ",
5
- "sources": [
6
- "webpack://ChameleonBackgrounds/webpack/universalModuleDefinition",
7
- "webpack://ChameleonBackgrounds/webpack/bootstrap",
8
- "webpack://ChameleonBackgrounds/webpack/runtime/define property getters",
9
- "webpack://ChameleonBackgrounds/webpack/runtime/hasOwnProperty shorthand",
10
- "webpack://ChameleonBackgrounds/webpack/runtime/make namespace object",
11
- "webpack://ChameleonBackgrounds/./src/chameleon-backgrounds.js"
12
- ],
13
- "sourcesContent": [
14
- "(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ChameleonBackgrounds\"] = factory();\n\telse\n\t\troot[\"ChameleonBackgrounds\"] = factory();\n})(this, () => {\nreturn ",
15
- "// The require scope\nconst __webpack_require__ = {};\n\n",
16
- "// define getter/value functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tif(Array.isArray(definition)) {\n\t\tvar i = 0;\n\t\twhile(i < definition.length) {\n\t\t\tvar key = definition[i++];\n\t\t\tvar binding = definition[i++];\n\t\t\tif(!__webpack_require__.o(exports, key)) {\n\t\t\t\tif(binding === 0) {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, value: definition[i++] });\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: binding });\n\t\t\t\t}\n\t\t\t} else if(binding === 0) { i++; }\n\t\t}\n\t} else {\n\t\tfor(var key in definition) {\n\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t\t}\n\t\t}\n\t}\n};",
17
- "__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))",
18
- "// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};",
19
- "/**\n * _____ _ _\n * / ____| | | |\n * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __\n * | | | '_ \\ / _` | '_ ` _ \\ / _ \\ |/ _ \\/ _ \\| '_ \\\n * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |\n * \\_____|_| |_|\\__,_|_| |_| |_|\\___|_|\\___|\\___|_| |_|_\n * | _ \\ | | | |\n * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___\n * | _ < / _` |/ __| |/ / _` | '__/ _ \\| | | | '_ \\ / _` / __|\n * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \\__ \\\n * |____/ \\__,_|\\___|_|\\_\\__, |_| \\___/ \\__,_|_| |_|\\__,_|___/\n * __/ |\n * |___/\n *\n * @module ChameleonBackgrounds\n * @version 3.0.0\n * @author Lennart van Ballegoij (https://weblenn.com/)\n * @license MIT\n * @see https://github.com/WebLenn/ChameleonBackgrounds\n *\n * A zero-dependency JavaScript library to dynamically load background\n * images with elegant fade-in transitions and slideshow support.\n */\n\n/**\n * @typedef {Object} ChameleonImageConfig\n * @property {string} url - The fallback URL.\n * @property {string} [srcset] - Optional srcset.\n * @property {string} [sizes] - Optional sizes.\n *\n * @typedef {Object} ChameleonOptions\n * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.\n * @property {'single'|'slider'} type - Background mode.\n * @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).\n * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).\n * @property {string} [overlayImage] - Optional overlay pattern image URL.\n * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).\n * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.\n * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.\n * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.\n * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).\n * @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.\n * @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).\n */\n\nclass ChameleonBackgrounds {\n /** @type {ChameleonOptions} */\n static #DEFAULTS = Object.freeze({\n element: 'body',\n type: 'single',\n src: '',\n overlayColor: '#0f1e25',\n overlayImage: null,\n minOverlay: 0,\n transitionDuration: 2000,\n sliderDuration: 8000,\n sliderLoop: false,\n fetchPriority: 'auto',\n lazyLoad: true,\n transitionMode: 'solid',\n respectReducedMotion: false,\n });\n\n // Legacy snake_case → camelCase alias map\n static #ALIASES = Object.freeze({\n transition_duration: 'transitionDuration',\n slider_duration: 'sliderDuration',\n slider_loop: 'sliderLoop',\n min_overlay: 'minOverlay',\n overlay_color: 'overlayColor',\n overlay_image: 'overlayImage',\n fetch_priority: 'fetchPriority',\n lazy_load: 'lazyLoad',\n transition_mode: 'transitionMode',\n respect_reduced_motion: 'respectReducedMotion',\n });\n\n /** @type {boolean} */\n static #globalStylesInjected = false;\n\n /** @type {ChameleonOptions} */\n #options;\n\n /** @type {HTMLElement} */\n #element;\n\n /** @type {string} */\n #uid;\n\n /** @type {string} */\n #originalHTML;\n\n /** @type {number|null} */\n #sliderIntervalId = null;\n\n /** @type {number} */\n #currentSlideIndex = 0;\n\n /** @type {boolean} */\n #isPaused = false;\n\n /** @type {IntersectionObserver|null} */\n #observer = null;\n\n /** @type {boolean} */\n #destroyed = false;\n\n /**\n * Create a new ChameleonBackgrounds instance.\n * @param {Partial<ChameleonOptions>} [options]\n */\n constructor(options = {}) {\n const normalized = ChameleonBackgrounds.#normalizeOptions(options);\n this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };\n this.#uid = ChameleonBackgrounds.#generateUID();\n this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);\n this.#originalHTML = this.#element.innerHTML;\n\n this.#init();\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Clean up all injected DOM, styles, and intervals.\n * Restores the target element to its original state.\n */\n destroy() {\n if (this.#destroyed) return;\n this.#destroyed = true;\n\n // Stop any running slider\n if (this.#sliderIntervalId !== null) {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n\n if (this.#observer) {\n this.#observer.disconnect();\n this.#observer = null;\n }\n\n // Clean up dynamic classes on host\n this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');\n\n // Unwrap the original content instead of resetting innerHTML\n const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);\n if (wrapper && wrapper.parentNode === this.#element) {\n while (wrapper.firstChild) {\n this.#element.insertBefore(wrapper.firstChild, wrapper);\n }\n this.#element.removeChild(wrapper);\n }\n\n // Remove loader\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.remove();\n }\n\n // Remove temporary crossfade elements\n const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);\n crossfadeElements.forEach(el => el.remove());\n\n // Remove inline CSS Variables and background-image\n this.#element.style.backgroundImage = '';\n this.#element.style.removeProperty('--cbg-duration');\n this.#element.style.removeProperty('--cbg-overlay-color');\n this.#element.style.removeProperty('--cbg-overlay-image');\n this.#element.style.removeProperty('--cbg-min-overlay');\n }\n\n /**\n * The resolved options (read-only copy).\n * @returns {ChameleonOptions}\n */\n get options() {\n return { ...this.#options };\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n #init() {\n this.#injectGlobalStyles();\n this.#updateCSSVariables();\n this.#buildDOM();\n\n // Check for prefers-reduced-motion to auto-pause slider if enabled\n if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n this.#isPaused = true;\n }\n\n if (this.#options.lazyLoad && 'IntersectionObserver' in window) {\n this.#observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n this.#observer.disconnect();\n this.#observer = null;\n this.#retrieveBackground();\n }\n });\n });\n this.#observer.observe(this.#element);\n } else {\n if (this.#element.matches('body')) {\n queueMicrotask(() => this.#retrieveBackground());\n } else {\n if (document.readyState === 'complete') {\n this.#retrieveBackground();\n } else {\n window.addEventListener('load', () => this.#retrieveBackground(), { once: true });\n }\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Play / Pause API\n // ---------------------------------------------------------------------------\n\n /**\n * Pause the slideshow.\n */\n pause() {\n if (this.#destroyed || this.#options.type !== 'slider') return;\n this.#isPaused = true;\n if (this.#sliderIntervalId !== null) {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n }\n\n /**\n * Resume the slideshow.\n */\n play() {\n if (this.#destroyed || this.#options.type !== 'slider') return;\n this.#isPaused = false;\n // Only restart if not currently running\n if (this.#sliderIntervalId === null) {\n this.#startSliderLoop();\n }\n }\n\n // ---------------------------------------------------------------------------\n // DOM Construction\n // ---------------------------------------------------------------------------\n\n /**\n * Inject global styles once.\n */\n #injectGlobalStyles() {\n if (ChameleonBackgrounds.#globalStylesInjected) return;\n ChameleonBackgrounds.#globalStylesInjected = true;\n\n const css = `\n .cbg-host {\n position: relative;\n }\n body.cbg-host {\n /* body naturally acts as relative for background sizing if not explicitly positioned differently */\n }\n .cbg-inner {\n z-index: 2;\n position: relative;\n }\n .cbg-loader {\n height: 100%;\n width: 100%;\n position: absolute;\n top: 0;\n left: 0;\n background-image: var(--cbg-overlay-image, none);\n background-color: var(--cbg-overlay-color, transparent);\n opacity: 1;\n z-index: 1;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-loader {\n position: fixed;\n }\n .cbg-crossfade-el {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n z-index: 0;\n opacity: 0;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-crossfade-el {\n position: fixed;\n }\n `;\n\n const style = document.createElement('style');\n style.id = 'chameleon-global-styles';\n style.textContent = css;\n document.head.appendChild(style);\n }\n\n /**\n * Apply CSS custom properties to the host element.\n */\n #updateCSSVariables() {\n this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');\n this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);\n this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);\n\n if (this.#options.overlayImage) {\n this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);\n } else {\n this.#element.style.setProperty('--cbg-overlay-image', 'none');\n }\n\n this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));\n }\n\n /**\n * Wrap existing content and insert the overlay loader.\n */\n #buildDOM() {\n const uid = this.#uid;\n\n const wrapper = document.createElement('div');\n wrapper.id = `cbg-inner-${uid}`;\n wrapper.classList.add('cbg-inner');\n\n // Move all child nodes from the element into the wrapper\n while (this.#element.firstChild) {\n wrapper.appendChild(this.#element.firstChild);\n }\n\n const loader = document.createElement('div');\n loader.classList.add('cbg-loader', `cbg-loader-${uid}`);\n\n // Append wrapper and loader\n this.#element.appendChild(wrapper);\n this.#element.appendChild(loader);\n }\n\n // ---------------------------------------------------------------------------\n // Background Loading\n // ---------------------------------------------------------------------------\n\n /**\n * Route to the correct loader based on type.\n */\n #retrieveBackground() {\n if (this.#destroyed) return;\n\n if (this.#options.type === 'single') {\n const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;\n this.#loadBackground(singleSrc);\n } else if (this.#options.type === 'slider') {\n this.#startSlider();\n }\n }\n\n /**\n * Preload an image and apply it as the element's background.\n * Handles both string URLs and responsive image objects ({ url, srcset, sizes }).\n * @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.\n * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).\n * @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.\n * @returns {Promise<void>}\n */\n #loadBackground(srcConfig, isFirst = true, isCrossfade = false) {\n if (this.#destroyed) return Promise.resolve();\n\n return new Promise((resolve) => {\n const img = new Image();\n\n let urlStr = srcConfig;\n if (typeof srcConfig === 'object' && srcConfig.url) {\n urlStr = srcConfig.url;\n if (srcConfig.srcset) img.srcset = srcConfig.srcset;\n if (srcConfig.sizes) img.sizes = srcConfig.sizes;\n }\n\n if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {\n img.fetchPriority = this.#options.fetchPriority;\n }\n\n // Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers\n img.style.position = 'absolute';\n img.style.visibility = 'hidden';\n img.style.width = '0';\n img.style.height = '0';\n this.#element.appendChild(img);\n\n img.onload = () => {\n img.remove();\n if (this.#destroyed) return resolve();\n\n // `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`\n const finalUrl = img.currentSrc || urlStr;\n\n if (isCrossfade && !isFirst) {\n // --- Crossfade Transition Mode ---\n const crossfadeDiv = document.createElement('div');\n crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);\n crossfadeDiv.style.backgroundImage = `url(\"${finalUrl}\")`;\n\n // Insert right behind the loader (or directly at start of host)\n this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);\n\n // Trigger layout so the transition works\n void crossfadeDiv.offsetWidth;\n\n crossfadeDiv.style.opacity = '1';\n\n setTimeout(() => {\n if (this.#destroyed) return resolve();\n this.#element.style.backgroundImage = `url(\"${finalUrl}\")`;\n this.#element.style.backgroundSize = 'cover';\n this.#element.style.backgroundRepeat = 'no-repeat';\n crossfadeDiv.remove();\n resolve();\n }, this.#options.transitionDuration);\n\n } else {\n // --- Solid Transition Mode or First Load ---\n this.#element.style.backgroundImage = `url(\"${finalUrl}\")`;\n this.#element.style.backgroundSize = 'cover';\n this.#element.style.backgroundRepeat = 'no-repeat';\n\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = String(this.#options.minOverlay);\n }\n resolve();\n }\n };\n\n img.onerror = () => {\n img.remove();\n console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);\n resolve(); // resolve anyway to keep logic flowing\n };\n\n img.src = urlStr;\n });\n }\n\n /**\n /**\n * Reload background with a fade-out → swap → fade-in cycle.\n * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.\n * @returns {Promise<void>}\n */\n reloadBackground(src) {\n if (this.#destroyed) return Promise.resolve();\n\n if (src !== undefined) {\n if (this.#options.type === 'slider' && typeof src === 'string') {\n // If the user passes a comma-separated string, split it.\n this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];\n } else {\n this.#options.src = src;\n }\n }\n\n // Stop any running slider\n if (this.#sliderIntervalId !== null) {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n\n // For solid mode, we fade the loader up. For crossfade, we don't.\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader && this.#options.transitionMode === 'solid') {\n loader.style.opacity = '1';\n }\n\n return new Promise((resolve) => {\n const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;\n setTimeout(() => {\n if (this.#destroyed) return resolve();\n\n if (this.#options.type === 'single') {\n const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;\n this.#loadBackground(singleSrc, false, this.#options.transitionMode === 'crossfade').then(resolve);\n } else if (this.#options.type === 'slider') {\n this.#startSlider();\n resolve();\n }\n }, waitTime);\n });\n }\n\n /**\n * Update options on the fly without destroying the instance.\n * @param {Object} newOptions - The new options to merge.\n */\n reloadOptions(newOptions) {\n if (this.#destroyed) return;\n\n const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);\n this.#options = { ...this.#options, ...normalized };\n\n this.#updateCSSVariables();\n // Restart slider if needed\n if (this.#options.type === 'slider' && !this.#isPaused && !this.#sliderIntervalId) {\n this.#startSlider();\n }\n }\n\n // ---------------------------------------------------------------------------\n // Slider\n // ---------------------------------------------------------------------------\n\n /**\n * Start the background slideshow.\n */\n #startSlider() {\n if (this.#destroyed) return;\n\n const sources = this.#options.src;\n if (!Array.isArray(sources) || sources.length === 0) {\n console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');\n return;\n }\n\n // Load the first slide immediately\n this.#currentSlideIndex = 0;\n this.#loadBackground(sources[this.#currentSlideIndex]).then(() => {\n if (this.#destroyed) return;\n\n this.#currentSlideIndex = 1;\n if (sources.length === 1) return; // only one slide, nothing to rotate\n\n this.#startSliderLoop();\n });\n }\n\n #startSliderLoop() {\n const sources = this.#options.src;\n \n // Parse as integers to prevent string concatenation bugs if users passed strings\n const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;\n const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;\n const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);\n\n this.#sliderIntervalId = setInterval(() => {\n if (this.#destroyed || this.#isPaused) {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n return;\n }\n\n this.#cycleSliderSlide(sources[this.#currentSlideIndex]);\n this.#currentSlideIndex++;\n\n if (this.#currentSlideIndex >= sources.length) {\n if (this.#options.sliderLoop) {\n this.#currentSlideIndex = 0;\n } else {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n }\n }, interval);\n }\n\n /**\n * Cycle to a specific slide without resetting the slider instance.\n * @param {string|ChameleonImageConfig} src - The image URL/config to load.\n */\n #cycleSliderSlide(src) {\n if (this.#destroyed) return;\n\n if (this.#options.transitionMode === 'solid') {\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = '1';\n }\n\n setTimeout(() => {\n if (this.#destroyed) return;\n this.#loadBackground(src, false, false);\n }, this.#options.transitionDuration);\n } else {\n // Crossfade mode triggers immediately\n this.#loadBackground(src, false, true);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Static Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Convert legacy snake_case option keys to camelCase.\n * @param {Object} opts\n * @returns {Object}\n */\n static #normalizeOptions(opts) {\n const result = {};\n for (const [key, value] of Object.entries(opts)) {\n const alias = ChameleonBackgrounds.#ALIASES[key];\n result[alias ?? key] = value;\n }\n return result;\n }\n\n /**\n * Resolve a CSS selector string or HTMLElement to an HTMLElement.\n * @param {string|HTMLElement} el\n * @returns {HTMLElement}\n */\n static #resolveElement(el) {\n if (el instanceof HTMLElement) return el;\n if (typeof el === 'string') {\n if (el === 'body') return document.body;\n const found = document.querySelector(el);\n if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);\n return /** @type {HTMLElement} */ (found);\n }\n throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);\n }\n\n /**\n * Generate a short unique ID for scoping styles.\n * @returns {string}\n */\n static #generateUID() {\n // Prefer crypto.randomUUID where available, otherwise fall back\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID().slice(0, 8);\n }\n // Fallback: random alphanumeric string\n return Array.from({ length: 8 }, () =>\n 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))\n ).join('');\n }\n}\n\nexport default ChameleonBackgrounds;\nexport { ChameleonBackgrounds };\n"
20
- ],
21
- "names": [],
22
- "sourceRoot": ""
23
- }
1
+ {"version":3,"file":"chameleon-backgrounds.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;UCVA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,2CAA2C,0CAA0C;WACrF,MAAM;WACN,2CAA2C,gCAAgC;WAC3E;WACA,KAAK,yBAAyB;WAC9B;WACA,GAAG;WACH;WACA;WACA,0CAA0C,wCAAwC;WAClF;WACA;WACA;WACA,E;;;;;WCtBA,wF;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D,E;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,aAAa,QAAQ;AACrB,cAAc,QAAQ;AACtB,cAAc,QAAQ;AACtB,cAAc,QAAQ;AACtB;AACA,aAAa,QAAQ;AACrB,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,6DAA6D;AAC3E,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC,cAAc,yBAAyB;AACvC;;AAEA;AACA,aAAa,kBAAkB;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH,aAAa,SAAS;AACtB;;AAEA,aAAa,kBAAkB;AAC/B;;AAEA,aAAa,aAAa;AAC1B;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,aAAa;AAC1B;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,SAAS;AACtB;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,SAAS;AACtB;;AAEA,aAAa,2BAA2B;AACxC;;AAEA,aAAa,SAAS;AACtB;;AAEA;AACA;AACA,aAAa,2BAA2B;AACxC;AACA,0BAA0B;AAC1B;AACA,sBAAsB;AACtB;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,+CAA+C,UAAU;;AAEzD;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;AACA,+EAA+E,UAAU;AACzF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA,aAAa;AACb;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA,MAAM;AACN;AACA;AACA,QAAQ;AACR;AACA;AACA,UAAU;AACV,8EAA8E,YAAY;AAC1F;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,4CAA4C,UAAU;AACtD,yDAAyD,wCAAwC;AACjG;;AAEA;AACA,oEAAoE,2BAA2B;AAC/F,MAAM;AACN;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,8BAA8B,IAAI;AAClC;;AAEA;AACA;AACA;AACA;;AAEA;AACA,qDAAqD,IAAI;;AAEzD;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;;AAEA;AACA;AACA,8DAA8D,oBAAoB;AAClF,aAAa,6BAA6B;AAC1C,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,eAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,0EAA0E,UAAU;AACpF,uDAAuD,SAAS;;AAEhE;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA,0DAA0D,SAAS;AACnE;AACA;AACA;AACA;AACA,WAAW;;AAEX,UAAU;AACV;AACA,wDAAwD,SAAS;AACjE;AACA;;AAEA,oEAAoE,UAAU;AAC9E;AACA;AACA;AACA;AACA;AACA;AACA,WAAW;AACX;AACA;;AAEA;AACA;AACA;AACA,mBAAmB;AACnB;;AAEA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,eAAe;AACf;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;;AAEA;AACA,sBAAsB;;AAEtB;AACA,2DAA2D;AAC3D;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;;AAEA;AACA,OAAO;AACP,KAAK;AACL;;AAEA;AACA;AACA,aAAa,6BAA6B;AAC1C,aAAa,QAAQ;AACrB,eAAe;AACf;AACA;AACA;;AAEA;AACA;AACA,kEAAkE,UAAU;AAC5E;AACA;AACA;;AAEA;AACA;AACA;AACA,SAAS;AACT,QAAQ;AACR;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,aAAa,oBAAoB;AACjC,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA,+EAA+E,GAAG;AAClF,wBAAwB,aAAa;AACrC;AACA,mEAAmE,GAAG;AACtE;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,WAAW;AACnC;AACA;AACA;AACA;;AAEA,iEAAe,oBAAoB,EAAC;AACJ","sources":["webpack://ChameleonBackgrounds/webpack/universalModuleDefinition","webpack://ChameleonBackgrounds/webpack/bootstrap","webpack://ChameleonBackgrounds/webpack/runtime/define property getters","webpack://ChameleonBackgrounds/webpack/runtime/hasOwnProperty shorthand","webpack://ChameleonBackgrounds/webpack/runtime/make namespace object","webpack://ChameleonBackgrounds/./src/chameleon-backgrounds.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ChameleonBackgrounds\"] = factory();\n\telse\n\t\troot[\"ChameleonBackgrounds\"] = factory();\n})(this, () => {\nreturn ","// The require scope\nconst __webpack_require__ = {};\n\n","// define getter/value functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tif(Array.isArray(definition)) {\n\t\tvar i = 0;\n\t\twhile(i < definition.length) {\n\t\t\tvar key = definition[i++];\n\t\t\tvar binding = definition[i++];\n\t\t\tif(!__webpack_require__.o(exports, key)) {\n\t\t\t\tif(binding === 0) {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, value: definition[i++] });\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: binding });\n\t\t\t\t}\n\t\t\t} else if(binding === 0) { i++; }\n\t\t}\n\t} else {\n\t\tfor(var key in definition) {\n\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t\t}\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * _____ _ _\n * / ____| | | |\n * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __\n * | | | '_ \\ / _` | '_ ` _ \\ / _ \\ |/ _ \\/ _ \\| '_ \\\n * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |\n * \\_____|_| |_|\\__,_|_| |_| |_|\\___|_|\\___|\\___|_| |_|_\n * | _ \\ | | | |\n * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___\n * | _ < / _` |/ __| |/ / _` | '__/ _ \\| | | | '_ \\ / _` / __|\n * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \\__ \\\n * |____/ \\__,_|\\___|_|\\_\\__, |_| \\___/ \\__,_|_| |_|\\__,_|___/\n * __/ |\n * |___/\n *\n * @module ChameleonBackgrounds\n * @version 3.0.1\n * @author Lennart van Ballegoij (https://weblenn.com/)\n * @license MIT\n * @see https://github.com/WebLenn/ChameleonBackgrounds\n *\n * A zero-dependency JavaScript library to dynamically load background\n * images with elegant fade-in transitions and slideshow support.\n */\n\n/**\n * @typedef {Object} ChameleonImageConfig\n * @property {string} url - The fallback URL.\n * @property {string} [srcset] - Optional srcset.\n * @property {string} [sizes] - Optional sizes.\n *\n * @typedef {Object} ChameleonOptions\n * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.\n * @property {'single'|'slider'} type - Background mode.\n * @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).\n * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).\n * @property {string} [overlayImage] - Optional overlay pattern image URL.\n * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).\n * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.\n * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.\n * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.\n * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).\n * @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.\n * @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).\n */\n\nclass ChameleonBackgrounds {\n /** @type {ChameleonOptions} */\n static #DEFAULTS = Object.freeze({\n element: 'body',\n type: 'single',\n src: '',\n overlayColor: '#0f1e25',\n overlayImage: null,\n minOverlay: 0,\n transitionDuration: 2000,\n sliderDuration: 8000,\n sliderLoop: false,\n fetchPriority: 'auto',\n lazyLoad: true,\n transitionMode: 'solid',\n respectReducedMotion: false,\n });\n\n // Legacy snake_case → camelCase alias map\n static #ALIASES = Object.freeze({\n transition_duration: 'transitionDuration',\n slider_duration: 'sliderDuration',\n slider_loop: 'sliderLoop',\n min_overlay: 'minOverlay',\n overlay_color: 'overlayColor',\n overlay_image: 'overlayImage',\n fetch_priority: 'fetchPriority',\n lazy_load: 'lazyLoad',\n transition_mode: 'transitionMode',\n respect_reduced_motion: 'respectReducedMotion',\n });\n\n /** @type {boolean} */\n static #globalStylesInjected = false;\n\n /** @type {ChameleonOptions} */\n #options;\n\n /** @type {HTMLElement} */\n #element;\n\n /** @type {string} */\n #uid;\n\n /** @type {string} */\n #originalHTML;\n\n /** @type {number|null} */\n #sliderIntervalId = null;\n\n /** @type {number} */\n #currentSlideIndex = 0;\n\n /** @type {boolean} */\n #hasLoadedFirstBackground = false;\n\n /** @type {number} */\n #transitionId = 0;\n\n /** @type {boolean} */\n #isPaused = false;\n\n /** @type {IntersectionObserver|null} */\n #observer = null;\n\n /** @type {boolean} */\n #destroyed = false;\n\n /**\n * Create a new ChameleonBackgrounds instance.\n * @param {Partial<ChameleonOptions>} [options]\n */\n constructor(options = {}) {\n const normalized = ChameleonBackgrounds.#normalizeOptions(options);\n this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };\n this.#uid = ChameleonBackgrounds.#generateUID();\n this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);\n this.#originalHTML = this.#element.innerHTML;\n\n this.#init();\n }\n\n // ---------------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------------\n\n /**\n * Clean up all injected DOM, styles, and intervals.\n * Restores the target element to its original state.\n */\n destroy() {\n if (this.#destroyed) return;\n this.#destroyed = true;\n\n // Stop any running slider\n if (this.#sliderIntervalId !== null) {\n clearTimeout(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n\n if (this.#observer) {\n this.#observer.disconnect();\n this.#observer = null;\n }\n\n // Clean up dynamic classes on host\n this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');\n\n // Unwrap the original content instead of resetting innerHTML\n const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);\n if (wrapper && wrapper.parentNode === this.#element) {\n while (wrapper.firstChild) {\n this.#element.insertBefore(wrapper.firstChild, wrapper);\n }\n this.#element.removeChild(wrapper);\n }\n\n // Remove loader\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.remove();\n }\n\n // Remove temporary crossfade elements\n const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);\n crossfadeElements.forEach(el => el.remove());\n\n // Remove inline CSS Variables and background-image\n this.#element.style.backgroundImage = '';\n this.#element.style.removeProperty('--cbg-duration');\n this.#element.style.removeProperty('--cbg-overlay-color');\n this.#element.style.removeProperty('--cbg-overlay-image');\n this.#element.style.removeProperty('--cbg-min-overlay');\n }\n\n /**\n * The resolved options (read-only copy).\n * @returns {ChameleonOptions}\n */\n get options() {\n return { ...this.#options };\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n #init() {\n this.#injectGlobalStyles();\n this.#updateCSSVariables();\n this.#buildDOM();\n\n // Check for prefers-reduced-motion to auto-pause slider if enabled\n if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n this.#isPaused = true;\n }\n\n if (this.#options.lazyLoad && 'IntersectionObserver' in window) {\n this.#observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n this.#observer.disconnect();\n this.#observer = null;\n this.#retrieveBackground();\n }\n });\n });\n this.#observer.observe(this.#element);\n } else {\n if (this.#element.matches('body')) {\n queueMicrotask(() => this.#retrieveBackground());\n } else {\n if (document.readyState === 'complete') {\n this.#retrieveBackground();\n } else {\n window.addEventListener('load', () => this.#retrieveBackground(), { once: true });\n }\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Play / Pause API\n // ---------------------------------------------------------------------------\n\n /**\n * Pause the slideshow.\n */\n pause() {\n if (this.#destroyed || this.#options.type !== 'slider') return;\n this.#isPaused = true;\n if (this.#sliderIntervalId !== null) {\n clearTimeout(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n }\n\n /**\n * Resume the slideshow.\n */\n play() {\n if (this.#destroyed || this.#options.type !== 'slider') return;\n this.#isPaused = false;\n // Only restart if not currently running\n if (this.#sliderIntervalId === null) {\n this.#startSliderLoop();\n }\n }\n\n // ---------------------------------------------------------------------------\n // DOM Construction\n // ---------------------------------------------------------------------------\n\n /**\n * Inject global styles once.\n */\n #injectGlobalStyles() {\n if (ChameleonBackgrounds.#globalStylesInjected) return;\n ChameleonBackgrounds.#globalStylesInjected = true;\n\n const css = `\n .cbg-host {\n position: relative;\n }\n body.cbg-host {\n /* body naturally acts as relative for background sizing if not explicitly positioned differently */\n }\n .cbg-inner {\n z-index: 2;\n position: relative;\n }\n .cbg-loader {\n height: 100%;\n width: 100%;\n position: absolute;\n top: 0;\n left: 0;\n background-image: var(--cbg-overlay-image, none);\n background-color: var(--cbg-overlay-color, transparent);\n opacity: 1;\n z-index: 1;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-loader {\n position: fixed;\n }\n .cbg-crossfade-el {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n z-index: 0;\n opacity: 0;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-crossfade-el {\n position: fixed;\n }\n `;\n\n const style = document.createElement('style');\n style.id = 'chameleon-global-styles';\n style.textContent = css;\n document.head.appendChild(style);\n }\n\n /**\n * Apply CSS custom properties to the host element.\n */\n #updateCSSVariables() {\n this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');\n this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);\n this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);\n\n if (this.#options.overlayImage) {\n this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);\n } else {\n this.#element.style.setProperty('--cbg-overlay-image', 'none');\n }\n\n this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));\n }\n\n /**\n * Wrap existing content and insert the overlay loader.\n */\n #buildDOM() {\n const uid = this.#uid;\n\n const wrapper = document.createElement('div');\n wrapper.id = `cbg-inner-${uid}`;\n wrapper.classList.add('cbg-inner');\n\n // Move all child nodes from the element into the wrapper\n while (this.#element.firstChild) {\n wrapper.appendChild(this.#element.firstChild);\n }\n\n const loader = document.createElement('div');\n loader.classList.add('cbg-loader', `cbg-loader-${uid}`);\n\n // Append wrapper and loader\n this.#element.appendChild(wrapper);\n this.#element.appendChild(loader);\n }\n\n // ---------------------------------------------------------------------------\n // Background Loading\n // ---------------------------------------------------------------------------\n\n /**\n * Route to the correct loader based on type.\n */\n #retrieveBackground() {\n if (this.#destroyed) return;\n\n if (this.#options.type === 'single') {\n const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;\n this.#loadBackground(singleSrc);\n } else if (this.#options.type === 'slider') {\n this.#startSlider();\n }\n }\n\n /**\n * Preload an image and apply it as the element's background.\n * Handles both string URLs and responsive image objects ({ url, srcset, sizes }).\n * @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.\n * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).\n * @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.\n * @returns {Promise<void>}\n */\n #loadBackground(srcConfig, isFirst = true, isCrossfade = false) {\n if (this.#destroyed) return Promise.resolve();\n\n return new Promise((resolve) => {\n const img = new Image();\n\n let urlStr = srcConfig;\n if (typeof srcConfig === 'object' && srcConfig.url) {\n urlStr = srcConfig.url;\n if (srcConfig.srcset) img.srcset = srcConfig.srcset;\n if (srcConfig.sizes) img.sizes = srcConfig.sizes;\n }\n\n if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {\n img.fetchPriority = this.#options.fetchPriority;\n }\n\n // Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers\n img.style.position = 'absolute';\n img.style.visibility = 'hidden';\n img.style.width = '0';\n img.style.height = '0';\n this.#element.appendChild(img);\n\n img.onload = () => {\n img.remove();\n if (this.#destroyed) return resolve();\n\n // `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`\n const finalUrl = img.currentSrc || urlStr;\n\n if (isCrossfade && !isFirst) {\n // --- Crossfade Transition Mode ---\n const crossfadeDiv = document.createElement('div');\n crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);\n crossfadeDiv.style.backgroundImage = `url(\"${finalUrl}\")`;\n\n // Insert right behind the loader (or directly at start of host)\n this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);\n\n // Trigger layout so the transition works\n void crossfadeDiv.offsetWidth;\n\n crossfadeDiv.style.opacity = '1';\n\n setTimeout(() => {\n if (this.#destroyed) return resolve();\n this.#element.style.backgroundImage = `url(\"${finalUrl}\")`;\n this.#element.style.backgroundSize = 'cover';\n this.#element.style.backgroundRepeat = 'no-repeat';\n crossfadeDiv.remove();\n resolve();\n }, this.#options.transitionDuration);\n\n } else {\n // --- Solid Transition Mode or First Load ---\n this.#element.style.backgroundImage = `url(\"${finalUrl}\")`;\n this.#element.style.backgroundSize = 'cover';\n this.#element.style.backgroundRepeat = 'no-repeat';\n\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = String(this.#options.minOverlay);\n }\n this.#hasLoadedFirstBackground = true;\n setTimeout(() => {\n resolve();\n }, this.#options.transitionDuration);\n }\n };\n\n img.onerror = () => {\n img.remove();\n console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);\n resolve(); // resolve anyway to keep logic flowing\n };\n\n img.src = urlStr;\n });\n }\n\n /**\n /**\n * Reload background with a fade-out → swap → fade-in cycle.\n * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.\n * @returns {Promise<void>}\n */\n reloadBackground(src) {\n if (this.#destroyed) return Promise.resolve();\n\n this.#transitionId++;\n\n if (src !== undefined) {\n if (this.#options.type === 'slider' && typeof src === 'string') {\n // If the user passes a comma-separated string, split it.\n this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];\n } else {\n this.#options.src = src;\n }\n }\n\n // Stop any running slider\n if (this.#sliderIntervalId !== null) {\n clearTimeout(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n\n if (this.#options.type === 'single') {\n const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;\n return this.#cycleSliderSlide(singleSrc, this.#transitionId);\n } else if (this.#options.type === 'slider') {\n this.#startSlider(this.#transitionId);\n return Promise.resolve();\n }\n }\n\n /**\n * Update options on the fly without destroying the instance.\n * @param {Object} newOptions - The new options to merge.\n */\n reloadOptions(newOptions) {\n if (this.#destroyed) return;\n\n const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);\n this.#options = { ...this.#options, ...normalized };\n\n this.#updateCSSVariables();\n // In v1/v2, reloadOptions did not restart the slider; the user called reloadBackground.\n // We intentionally do not auto-restart here to prevent concurrent race conditions.\n }\n\n // ---------------------------------------------------------------------------\n // Slider\n // ---------------------------------------------------------------------------\n\n /**\n * Start the background slideshow.\n * @param {number} tid - The transition ID\n */\n #startSlider(tid = this.#transitionId) {\n if (this.#destroyed) return;\n\n const sources = this.#options.src;\n if (!Array.isArray(sources) || sources.length === 0) {\n console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');\n return;\n }\n\n this.#currentSlideIndex = 0;\n\n if (this.#hasLoadedFirstBackground) {\n // Instance already has a background, crossfade smoothly\n this.#cycleSliderSlide(sources[this.#currentSlideIndex], tid).then(() => {\n if (this.#destroyed || this.#transitionId !== tid) return;\n this.#currentSlideIndex = 1;\n if (sources.length > 1) this.#startSliderLoop();\n });\n } else {\n // First load, load immediately without solid color flash\n this.#loadBackground(sources[this.#currentSlideIndex], true, this.#options.transitionMode === 'crossfade').then(() => {\n if (this.#destroyed || this.#transitionId !== tid) return;\n this.#hasLoadedFirstBackground = true;\n this.#currentSlideIndex = 1;\n if (sources.length > 1) this.#startSliderLoop();\n });\n }\n }\n\n #startSliderLoop() {\n if (this.#destroyed || this.#isPaused) return;\n\n // Parse as integers to prevent string concatenation bugs if users passed strings\n const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;\n\n this.#sliderIntervalId = setTimeout(() => {\n if (this.#destroyed || this.#isPaused) {\n this.#sliderIntervalId = null;\n return;\n }\n\n const tid = this.#transitionId;\n this.#cycleSliderSlide(this.#options.src[this.#currentSlideIndex], tid).then(() => {\n if (this.#destroyed || this.#isPaused || this.#transitionId !== tid) {\n this.#sliderIntervalId = null;\n return;\n }\n\n this.#currentSlideIndex++;\n\n if (this.#currentSlideIndex >= this.#options.src.length) {\n if (this.#options.sliderLoop) {\n this.#currentSlideIndex = 0;\n } else {\n this.#sliderIntervalId = null;\n return;\n }\n }\n\n this.#startSliderLoop();\n });\n }, sDuration);\n }\n\n /**\n * Cycle to a specific slide without resetting the slider instance.\n * @param {string|ChameleonImageConfig} src - The image URL/config to load.\n * @param {number} tid - The transition ID\n * @returns {Promise<void>}\n */\n #cycleSliderSlide(src, tid = this.#transitionId) {\n if (this.#destroyed) return Promise.resolve();\n\n return new Promise(resolve => {\n if (this.#options.transitionMode === 'solid') {\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = '1';\n }\n\n setTimeout(() => {\n if (this.#destroyed || this.#transitionId !== tid) return resolve();\n this.#loadBackground(src, false, false).then(resolve);\n }, this.#options.transitionDuration);\n } else {\n // Crossfade mode triggers immediately\n this.#loadBackground(src, false, true).then(resolve);\n }\n });\n }\n\n // ---------------------------------------------------------------------------\n // Static Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Convert legacy snake_case option keys to camelCase.\n * @param {Object} opts\n * @returns {Object}\n */\n static #normalizeOptions(opts) {\n const result = {};\n for (const [key, value] of Object.entries(opts)) {\n const alias = ChameleonBackgrounds.#ALIASES[key];\n result[alias ?? key] = value;\n }\n return result;\n }\n\n /**\n * Resolve a CSS selector string or HTMLElement to an HTMLElement.\n * @param {string|HTMLElement} el\n * @returns {HTMLElement}\n */\n static #resolveElement(el) {\n if (el instanceof HTMLElement) return el;\n if (typeof el === 'string') {\n if (el === 'body') return document.body;\n const found = document.querySelector(el);\n if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);\n return /** @type {HTMLElement} */ (found);\n }\n throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);\n }\n\n /**\n * Generate a short unique ID for scoping styles.\n * @returns {string}\n */\n static #generateUID() {\n // Prefer crypto.randomUUID where available, otherwise fall back\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID().slice(0, 8);\n }\n // Fallback: random alphanumeric string\n return Array.from({ length: 8 }, () =>\n 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))\n ).join('');\n }\n}\n\nexport default ChameleonBackgrounds;\nexport { ChameleonBackgrounds };\n"],"names":[],"sourceRoot":""}
@@ -1,2 +1,2 @@
1
1
  /*! For license information please see chameleon-backgrounds.min.js.LICENSE.txt */
2
- !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ChameleonBackgrounds=t():e.ChameleonBackgrounds=t()}(this,()=>(()=>{"use strict";const e={d:(t,i)=>{if(Array.isArray(i))for(var s=0;s<i.length;){var o=i[s++],r=i[s++];e.o(t,o)?0===r&&s++:0===r?Object.defineProperty(t,o,{enumerable:!0,value:i[s++]}):Object.defineProperty(t,o,{enumerable:!0,get:r})}else for(var o in i)e.o(i,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:i[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};let t={};class i{static#e=Object.freeze({element:"body",type:"single",src:"",overlayColor:"#0f1e25",overlayImage:null,minOverlay:0,transitionDuration:2e3,sliderDuration:8e3,sliderLoop:!1,fetchPriority:"auto",lazyLoad:!0,transitionMode:"solid",respectReducedMotion:!1});static#t=Object.freeze({transition_duration:"transitionDuration",slider_duration:"sliderDuration",slider_loop:"sliderLoop",min_overlay:"minOverlay",overlay_color:"overlayColor",overlay_image:"overlayImage",fetch_priority:"fetchPriority",lazy_load:"lazyLoad",transition_mode:"transitionMode",respect_reduced_motion:"respectReducedMotion"});static#i=!1;#s;#o;#r;#n;#l=null;#d=0;#a=!1;#c=null;#h=!1;constructor(e={}){const t=i.#u(e);this.#s={...i.#e,...t},this.#r=i.#y(),this.#o=i.#p(this.#s.element),this.#n=this.#o.innerHTML,this.#m()}destroy(){if(this.#h)return;this.#h=!0,null!==this.#l&&(clearInterval(this.#l),this.#l=null),this.#c&&(this.#c.disconnect(),this.#c=null),this.#o.classList.remove(`cbg-host-${this.#r}`,"cbg-host");const e=this.#o.querySelector(`#cbg-inner-${this.#r}`);if(e&&e.parentNode===this.#o){for(;e.firstChild;)this.#o.insertBefore(e.firstChild,e);this.#o.removeChild(e)}const t=this.#o.querySelector(`.cbg-loader-${this.#r}`);t&&t.remove(),this.#o.querySelectorAll(`.cbg-crossfade-${this.#r}`).forEach(e=>e.remove()),this.#o.style.backgroundImage="",this.#o.style.removeProperty("--cbg-duration"),this.#o.style.removeProperty("--cbg-overlay-color"),this.#o.style.removeProperty("--cbg-overlay-image"),this.#o.style.removeProperty("--cbg-min-overlay")}get options(){return{...this.#s}}#m(){this.#g(),this.#v(),this.#b(),this.#s.respectReducedMotion&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches&&(this.#a=!0),this.#s.lazyLoad&&"IntersectionObserver"in window?(this.#c=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(this.#c.disconnect(),this.#c=null,this.#f())})}),this.#c.observe(this.#o)):this.#o.matches("body")?queueMicrotask(()=>this.#f()):"complete"===document.readyState?this.#f():window.addEventListener("load",()=>this.#f(),{once:!0})}pause(){this.#h||"slider"!==this.#s.type||(this.#a=!0,null!==this.#l&&(clearInterval(this.#l),this.#l=null))}play(){this.#h||"slider"!==this.#s.type||(this.#a=!1,null===this.#l&&this.#I())}#g(){if(i.#i)return;i.#i=!0;const e=document.createElement("style");e.id="chameleon-global-styles",e.textContent="\n .cbg-host {\n position: relative;\n }\n body.cbg-host {\n /* body naturally acts as relative for background sizing if not explicitly positioned differently */\n }\n .cbg-inner {\n z-index: 2;\n position: relative;\n }\n .cbg-loader {\n height: 100%;\n width: 100%;\n position: absolute;\n top: 0;\n left: 0;\n background-image: var(--cbg-overlay-image, none);\n background-color: var(--cbg-overlay-color, transparent);\n opacity: 1;\n z-index: 1;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-loader {\n position: fixed;\n }\n .cbg-crossfade-el {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n z-index: 0;\n opacity: 0;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-crossfade-el {\n position: fixed;\n }\n ",document.head.appendChild(e)}#v(){this.#o.classList.add(`cbg-host-${this.#r}`,"cbg-host"),this.#o.style.setProperty("--cbg-duration",this.#s.transitionDuration/1e3+"s"),this.#o.style.setProperty("--cbg-overlay-color",this.#s.overlayColor),this.#s.overlayImage?this.#o.style.setProperty("--cbg-overlay-image",`url(${this.#s.overlayImage})`):this.#o.style.setProperty("--cbg-overlay-image","none"),this.#o.style.setProperty("--cbg-min-overlay",String(this.#s.minOverlay))}#b(){const e=this.#r,t=document.createElement("div");for(t.id=`cbg-inner-${e}`,t.classList.add("cbg-inner");this.#o.firstChild;)t.appendChild(this.#o.firstChild);const i=document.createElement("div");i.classList.add("cbg-loader",`cbg-loader-${e}`),this.#o.appendChild(t),this.#o.appendChild(i)}#f(){if(!this.#h)if("single"===this.#s.type){const e=Array.isArray(this.#s.src)?this.#s.src[0]:this.#s.src;this.#S(e)}else"slider"===this.#s.type&&this.#k()}#S(e,t=!0,i=!1){return this.#h?Promise.resolve():new Promise(s=>{const o=new Image;let r=e;"object"==typeof e&&e.url&&(r=e.url,e.srcset&&(o.srcset=e.srcset),e.sizes&&(o.sizes=e.sizes)),t&&"fetchPriority"in o&&"auto"!==this.#s.fetchPriority&&(o.fetchPriority=this.#s.fetchPriority),o.style.position="absolute",o.style.visibility="hidden",o.style.width="0",o.style.height="0",this.#o.appendChild(o),o.onload=()=>{if(o.remove(),this.#h)return s();const e=o.currentSrc||r;if(i&&!t){const t=document.createElement("div");t.classList.add("cbg-crossfade-el",`cbg-crossfade-${this.#r}`),t.style.backgroundImage=`url("${e}")`,this.#o.insertBefore(t,this.#o.firstChild),t.offsetWidth,t.style.opacity="1",setTimeout(()=>{if(this.#h)return s();this.#o.style.backgroundImage=`url("${e}")`,this.#o.style.backgroundSize="cover",this.#o.style.backgroundRepeat="no-repeat",t.remove(),s()},this.#s.transitionDuration)}else{this.#o.style.backgroundImage=`url("${e}")`,this.#o.style.backgroundSize="cover",this.#o.style.backgroundRepeat="no-repeat";const t=this.#o.querySelector(`.cbg-loader-${this.#r}`);t&&(t.style.opacity=String(this.#s.minOverlay)),s()}},o.onerror=()=>{o.remove(),console.warn("[ChameleonBackgrounds] Failed to load image:",e),s()},o.src=r})}reloadBackground(e){if(this.#h)return Promise.resolve();void 0!==e&&("slider"===this.#s.type&&"string"==typeof e?this.#s.src=e.includes(",")?e.split(",").map(e=>e.trim()):[e]:this.#s.src=e),null!==this.#l&&(clearInterval(this.#l),this.#l=null);const t=this.#o.querySelector(`.cbg-loader-${this.#r}`);return t&&"solid"===this.#s.transitionMode&&(t.style.opacity="1"),new Promise(e=>{const t="solid"===this.#s.transitionMode?this.#s.transitionDuration:0;setTimeout(()=>{if(this.#h)return e();if("single"===this.#s.type){const t=Array.isArray(this.#s.src)?this.#s.src[0]:this.#s.src;this.#S(t,!1,"crossfade"===this.#s.transitionMode).then(e)}else"slider"===this.#s.type&&(this.#k(),e())},t)})}reloadOptions(e){if(this.#h)return;const t=i.#u(e);this.#s={...this.#s,...t},this.#v(),"slider"!==this.#s.type||this.#a||this.#l||this.#k()}#k(){if(this.#h)return;const e=this.#s.src;Array.isArray(e)&&0!==e.length?(this.#d=0,this.#S(e[this.#d]).then(()=>{this.#h||(this.#d=1,1!==e.length&&this.#I())})):console.warn("[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.")}#I(){const e=this.#s.src,t=(parseInt(this.#s.sliderDuration,10)||8e3)+(parseInt(this.#s.transitionDuration,10)||2e3)*("solid"===this.#s.transitionMode?2:1);this.#l=setInterval(()=>{if(this.#h||this.#a)return clearInterval(this.#l),void(this.#l=null);this.#P(e[this.#d]),this.#d++,this.#d>=e.length&&(this.#s.sliderLoop?this.#d=0:(clearInterval(this.#l),this.#l=null))},t)}#P(e){if(!this.#h)if("solid"===this.#s.transitionMode){const t=this.#o.querySelector(`.cbg-loader-${this.#r}`);t&&(t.style.opacity="1"),setTimeout(()=>{this.#h||this.#S(e,!1,!1)},this.#s.transitionDuration)}else this.#S(e,!1,!0)}static#u(e){const t={};for(const[s,o]of Object.entries(e))t[i.#t[s]??s]=o;return t}static#p(e){if(e instanceof HTMLElement)return e;if("string"==typeof e){if("body"===e)return document.body;const t=document.querySelector(e);if(!t)throw new Error(`[ChameleonBackgrounds] Element not found: ${e}`);return t}throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${e}`)}static#y(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID().slice(0,8):Array.from({length:8},()=>"abcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(36*Math.random()))).join("")}}const s=i;return e.d(t,["default",0,s]),t=t.default,t})());
2
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ChameleonBackgrounds=t():e.ChameleonBackgrounds=t()}(this,()=>(()=>{"use strict";const e={d:(t,i)=>{if(Array.isArray(i))for(var s=0;s<i.length;){var r=i[s++],o=i[s++];e.o(t,r)?0===o&&s++:0===o?Object.defineProperty(t,r,{enumerable:!0,value:i[s++]}):Object.defineProperty(t,r,{enumerable:!0,get:o})}else for(var r in i)e.o(i,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:i[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};let t={};class i{static#e=Object.freeze({element:"body",type:"single",src:"",overlayColor:"#0f1e25",overlayImage:null,minOverlay:0,transitionDuration:2e3,sliderDuration:8e3,sliderLoop:!1,fetchPriority:"auto",lazyLoad:!0,transitionMode:"solid",respectReducedMotion:!1});static#t=Object.freeze({transition_duration:"transitionDuration",slider_duration:"sliderDuration",slider_loop:"sliderLoop",min_overlay:"minOverlay",overlay_color:"overlayColor",overlay_image:"overlayImage",fetch_priority:"fetchPriority",lazy_load:"lazyLoad",transition_mode:"transitionMode",respect_reduced_motion:"respectReducedMotion"});static#i=!1;#s;#r;#o;#n;#l=null;#d=0;#a=!1;#c=0;#h=!1;#u=null;#y=!1;constructor(e={}){const t=i.#p(e);this.#s={...i.#e,...t},this.#o=i.#m(),this.#r=i.#g(this.#s.element),this.#n=this.#r.innerHTML,this.#b()}destroy(){if(this.#y)return;this.#y=!0,null!==this.#l&&(clearTimeout(this.#l),this.#l=null),this.#u&&(this.#u.disconnect(),this.#u=null),this.#r.classList.remove(`cbg-host-${this.#o}`,"cbg-host");const e=this.#r.querySelector(`#cbg-inner-${this.#o}`);if(e&&e.parentNode===this.#r){for(;e.firstChild;)this.#r.insertBefore(e.firstChild,e);this.#r.removeChild(e)}const t=this.#r.querySelector(`.cbg-loader-${this.#o}`);t&&t.remove(),this.#r.querySelectorAll(`.cbg-crossfade-${this.#o}`).forEach(e=>e.remove()),this.#r.style.backgroundImage="",this.#r.style.removeProperty("--cbg-duration"),this.#r.style.removeProperty("--cbg-overlay-color"),this.#r.style.removeProperty("--cbg-overlay-image"),this.#r.style.removeProperty("--cbg-min-overlay")}get options(){return{...this.#s}}#b(){this.#v(),this.#f(),this.#I(),this.#s.respectReducedMotion&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches&&(this.#h=!0),this.#s.lazyLoad&&"IntersectionObserver"in window?(this.#u=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(this.#u.disconnect(),this.#u=null,this.#S())})}),this.#u.observe(this.#r)):this.#r.matches("body")?queueMicrotask(()=>this.#S()):"complete"===document.readyState?this.#S():window.addEventListener("load",()=>this.#S(),{once:!0})}pause(){this.#y||"slider"!==this.#s.type||(this.#h=!0,null!==this.#l&&(clearTimeout(this.#l),this.#l=null))}play(){this.#y||"slider"!==this.#s.type||(this.#h=!1,null===this.#l&&this.#k())}#v(){if(i.#i)return;i.#i=!0;const e=document.createElement("style");e.id="chameleon-global-styles",e.textContent="\n .cbg-host {\n position: relative;\n }\n body.cbg-host {\n /* body naturally acts as relative for background sizing if not explicitly positioned differently */\n }\n .cbg-inner {\n z-index: 2;\n position: relative;\n }\n .cbg-loader {\n height: 100%;\n width: 100%;\n position: absolute;\n top: 0;\n left: 0;\n background-image: var(--cbg-overlay-image, none);\n background-color: var(--cbg-overlay-color, transparent);\n opacity: 1;\n z-index: 1;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-loader {\n position: fixed;\n }\n .cbg-crossfade-el {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n z-index: 0;\n opacity: 0;\n transition: opacity var(--cbg-duration, 2s) ease;\n pointer-events: none;\n }\n body.cbg-host > .cbg-crossfade-el {\n position: fixed;\n }\n ",document.head.appendChild(e)}#f(){this.#r.classList.add(`cbg-host-${this.#o}`,"cbg-host"),this.#r.style.setProperty("--cbg-duration",this.#s.transitionDuration/1e3+"s"),this.#r.style.setProperty("--cbg-overlay-color",this.#s.overlayColor),this.#s.overlayImage?this.#r.style.setProperty("--cbg-overlay-image",`url(${this.#s.overlayImage})`):this.#r.style.setProperty("--cbg-overlay-image","none"),this.#r.style.setProperty("--cbg-min-overlay",String(this.#s.minOverlay))}#I(){const e=this.#o,t=document.createElement("div");for(t.id=`cbg-inner-${e}`,t.classList.add("cbg-inner");this.#r.firstChild;)t.appendChild(this.#r.firstChild);const i=document.createElement("div");i.classList.add("cbg-loader",`cbg-loader-${e}`),this.#r.appendChild(t),this.#r.appendChild(i)}#S(){if(!this.#y)if("single"===this.#s.type){const e=Array.isArray(this.#s.src)?this.#s.src[0]:this.#s.src;this.#P(e)}else"slider"===this.#s.type&&this.#L()}#P(e,t=!0,i=!1){return this.#y?Promise.resolve():new Promise(s=>{const r=new Image;let o=e;"object"==typeof e&&e.url&&(o=e.url,e.srcset&&(r.srcset=e.srcset),e.sizes&&(r.sizes=e.sizes)),t&&"fetchPriority"in r&&"auto"!==this.#s.fetchPriority&&(r.fetchPriority=this.#s.fetchPriority),r.style.position="absolute",r.style.visibility="hidden",r.style.width="0",r.style.height="0",this.#r.appendChild(r),r.onload=()=>{if(r.remove(),this.#y)return s();const e=r.currentSrc||o;if(i&&!t){const t=document.createElement("div");t.classList.add("cbg-crossfade-el",`cbg-crossfade-${this.#o}`),t.style.backgroundImage=`url("${e}")`,this.#r.insertBefore(t,this.#r.firstChild),t.offsetWidth,t.style.opacity="1",setTimeout(()=>{if(this.#y)return s();this.#r.style.backgroundImage=`url("${e}")`,this.#r.style.backgroundSize="cover",this.#r.style.backgroundRepeat="no-repeat",t.remove(),s()},this.#s.transitionDuration)}else{this.#r.style.backgroundImage=`url("${e}")`,this.#r.style.backgroundSize="cover",this.#r.style.backgroundRepeat="no-repeat";const t=this.#r.querySelector(`.cbg-loader-${this.#o}`);t&&(t.style.opacity=String(this.#s.minOverlay)),this.#a=!0,setTimeout(()=>{s()},this.#s.transitionDuration)}},r.onerror=()=>{r.remove(),console.warn("[ChameleonBackgrounds] Failed to load image:",e),s()},r.src=o})}reloadBackground(e){if(this.#y)return Promise.resolve();if(this.#c++,void 0!==e&&("slider"===this.#s.type&&"string"==typeof e?this.#s.src=e.includes(",")?e.split(",").map(e=>e.trim()):[e]:this.#s.src=e),null!==this.#l&&(clearTimeout(this.#l),this.#l=null),"single"===this.#s.type){const e=Array.isArray(this.#s.src)?this.#s.src[0]:this.#s.src;return this.#C(e,this.#c)}return"slider"===this.#s.type?(this.#L(this.#c),Promise.resolve()):void 0}reloadOptions(e){if(this.#y)return;const t=i.#p(e);this.#s={...this.#s,...t},this.#f()}#L(e=this.#c){if(this.#y)return;const t=this.#s.src;Array.isArray(t)&&0!==t.length?(this.#d=0,this.#a?this.#C(t[this.#d],e).then(()=>{this.#y||this.#c!==e||(this.#d=1,t.length>1&&this.#k())}):this.#P(t[this.#d],!0,"crossfade"===this.#s.transitionMode).then(()=>{this.#y||this.#c!==e||(this.#a=!0,this.#d=1,t.length>1&&this.#k())})):console.warn("[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.")}#k(){if(this.#y||this.#h)return;const e=parseInt(this.#s.sliderDuration,10)||8e3;this.#l=setTimeout(()=>{if(this.#y||this.#h)return void(this.#l=null);const e=this.#c;this.#C(this.#s.src[this.#d],e).then(()=>{if(this.#y||this.#h||this.#c!==e)this.#l=null;else{if(this.#d++,this.#d>=this.#s.src.length){if(!this.#s.sliderLoop)return void(this.#l=null);this.#d=0}this.#k()}})},e)}#C(e,t=this.#c){return this.#y?Promise.resolve():new Promise(i=>{if("solid"===this.#s.transitionMode){const s=this.#r.querySelector(`.cbg-loader-${this.#o}`);s&&(s.style.opacity="1"),setTimeout(()=>{if(this.#y||this.#c!==t)return i();this.#P(e,!1,!1).then(i)},this.#s.transitionDuration)}else this.#P(e,!1,!0).then(i)})}static#p(e){const t={};for(const[s,r]of Object.entries(e))t[i.#t[s]??s]=r;return t}static#g(e){if(e instanceof HTMLElement)return e;if("string"==typeof e){if("body"===e)return document.body;const t=document.querySelector(e);if(!t)throw new Error(`[ChameleonBackgrounds] Element not found: ${e}`);return t}throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${e}`)}static#m(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID().slice(0,8):Array.from({length:8},()=>"abcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(36*Math.random()))).join("")}}const s=i;return e.d(t,["default",0,s]),t=t.default,t})());
@@ -14,7 +14,7 @@
14
14
  * |___/
15
15
  *
16
16
  * @module ChameleonBackgrounds
17
- * @version 3.0.0
17
+ * @version 3.0.1
18
18
  * @author Lennart van Ballegoij (https://weblenn.com/)
19
19
  * @license MIT
20
20
  * @see https://github.com/WebLenn/ChameleonBackgrounds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chameleon-backgrounds",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "A zero-dependency JavaScript library to dynamically load background images with elegant fade-in transitions and slideshow support.",
5
5
  "type": "module",
6
6
  "main": "dist/chameleon-backgrounds.js",
@@ -14,7 +14,7 @@
14
14
  * |___/
15
15
  *
16
16
  * @module ChameleonBackgrounds
17
- * @version 3.0.0
17
+ * @version 3.0.1
18
18
  * @author Lennart van Ballegoij (https://weblenn.com/)
19
19
  * @license MIT
20
20
  * @see https://github.com/WebLenn/ChameleonBackgrounds
@@ -97,6 +97,12 @@ class ChameleonBackgrounds {
97
97
  /** @type {number} */
98
98
  #currentSlideIndex = 0;
99
99
 
100
+ /** @type {boolean} */
101
+ #hasLoadedFirstBackground = false;
102
+
103
+ /** @type {number} */
104
+ #transitionId = 0;
105
+
100
106
  /** @type {boolean} */
101
107
  #isPaused = false;
102
108
 
@@ -134,7 +140,7 @@ class ChameleonBackgrounds {
134
140
 
135
141
  // Stop any running slider
136
142
  if (this.#sliderIntervalId !== null) {
137
- clearInterval(this.#sliderIntervalId);
143
+ clearTimeout(this.#sliderIntervalId);
138
144
  this.#sliderIntervalId = null;
139
145
  }
140
146
 
@@ -230,7 +236,7 @@ class ChameleonBackgrounds {
230
236
  if (this.#destroyed || this.#options.type !== 'slider') return;
231
237
  this.#isPaused = true;
232
238
  if (this.#sliderIntervalId !== null) {
233
- clearInterval(this.#sliderIntervalId);
239
+ clearTimeout(this.#sliderIntervalId);
234
240
  this.#sliderIntervalId = null;
235
241
  }
236
242
  }
@@ -440,7 +446,10 @@ class ChameleonBackgrounds {
440
446
  if (loader) {
441
447
  loader.style.opacity = String(this.#options.minOverlay);
442
448
  }
443
- resolve();
449
+ this.#hasLoadedFirstBackground = true;
450
+ setTimeout(() => {
451
+ resolve();
452
+ }, this.#options.transitionDuration);
444
453
  }
445
454
  };
446
455
 
@@ -463,6 +472,8 @@ class ChameleonBackgrounds {
463
472
  reloadBackground(src) {
464
473
  if (this.#destroyed) return Promise.resolve();
465
474
 
475
+ this.#transitionId++;
476
+
466
477
  if (src !== undefined) {
467
478
  if (this.#options.type === 'slider' && typeof src === 'string') {
468
479
  // If the user passes a comma-separated string, split it.
@@ -474,30 +485,17 @@ class ChameleonBackgrounds {
474
485
 
475
486
  // Stop any running slider
476
487
  if (this.#sliderIntervalId !== null) {
477
- clearInterval(this.#sliderIntervalId);
488
+ clearTimeout(this.#sliderIntervalId);
478
489
  this.#sliderIntervalId = null;
479
490
  }
480
491
 
481
- // For solid mode, we fade the loader up. For crossfade, we don't.
482
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
483
- if (loader && this.#options.transitionMode === 'solid') {
484
- loader.style.opacity = '1';
492
+ if (this.#options.type === 'single') {
493
+ const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
494
+ return this.#cycleSliderSlide(singleSrc, this.#transitionId);
495
+ } else if (this.#options.type === 'slider') {
496
+ this.#startSlider(this.#transitionId);
497
+ return Promise.resolve();
485
498
  }
486
-
487
- return new Promise((resolve) => {
488
- const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;
489
- setTimeout(() => {
490
- if (this.#destroyed) return resolve();
491
-
492
- if (this.#options.type === 'single') {
493
- const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
494
- this.#loadBackground(singleSrc, false, this.#options.transitionMode === 'crossfade').then(resolve);
495
- } else if (this.#options.type === 'slider') {
496
- this.#startSlider();
497
- resolve();
498
- }
499
- }, waitTime);
500
- });
501
499
  }
502
500
 
503
501
  /**
@@ -511,10 +509,8 @@ class ChameleonBackgrounds {
511
509
  this.#options = { ...this.#options, ...normalized };
512
510
 
513
511
  this.#updateCSSVariables();
514
- // Restart slider if needed
515
- if (this.#options.type === 'slider' && !this.#isPaused && !this.#sliderIntervalId) {
516
- this.#startSlider();
517
- }
512
+ // In v1/v2, reloadOptions did not restart the slider; the user called reloadBackground.
513
+ // We intentionally do not auto-restart here to prevent concurrent race conditions.
518
514
  }
519
515
 
520
516
  // ---------------------------------------------------------------------------
@@ -523,8 +519,9 @@ class ChameleonBackgrounds {
523
519
 
524
520
  /**
525
521
  * Start the background slideshow.
522
+ * @param {number} tid - The transition ID
526
523
  */
527
- #startSlider() {
524
+ #startSlider(tid = this.#transitionId) {
528
525
  if (this.#destroyed) return;
529
526
 
530
527
  const sources = this.#options.src;
@@ -533,68 +530,86 @@ class ChameleonBackgrounds {
533
530
  return;
534
531
  }
535
532
 
536
- // Load the first slide immediately
537
533
  this.#currentSlideIndex = 0;
538
- this.#loadBackground(sources[this.#currentSlideIndex]).then(() => {
539
- if (this.#destroyed) return;
540
-
541
- this.#currentSlideIndex = 1;
542
- if (sources.length === 1) return; // only one slide, nothing to rotate
543
534
 
544
- this.#startSliderLoop();
545
- });
535
+ if (this.#hasLoadedFirstBackground) {
536
+ // Instance already has a background, crossfade smoothly
537
+ this.#cycleSliderSlide(sources[this.#currentSlideIndex], tid).then(() => {
538
+ if (this.#destroyed || this.#transitionId !== tid) return;
539
+ this.#currentSlideIndex = 1;
540
+ if (sources.length > 1) this.#startSliderLoop();
541
+ });
542
+ } else {
543
+ // First load, load immediately without solid color flash
544
+ this.#loadBackground(sources[this.#currentSlideIndex], true, this.#options.transitionMode === 'crossfade').then(() => {
545
+ if (this.#destroyed || this.#transitionId !== tid) return;
546
+ this.#hasLoadedFirstBackground = true;
547
+ this.#currentSlideIndex = 1;
548
+ if (sources.length > 1) this.#startSliderLoop();
549
+ });
550
+ }
546
551
  }
547
552
 
548
553
  #startSliderLoop() {
549
- const sources = this.#options.src;
554
+ if (this.#destroyed || this.#isPaused) return;
550
555
 
551
556
  // Parse as integers to prevent string concatenation bugs if users passed strings
552
557
  const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
553
- const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;
554
- const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);
555
558
 
556
- this.#sliderIntervalId = setInterval(() => {
559
+ this.#sliderIntervalId = setTimeout(() => {
557
560
  if (this.#destroyed || this.#isPaused) {
558
- clearInterval(this.#sliderIntervalId);
559
561
  this.#sliderIntervalId = null;
560
562
  return;
561
563
  }
562
564
 
563
- this.#cycleSliderSlide(sources[this.#currentSlideIndex]);
564
- this.#currentSlideIndex++;
565
-
566
- if (this.#currentSlideIndex >= sources.length) {
567
- if (this.#options.sliderLoop) {
568
- this.#currentSlideIndex = 0;
569
- } else {
570
- clearInterval(this.#sliderIntervalId);
565
+ const tid = this.#transitionId;
566
+ this.#cycleSliderSlide(this.#options.src[this.#currentSlideIndex], tid).then(() => {
567
+ if (this.#destroyed || this.#isPaused || this.#transitionId !== tid) {
571
568
  this.#sliderIntervalId = null;
569
+ return;
572
570
  }
573
- }
574
- }, interval);
571
+
572
+ this.#currentSlideIndex++;
573
+
574
+ if (this.#currentSlideIndex >= this.#options.src.length) {
575
+ if (this.#options.sliderLoop) {
576
+ this.#currentSlideIndex = 0;
577
+ } else {
578
+ this.#sliderIntervalId = null;
579
+ return;
580
+ }
581
+ }
582
+
583
+ this.#startSliderLoop();
584
+ });
585
+ }, sDuration);
575
586
  }
576
587
 
577
588
  /**
578
589
  * Cycle to a specific slide without resetting the slider instance.
579
590
  * @param {string|ChameleonImageConfig} src - The image URL/config to load.
591
+ * @param {number} tid - The transition ID
592
+ * @returns {Promise<void>}
580
593
  */
581
- #cycleSliderSlide(src) {
582
- if (this.#destroyed) return;
594
+ #cycleSliderSlide(src, tid = this.#transitionId) {
595
+ if (this.#destroyed) return Promise.resolve();
583
596
 
584
- if (this.#options.transitionMode === 'solid') {
585
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
586
- if (loader) {
587
- loader.style.opacity = '1';
588
- }
597
+ return new Promise(resolve => {
598
+ if (this.#options.transitionMode === 'solid') {
599
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
600
+ if (loader) {
601
+ loader.style.opacity = '1';
602
+ }
589
603
 
590
- setTimeout(() => {
591
- if (this.#destroyed) return;
592
- this.#loadBackground(src, false, false);
593
- }, this.#options.transitionDuration);
594
- } else {
595
- // Crossfade mode triggers immediately
596
- this.#loadBackground(src, false, true);
597
- }
604
+ setTimeout(() => {
605
+ if (this.#destroyed || this.#transitionId !== tid) return resolve();
606
+ this.#loadBackground(src, false, false).then(resolve);
607
+ }, this.#options.transitionDuration);
608
+ } else {
609
+ // Crossfade mode triggers immediately
610
+ this.#loadBackground(src, false, true).then(resolve);
611
+ }
612
+ });
598
613
  }
599
614
 
600
615
  // ---------------------------------------------------------------------------