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
|
|
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})());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chameleon-backgrounds",
|
|
3
|
-
"version": "3.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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
488
|
+
clearTimeout(this.#sliderIntervalId);
|
|
478
489
|
this.#sliderIntervalId = null;
|
|
479
490
|
}
|
|
480
491
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
//
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
loader
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|