chameleon-backgrounds 2.1.2 → 3.0.0

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,551 +1,743 @@
1
1
  (function webpackUniversalModuleDefinition(root, factory) {
2
- if(typeof exports === 'object' && typeof module === 'object')
3
- module.exports = factory();
4
- else if(typeof define === 'function' && define.amd)
5
- define([], factory);
6
- else if(typeof exports === 'object')
7
- exports["ChameleonBackgrounds"] = factory();
8
- else
9
- root["ChameleonBackgrounds"] = factory();
2
+ if (typeof exports === 'object' && typeof module === 'object')
3
+ module.exports = factory();
4
+ else if (typeof define === 'function' && define.amd)
5
+ define([], factory);
6
+ else if (typeof exports === 'object')
7
+ exports["ChameleonBackgrounds"] = factory();
8
+ else
9
+ root["ChameleonBackgrounds"] = factory();
10
10
  })(this, () => {
11
- return /******/ (() => { // webpackBootstrap
11
+ return /******/ (() => { // webpackBootstrap
12
12
  /******/ "use strict";
13
13
  /******/ // The require scope
14
14
  /******/ const __webpack_require__ = {};
15
- /******/
15
+ /******/
16
16
  /************************************************************************/
17
17
  /******/ /* webpack/runtime/define property getters */
18
18
  /******/ (() => {
19
19
  /******/ // define getter/value functions for harmony exports
20
20
  /******/ __webpack_require__.d = (exports, definition) => {
21
- /******/ if(Array.isArray(definition)) {
21
+ /******/ if (Array.isArray(definition)) {
22
22
  /******/ var i = 0;
23
- /******/ while(i < definition.length) {
23
+ /******/ while (i < definition.length) {
24
24
  /******/ var key = definition[i++];
25
25
  /******/ var binding = definition[i++];
26
- /******/ if(!__webpack_require__.o(exports, key)) {
27
- /******/ if(binding === 0) {
26
+ /******/ if (!__webpack_require__.o(exports, key)) {
27
+ /******/ if (binding === 0) {
28
28
  /******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
29
- /******/ } else {
29
+ /******/
30
+ } else {
30
31
  /******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
31
- /******/ }
32
- /******/ } else if(binding === 0) { i++; }
33
- /******/ }
34
- /******/ } else {
35
- /******/ for(var key in definition) {
36
- /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
32
+ /******/
33
+ }
34
+ /******/
35
+ } else if (binding === 0) { i++; }
36
+ /******/
37
+ }
38
+ /******/
39
+ } else {
40
+ /******/ for (var key in definition) {
41
+ /******/ if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
37
42
  /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
38
- /******/ }
39
- /******/ }
40
- /******/ }
41
- /******/ };
42
- /******/ })();
43
- /******/
43
+ /******/
44
+ }
45
+ /******/
46
+ }
47
+ /******/
48
+ }
49
+ /******/
50
+ };
51
+ /******/
52
+ })();
53
+ /******/
44
54
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
45
55
  /******/ (() => {
46
56
  /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
47
- /******/ })();
48
- /******/
57
+ /******/
58
+ })();
59
+ /******/
49
60
  /******/ /* webpack/runtime/make namespace object */
50
61
  /******/ (() => {
51
62
  /******/ // define __esModule on exports
52
63
  /******/ __webpack_require__.r = (exports) => {
53
- /******/ if(Symbol.toStringTag) {
64
+ /******/ if (Symbol.toStringTag) {
54
65
  /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
55
- /******/ }
66
+ /******/
67
+ }
56
68
  /******/ Object.defineProperty(exports, '__esModule', { value: true });
57
- /******/ };
58
- /******/ })();
59
- /******/
60
- /************************************************************************/
61
- let __webpack_exports__ = {};
62
- /*!**************************************!*\
63
- !*** ./src/chameleon-backgrounds.js ***!
64
- \**************************************/
65
- __webpack_require__.r(__webpack_exports__);
69
+ /******/
70
+ };
71
+ /******/
72
+ })();
73
+ /******/
74
+ /************************************************************************/
75
+ let __webpack_exports__ = {};
76
+ /*!**************************************!*\
77
+ !*** ./src/chameleon-backgrounds.js ***!
78
+ \**************************************/
79
+ __webpack_require__.r(__webpack_exports__);
66
80
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
67
81
  /* harmony export */ ChameleonBackgrounds: () => (/* binding */ ChameleonBackgrounds),
68
82
  /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
69
- /* harmony export */ });
70
- /**
71
- * _____ _ _
72
- * / ____| | | |
73
- * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __
74
- * | | | '_ \ / _` | '_ ` _ \ / _ \ |/ _ \/ _ \| '_ \
75
- * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |
76
- * \_____|_| |_|\__,_|_| |_| |_|\___|_|\___|\___|_| |_|_
77
- * | _ \ | | | |
78
- * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___
79
- * | _ < / _` |/ __| |/ / _` | '__/ _ \| | | | '_ \ / _` / __|
80
- * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \__ \
81
- * |____/ \__,_|\___|_|\_\__, |_| \___/ \__,_|_| |_|\__,_|___/
82
- * __/ |
83
- * |___/
84
- *
85
- * @module ChameleonBackgrounds
86
- * @version 2.1.2
87
- * @author Lennart van Ballegoij (https://weblenn.com/)
88
- * @license MIT
89
- * @see https://github.com/WebLenn/ChameleonBackgrounds
90
- *
91
- * A zero-dependency JavaScript library to dynamically load background
92
- * images with elegant fade-in transitions and slideshow support.
93
- */
94
-
95
- /**
96
- * @typedef {Object} ChameleonOptions
97
- * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
98
- * @property {'single'|'slider'} type - Background mode.
99
- * @property {string|string[]} src - Image URL(s). String for 'single', array for 'slider'.
100
- * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
101
- * @property {string} [overlayImage] - Optional overlay pattern image URL.
102
- * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
103
- * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
104
- * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
105
- * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
106
- */
107
-
108
- class ChameleonBackgrounds {
109
- /** @type {ChameleonOptions} */
110
- static #DEFAULTS = Object.freeze({
111
- element: 'body',
112
- type: 'single',
113
- src: '',
114
- overlayColor: '#0f1e25',
115
- overlayImage: null,
116
- minOverlay: 0,
117
- transitionDuration: 2000,
118
- sliderDuration: 8000,
119
- sliderLoop: false,
120
- });
121
-
122
- // Legacy snake_case → camelCase alias map
123
- static #ALIASES = Object.freeze({
124
- transition_duration: 'transitionDuration',
125
- slider_duration: 'sliderDuration',
126
- slider_loop: 'sliderLoop',
127
- min_overlay: 'minOverlay',
128
- overlay_color: 'overlayColor',
129
- overlay_image: 'overlayImage',
130
- });
131
-
132
- /** @type {ChameleonOptions} */
133
- #options;
134
-
135
- /** @type {HTMLElement} */
136
- #element;
137
-
138
- /** @type {string} */
139
- #uid;
140
-
141
- /** @type {string} */
142
- #originalHTML;
143
-
144
- /** @type {HTMLStyleElement|null} */
145
- #styleElement = null;
146
-
147
- /** @type {number|null} */
148
- #sliderIntervalId = null;
149
-
150
- /** @type {boolean} */
151
- #destroyed = false;
152
-
153
- /**
154
- * Create a new ChameleonBackgrounds instance.
155
- * @param {Partial<ChameleonOptions>} [options]
156
- */
157
- constructor(options = {}) {
158
- const normalized = ChameleonBackgrounds.#normalizeOptions(options);
159
- this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
160
- this.#uid = ChameleonBackgrounds.#generateUID();
161
- this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
162
- this.#originalHTML = this.#element.innerHTML;
163
-
164
- this.#init();
165
- }
166
-
167
- // ---------------------------------------------------------------------------
168
- // Public API
169
- // ---------------------------------------------------------------------------
170
-
171
- /**
172
- * Clean up all injected DOM, styles, and intervals.
173
- * Restores the target element to its original state.
174
- */
175
- destroy() {
176
- if (this.#destroyed) return;
177
- this.#destroyed = true;
178
-
179
- // Stop any running slider
180
- if (this.#sliderIntervalId !== null) {
181
- clearInterval(this.#sliderIntervalId);
182
- this.#sliderIntervalId = null;
183
- }
83
+ /* harmony export */
84
+ });
85
+ /**
86
+ * _____ _ _
87
+ * / ____| | | |
88
+ * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __
89
+ * | | | '_ \ / _` | '_ ` _ \ / _ \ |/ _ \/ _ \| '_ \
90
+ * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |
91
+ * \_____|_| |_|\__,_|_| |_| |_|\___|_|\___|\___|_| |_|_
92
+ * | _ \ | | | |
93
+ * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___
94
+ * | _ < / _` |/ __| |/ / _` | '__/ _ \| | | | '_ \ / _` / __|
95
+ * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \__ \
96
+ * |____/ \__,_|\___|_|\_\__, |_| \___/ \__,_|_| |_|\__,_|___/
97
+ * __/ |
98
+ * |___/
99
+ *
100
+ * @module ChameleonBackgrounds
101
+ * @version 3.0.0
102
+ * @author Lennart van Ballegoij (https://weblenn.com/)
103
+ * @license MIT
104
+ * @see https://github.com/WebLenn/ChameleonBackgrounds
105
+ *
106
+ * A zero-dependency JavaScript library to dynamically load background
107
+ * images with elegant fade-in transitions and slideshow support.
108
+ */
109
+
110
+ /**
111
+ * @typedef {Object} ChameleonImageConfig
112
+ * @property {string} url - The fallback URL.
113
+ * @property {string} [srcset] - Optional srcset.
114
+ * @property {string} [sizes] - Optional sizes.
115
+ *
116
+ * @typedef {Object} ChameleonOptions
117
+ * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
118
+ * @property {'single'|'slider'} type - Background mode.
119
+ * @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
120
+ * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
121
+ * @property {string} [overlayImage] - Optional overlay pattern image URL.
122
+ * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
123
+ * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
124
+ * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
125
+ * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
126
+ * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
127
+ * @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.
128
+ * @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).
129
+ */
130
+
131
+ class ChameleonBackgrounds {
132
+ /** @type {ChameleonOptions} */
133
+ static #DEFAULTS = Object.freeze({
134
+ element: 'body',
135
+ type: 'single',
136
+ src: '',
137
+ overlayColor: '#0f1e25',
138
+ overlayImage: null,
139
+ minOverlay: 0,
140
+ transitionDuration: 2000,
141
+ sliderDuration: 8000,
142
+ sliderLoop: false,
143
+ fetchPriority: 'auto',
144
+ lazyLoad: true,
145
+ transitionMode: 'solid',
146
+ respectReducedMotion: false,
147
+ });
148
+
149
+ // Legacy snake_case → camelCase alias map
150
+ static #ALIASES = Object.freeze({
151
+ transition_duration: 'transitionDuration',
152
+ slider_duration: 'sliderDuration',
153
+ slider_loop: 'sliderLoop',
154
+ min_overlay: 'minOverlay',
155
+ overlay_color: 'overlayColor',
156
+ overlay_image: 'overlayImage',
157
+ fetch_priority: 'fetchPriority',
158
+ lazy_load: 'lazyLoad',
159
+ transition_mode: 'transitionMode',
160
+ respect_reduced_motion: 'respectReducedMotion',
161
+ });
162
+
163
+ /** @type {boolean} */
164
+ static #globalStylesInjected = false;
165
+
166
+ /** @type {ChameleonOptions} */
167
+ #options;
168
+
169
+ /** @type {HTMLElement} */
170
+ #element;
171
+
172
+ /** @type {string} */
173
+ #uid;
174
+
175
+ /** @type {string} */
176
+ #originalHTML;
177
+
178
+ /** @type {number|null} */
179
+ #sliderIntervalId = null;
180
+
181
+ /** @type {number} */
182
+ #currentSlideIndex = 0;
183
+
184
+ /** @type {boolean} */
185
+ #isPaused = false;
186
+
187
+ /** @type {IntersectionObserver|null} */
188
+ #observer = null;
189
+
190
+ /** @type {boolean} */
191
+ #destroyed = false;
192
+
193
+ /**
194
+ * Create a new ChameleonBackgrounds instance.
195
+ * @param {Partial<ChameleonOptions>} [options]
196
+ */
197
+ constructor(options = {}) {
198
+ const normalized = ChameleonBackgrounds.#normalizeOptions(options);
199
+ this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
200
+ this.#uid = ChameleonBackgrounds.#generateUID();
201
+ this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
202
+ this.#originalHTML = this.#element.innerHTML;
203
+
204
+ this.#init();
205
+ }
184
206
 
185
- // Remove injected style element
186
- if (this.#styleElement?.parentNode) {
187
- this.#styleElement.parentNode.removeChild(this.#styleElement);
188
- this.#styleElement = null;
189
- }
207
+ // ---------------------------------------------------------------------------
208
+ // Public API
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Clean up all injected DOM, styles, and intervals.
213
+ * Restores the target element to its original state.
214
+ */
215
+ destroy() {
216
+ if (this.#destroyed) return;
217
+ this.#destroyed = true;
190
218
 
191
- // Unwrap the original content instead of resetting innerHTML
192
- const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
193
- if (wrapper && wrapper.parentNode === this.#element) {
194
- while (wrapper.firstChild) {
195
- this.#element.insertBefore(wrapper.firstChild, wrapper);
219
+ // Stop any running slider
220
+ if (this.#sliderIntervalId !== null) {
221
+ clearInterval(this.#sliderIntervalId);
222
+ this.#sliderIntervalId = null;
223
+ }
224
+
225
+ if (this.#observer) {
226
+ this.#observer.disconnect();
227
+ this.#observer = null;
228
+ }
229
+
230
+ // Clean up dynamic classes on host
231
+ this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
232
+
233
+ // Unwrap the original content instead of resetting innerHTML
234
+ const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
235
+ if (wrapper && wrapper.parentNode === this.#element) {
236
+ while (wrapper.firstChild) {
237
+ this.#element.insertBefore(wrapper.firstChild, wrapper);
238
+ }
239
+ this.#element.removeChild(wrapper);
240
+ }
241
+
242
+ // Remove loader
243
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
244
+ if (loader) {
245
+ loader.remove();
246
+ }
247
+
248
+ // Remove temporary crossfade elements
249
+ const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
250
+ crossfadeElements.forEach(el => el.remove());
251
+
252
+ // Remove inline CSS Variables and background-image
253
+ this.#element.style.backgroundImage = '';
254
+ this.#element.style.removeProperty('--cbg-duration');
255
+ this.#element.style.removeProperty('--cbg-overlay-color');
256
+ this.#element.style.removeProperty('--cbg-overlay-image');
257
+ this.#element.style.removeProperty('--cbg-min-overlay');
196
258
  }
197
- this.#element.removeChild(wrapper);
198
- }
199
259
 
200
- // Remove loader
201
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
202
- if (loader) {
203
- loader.remove();
204
- }
260
+ /**
261
+ * The resolved options (read-only copy).
262
+ * @returns {ChameleonOptions}
263
+ */
264
+ get options() {
265
+ return { ...this.#options };
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Initialization
270
+ // ---------------------------------------------------------------------------
271
+
272
+ #init() {
273
+ this.#injectGlobalStyles();
274
+ this.#updateCSSVariables();
275
+ this.#buildDOM();
205
276
 
206
- // Remove inline background-image
207
- this.#element.style.backgroundImage = '';
208
- }
209
-
210
- /**
211
- * The resolved options (read-only copy).
212
- * @returns {ChameleonOptions}
213
- */
214
- get options() {
215
- return { ...this.#options };
216
- }
217
-
218
- // ---------------------------------------------------------------------------
219
- // Initialization
220
- // ---------------------------------------------------------------------------
221
-
222
- #init() {
223
- this.#injectStyles();
224
- this.#buildDOM();
225
-
226
- // If the element is NOT <body>, wait for window load to start loading images.
227
- // For <body>, the loader is already visible, so we start immediately to avoid
228
- // the re-execution issue the legacy lib had with body scripts.
229
- if (this.#element.matches('body')) {
230
- // Use a microtask so the DOM changes above settle first.
231
- queueMicrotask(() => this.#retrieveBackground());
232
- } else {
233
- if (document.readyState === 'complete') {
234
- this.#retrieveBackground();
235
- } else {
236
- window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
277
+ // Check for prefers-reduced-motion to auto-pause slider if enabled
278
+ if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
279
+ this.#isPaused = true;
280
+ }
281
+
282
+ if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
283
+ this.#observer = new IntersectionObserver((entries) => {
284
+ entries.forEach((entry) => {
285
+ if (entry.isIntersecting) {
286
+ this.#observer.disconnect();
287
+ this.#observer = null;
288
+ this.#retrieveBackground();
289
+ }
290
+ });
291
+ });
292
+ this.#observer.observe(this.#element);
293
+ } else {
294
+ if (this.#element.matches('body')) {
295
+ queueMicrotask(() => this.#retrieveBackground());
296
+ } else {
297
+ if (document.readyState === 'complete') {
298
+ this.#retrieveBackground();
299
+ } else {
300
+ window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
301
+ }
302
+ }
303
+ }
237
304
  }
238
- }
239
- }
240
-
241
- // ---------------------------------------------------------------------------
242
- // DOM Construction
243
- // ---------------------------------------------------------------------------
244
-
245
- /**
246
- * Inject a scoped <style> element into the <head>.
247
- */
248
- #injectStyles() {
249
- const uid = this.#uid;
250
- const selector = this.#options.element === 'body' || this.#element.matches('body')
251
- ? 'body'
252
- : this.#options.element;
253
- const duration = this.#options.transitionDuration / 1000; // ms → s
254
- const position = this.#element.matches('body') ? 'fixed' : 'absolute';
255
- const overlayBg = this.#options.overlayImage
256
- ? `url(${this.#options.overlayImage})`
257
- : 'none';
258
-
259
- const css = `
260
- ${typeof selector === 'string' ? selector : `.cbg-host-${uid}`} {
261
- position: relative;
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Play / Pause API
308
+ // ---------------------------------------------------------------------------
309
+
310
+ /**
311
+ * Pause the slideshow.
312
+ */
313
+ pause() {
314
+ if (this.#destroyed || this.#options.type !== 'slider') return;
315
+ this.#isPaused = true;
316
+ if (this.#sliderIntervalId !== null) {
317
+ clearInterval(this.#sliderIntervalId);
318
+ this.#sliderIntervalId = null;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Resume the slideshow.
324
+ */
325
+ play() {
326
+ if (this.#destroyed || this.#options.type !== 'slider') return;
327
+ this.#isPaused = false;
328
+ // Only restart if not currently running
329
+ if (this.#sliderIntervalId === null) {
330
+ this.#startSliderLoop();
331
+ }
262
332
  }
263
333
 
264
- #cbg-inner-${uid} {
334
+ // ---------------------------------------------------------------------------
335
+ // DOM Construction
336
+ // ---------------------------------------------------------------------------
337
+
338
+ /**
339
+ * Inject global styles once.
340
+ */
341
+ #injectGlobalStyles() {
342
+ if (ChameleonBackgrounds.#globalStylesInjected) return;
343
+ ChameleonBackgrounds.#globalStylesInjected = true;
344
+
345
+ const css = `
346
+ .cbg-host {
347
+ position: relative;
348
+ }
349
+ body.cbg-host {
350
+ /* body naturally acts as relative for background sizing if not explicitly positioned differently */
351
+ }
352
+ .cbg-inner {
265
353
  z-index: 2;
266
354
  position: relative;
267
355
  }
268
-
269
- .cbg-loader-${uid} {
356
+ .cbg-loader {
270
357
  height: 100%;
271
358
  width: 100%;
272
- position: ${position};
273
- background-image: ${overlayBg};
274
- background-color: ${this.#options.overlayColor};
359
+ position: absolute;
360
+ top: 0;
361
+ left: 0;
362
+ background-image: var(--cbg-overlay-image, none);
363
+ background-color: var(--cbg-overlay-color, transparent);
275
364
  opacity: 1;
276
365
  z-index: 1;
277
- transition: opacity ${duration}s ease;
366
+ transition: opacity var(--cbg-duration, 2s) ease;
367
+ pointer-events: none;
368
+ }
369
+ body.cbg-host > .cbg-loader {
370
+ position: fixed;
371
+ }
372
+ .cbg-crossfade-el {
373
+ position: absolute;
278
374
  top: 0;
279
375
  left: 0;
376
+ height: 100%;
377
+ width: 100%;
378
+ background-size: cover;
379
+ background-position: center;
380
+ background-repeat: no-repeat;
381
+ z-index: 0;
382
+ opacity: 0;
383
+ transition: opacity var(--cbg-duration, 2s) ease;
384
+ pointer-events: none;
385
+ }
386
+ body.cbg-host > .cbg-crossfade-el {
387
+ position: fixed;
280
388
  }
281
389
  `;
282
390
 
283
- const style = document.createElement('style');
284
- style.dataset.chameleonUid = uid;
285
- style.textContent = css;
286
- document.head.appendChild(style);
287
- this.#styleElement = style;
288
- }
289
-
290
- /**
291
- * Wrap existing content and insert the overlay loader.
292
- */
293
- #buildDOM() {
294
- const uid = this.#uid;
295
-
296
- const wrapper = document.createElement('div');
297
- wrapper.id = `cbg-inner-${uid}`;
298
-
299
- // Move all child nodes from the element into the wrapper
300
- while (this.#element.firstChild) {
301
- wrapper.appendChild(this.#element.firstChild);
302
- }
391
+ const style = document.createElement('style');
392
+ style.id = 'chameleon-global-styles';
393
+ style.textContent = css;
394
+ document.head.appendChild(style);
395
+ }
303
396
 
304
- const loader = document.createElement('div');
305
- loader.classList.add(`cbg-loader-${uid}`);
397
+ /**
398
+ * Apply CSS custom properties to the host element.
399
+ */
400
+ #updateCSSVariables() {
401
+ this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
402
+ this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
403
+ this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
404
+
405
+ if (this.#options.overlayImage) {
406
+ this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
407
+ } else {
408
+ this.#element.style.setProperty('--cbg-overlay-image', 'none');
409
+ }
306
410
 
307
- // Append wrapper and loader
308
- this.#element.appendChild(wrapper);
309
- this.#element.appendChild(loader);
310
- }
411
+ this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
412
+ }
311
413
 
312
- // ---------------------------------------------------------------------------
313
- // Background Loading
314
- // ---------------------------------------------------------------------------
414
+ /**
415
+ * Wrap existing content and insert the overlay loader.
416
+ */
417
+ #buildDOM() {
418
+ const uid = this.#uid;
315
419
 
316
- /**
317
- * Route to the correct loader based on type.
318
- */
319
- #retrieveBackground() {
320
- if (this.#destroyed) return;
420
+ const wrapper = document.createElement('div');
421
+ wrapper.id = `cbg-inner-${uid}`;
422
+ wrapper.classList.add('cbg-inner');
321
423
 
322
- if (this.#options.type === 'single') {
323
- this.#loadBackground(typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0]);
324
- } else if (this.#options.type === 'slider') {
325
- this.#startSlider();
326
- }
327
- }
424
+ // Move all child nodes from the element into the wrapper
425
+ while (this.#element.firstChild) {
426
+ wrapper.appendChild(this.#element.firstChild);
427
+ }
328
428
 
329
- /**
330
- * Preload an image and apply it as the element's background.
331
- * @param {string} src - Image URL to load.
332
- * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
333
- * @returns {Promise<void>}
334
- */
335
- #loadBackground(src, isFirst = true) {
336
- if (this.#destroyed) return Promise.resolve();
429
+ const loader = document.createElement('div');
430
+ loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
337
431
 
338
- return new Promise((resolve) => {
339
- const img = new Image();
432
+ // Append wrapper and loader
433
+ this.#element.appendChild(wrapper);
434
+ this.#element.appendChild(loader);
435
+ }
340
436
 
341
- img.onload = () => {
342
- if (this.#destroyed) return resolve();
437
+ // ---------------------------------------------------------------------------
438
+ // Background Loading
439
+ // ---------------------------------------------------------------------------
343
440
 
344
- this.#element.style.backgroundImage = `url(${src})`;
345
- this.#element.style.backgroundSize = 'cover';
441
+ /**
442
+ * Route to the correct loader based on type.
443
+ */
444
+ #retrieveBackground() {
445
+ if (this.#destroyed) return;
346
446
 
347
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
348
- if (loader) {
349
- loader.style.opacity = String(this.#options.minOverlay);
447
+ if (this.#options.type === 'single') {
448
+ const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
449
+ this.#loadBackground(singleSrc);
450
+ } else if (this.#options.type === 'slider') {
451
+ this.#startSlider();
350
452
  }
453
+ }
351
454
 
352
- resolve();
353
- };
455
+ /**
456
+ * Preload an image and apply it as the element's background.
457
+ * Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
458
+ * @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
459
+ * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
460
+ * @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
461
+ * @returns {Promise<void>}
462
+ */
463
+ #loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
464
+ if (this.#destroyed) return Promise.resolve();
465
+
466
+ return new Promise((resolve) => {
467
+ const img = new Image();
468
+
469
+ let urlStr = srcConfig;
470
+ if (typeof srcConfig === 'object' && srcConfig.url) {
471
+ urlStr = srcConfig.url;
472
+ if (srcConfig.srcset) img.srcset = srcConfig.srcset;
473
+ if (srcConfig.sizes) img.sizes = srcConfig.sizes;
474
+ }
354
475
 
355
- img.onerror = () => {
356
- console.warn(`[ChameleonBackgrounds] Failed to load image: ${src}`);
357
- resolve();
358
- };
476
+ if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
477
+ img.fetchPriority = this.#options.fetchPriority;
478
+ }
359
479
 
360
- img.src = src;
361
- });
362
- }
363
-
364
- /**
365
- /**
366
- * Reload background with a fade-out → swap → fade-in cycle.
367
- * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
368
- * @returns {Promise<void>}
369
- */
370
- reloadBackground(src) {
371
- if (this.#destroyed) return Promise.resolve();
372
-
373
- if (src !== undefined) {
374
- if (this.#options.type === 'slider' && typeof src === 'string') {
375
- // If the user passes a comma-separated string, split it. Otherwise wrap it.
376
- this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
377
- } else {
378
- this.#options.src = src;
480
+ // Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
481
+ img.style.position = 'absolute';
482
+ img.style.visibility = 'hidden';
483
+ img.style.width = '0';
484
+ img.style.height = '0';
485
+ this.#element.appendChild(img);
486
+
487
+ img.onload = () => {
488
+ img.remove();
489
+ if (this.#destroyed) return resolve();
490
+
491
+ // `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
492
+ const finalUrl = img.currentSrc || urlStr;
493
+
494
+ if (isCrossfade && !isFirst) {
495
+ // --- Crossfade Transition Mode ---
496
+ const crossfadeDiv = document.createElement('div');
497
+ crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
498
+ crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
499
+
500
+ // Insert right behind the loader (or directly at start of host)
501
+ this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
502
+
503
+ // Trigger layout so the transition works
504
+ void crossfadeDiv.offsetWidth;
505
+
506
+ crossfadeDiv.style.opacity = '1';
507
+
508
+ setTimeout(() => {
509
+ if (this.#destroyed) return resolve();
510
+ this.#element.style.backgroundImage = `url("${finalUrl}")`;
511
+ this.#element.style.backgroundSize = 'cover';
512
+ this.#element.style.backgroundRepeat = 'no-repeat';
513
+ crossfadeDiv.remove();
514
+ resolve();
515
+ }, this.#options.transitionDuration);
516
+
517
+ } else {
518
+ // --- Solid Transition Mode or First Load ---
519
+ this.#element.style.backgroundImage = `url("${finalUrl}")`;
520
+ this.#element.style.backgroundSize = 'cover';
521
+ this.#element.style.backgroundRepeat = 'no-repeat';
522
+
523
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
524
+ if (loader) {
525
+ loader.style.opacity = String(this.#options.minOverlay);
526
+ }
527
+ resolve();
528
+ }
529
+ };
530
+
531
+ img.onerror = () => {
532
+ img.remove();
533
+ console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
534
+ resolve(); // resolve anyway to keep logic flowing
535
+ };
536
+
537
+ img.src = urlStr;
538
+ });
379
539
  }
380
- }
381
-
382
- // Stop any running slider
383
- if (this.#sliderIntervalId !== null) {
384
- clearInterval(this.#sliderIntervalId);
385
- this.#sliderIntervalId = null;
386
- }
387
540
 
388
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
389
- if (loader) {
390
- loader.style.opacity = '1';
391
- }
392
-
393
- return new Promise((resolve) => {
394
- setTimeout(() => {
395
- if (this.#destroyed) return resolve();
541
+ /**
542
+ /**
543
+ * Reload background with a fade-out → swap → fade-in cycle.
544
+ * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
545
+ * @returns {Promise<void>}
546
+ */
547
+ reloadBackground(src) {
548
+ if (this.#destroyed) return Promise.resolve();
549
+
550
+ if (src !== undefined) {
551
+ if (this.#options.type === 'slider' && typeof src === 'string') {
552
+ // If the user passes a comma-separated string, split it.
553
+ this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
554
+ } else {
555
+ this.#options.src = src;
556
+ }
557
+ }
396
558
 
397
- if (this.#options.type === 'single') {
398
- const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];
399
- this.#loadBackground(singleSrc, false).then(resolve);
400
- } else if (this.#options.type === 'slider') {
401
- this.#startSlider();
402
- resolve();
559
+ // Stop any running slider
560
+ if (this.#sliderIntervalId !== null) {
561
+ clearInterval(this.#sliderIntervalId);
562
+ this.#sliderIntervalId = null;
403
563
  }
404
- }, this.#options.transitionDuration);
405
- });
406
- }
407
-
408
- /**
409
- * Update options on the fly without destroying the instance.
410
- * @param {Object} newOptions - The new options to merge.
411
- */
412
- reloadOptions(newOptions) {
413
- if (this.#destroyed) return;
414
-
415
- const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
416
- this.#options = { ...this.#options, ...normalized };
417
-
418
- // Remove old styles
419
- if (this.#styleElement?.parentNode) {
420
- this.#styleElement.parentNode.removeChild(this.#styleElement);
421
- this.#styleElement = null;
422
- }
423
564
 
424
- // Inject updated styles
425
- this.#injectStyles();
426
- }
565
+ // For solid mode, we fade the loader up. For crossfade, we don't.
566
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
567
+ if (loader && this.#options.transitionMode === 'solid') {
568
+ loader.style.opacity = '1';
569
+ }
427
570
 
428
- // ---------------------------------------------------------------------------
429
- // Slider
430
- // ---------------------------------------------------------------------------
571
+ return new Promise((resolve) => {
572
+ const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;
573
+ setTimeout(() => {
574
+ if (this.#destroyed) return resolve();
575
+
576
+ if (this.#options.type === 'single') {
577
+ const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
578
+ this.#loadBackground(singleSrc, false, this.#options.transitionMode === 'crossfade').then(resolve);
579
+ } else if (this.#options.type === 'slider') {
580
+ this.#startSlider();
581
+ resolve();
582
+ }
583
+ }, waitTime);
584
+ });
585
+ }
431
586
 
432
- /**
433
- * Start the background slideshow.
434
- */
435
- #startSlider() {
436
- if (this.#destroyed) return;
587
+ /**
588
+ * Update options on the fly without destroying the instance.
589
+ * @param {Object} newOptions - The new options to merge.
590
+ */
591
+ reloadOptions(newOptions) {
592
+ if (this.#destroyed) return;
437
593
 
438
- const sources = this.#options.src;
439
- if (!Array.isArray(sources) || sources.length === 0) {
440
- console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
441
- return;
442
- }
594
+ const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
595
+ this.#options = { ...this.#options, ...normalized };
443
596
 
444
- // Load the first slide immediately
445
- let index = 0;
446
- this.#loadBackground(sources[index]).then(() => {
447
- if (this.#destroyed) return;
597
+ this.#updateCSSVariables();
598
+ // Restart slider if needed
599
+ if (this.#options.type === 'slider' && !this.#isPaused && !this.#sliderIntervalId) {
600
+ this.#startSlider();
601
+ }
602
+ }
448
603
 
449
- index = 1;
450
- if (sources.length === 1) return; // only one slide, nothing to rotate
604
+ // ---------------------------------------------------------------------------
605
+ // Slider
606
+ // ---------------------------------------------------------------------------
451
607
 
452
- const interval = this.#options.sliderDuration + this.#options.transitionDuration * 2;
608
+ /**
609
+ * Start the background slideshow.
610
+ */
611
+ #startSlider() {
612
+ if (this.#destroyed) return;
453
613
 
454
- this.#sliderIntervalId = setInterval(() => {
455
- if (this.#destroyed) {
456
- clearInterval(this.#sliderIntervalId);
614
+ const sources = this.#options.src;
615
+ if (!Array.isArray(sources) || sources.length === 0) {
616
+ console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
457
617
  return;
458
618
  }
459
619
 
460
- this.#cycleSliderSlide(sources[index]);
461
- index++;
620
+ // Load the first slide immediately
621
+ this.#currentSlideIndex = 0;
622
+ this.#loadBackground(sources[this.#currentSlideIndex]).then(() => {
623
+ if (this.#destroyed) return;
462
624
 
463
- if (index >= sources.length) {
464
- if (this.#options.sliderLoop) {
465
- index = 0;
466
- } else {
625
+ this.#currentSlideIndex = 1;
626
+ if (sources.length === 1) return; // only one slide, nothing to rotate
627
+
628
+ this.#startSliderLoop();
629
+ });
630
+ }
631
+
632
+ #startSliderLoop() {
633
+ const sources = this.#options.src;
634
+
635
+ // Parse as integers to prevent string concatenation bugs if users passed strings
636
+ const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
637
+ const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;
638
+ const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);
639
+
640
+ this.#sliderIntervalId = setInterval(() => {
641
+ if (this.#destroyed || this.#isPaused) {
467
642
  clearInterval(this.#sliderIntervalId);
468
643
  this.#sliderIntervalId = null;
644
+ return;
645
+ }
646
+
647
+ this.#cycleSliderSlide(sources[this.#currentSlideIndex]);
648
+ this.#currentSlideIndex++;
649
+
650
+ if (this.#currentSlideIndex >= sources.length) {
651
+ if (this.#options.sliderLoop) {
652
+ this.#currentSlideIndex = 0;
653
+ } else {
654
+ clearInterval(this.#sliderIntervalId);
655
+ this.#sliderIntervalId = null;
656
+ }
657
+ }
658
+ }, interval);
659
+ }
660
+
661
+ /**
662
+ * Cycle to a specific slide without resetting the slider instance.
663
+ * @param {string|ChameleonImageConfig} src - The image URL/config to load.
664
+ */
665
+ #cycleSliderSlide(src) {
666
+ if (this.#destroyed) return;
667
+
668
+ if (this.#options.transitionMode === 'solid') {
669
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
670
+ if (loader) {
671
+ loader.style.opacity = '1';
469
672
  }
673
+
674
+ setTimeout(() => {
675
+ if (this.#destroyed) return;
676
+ this.#loadBackground(src, false, false);
677
+ }, this.#options.transitionDuration);
678
+ } else {
679
+ // Crossfade mode triggers immediately
680
+ this.#loadBackground(src, false, true);
470
681
  }
471
- }, interval);
472
- });
473
- }
474
-
475
- /**
476
- * Cycle to a specific slide without resetting the slider instance.
477
- * @param {string} src - The image URL to load.
478
- */
479
- #cycleSliderSlide(src) {
480
- if (this.#destroyed) return;
481
-
482
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
483
- if (loader) {
484
- loader.style.opacity = '1';
485
- }
682
+ }
486
683
 
487
- setTimeout(() => {
488
- if (this.#destroyed) return;
489
- this.#loadBackground(src, false);
490
- }, this.#options.transitionDuration);
491
- }
492
-
493
- // ---------------------------------------------------------------------------
494
- // Static Helpers
495
- // ---------------------------------------------------------------------------
496
-
497
- /**
498
- * Convert legacy snake_case option keys to camelCase.
499
- * @param {Object} opts
500
- * @returns {Object}
501
- */
502
- static #normalizeOptions(opts) {
503
- const result = {};
504
- for (const [key, value] of Object.entries(opts)) {
505
- const alias = ChameleonBackgrounds.#ALIASES[key];
506
- result[alias ?? key] = value;
507
- }
508
- return result;
509
- }
510
-
511
- /**
512
- * Resolve a CSS selector string or HTMLElement to an HTMLElement.
513
- * @param {string|HTMLElement} el
514
- * @returns {HTMLElement}
515
- */
516
- static #resolveElement(el) {
517
- if (el instanceof HTMLElement) return el;
518
- if (typeof el === 'string') {
519
- if (el === 'body') return document.body;
520
- const found = document.querySelector(el);
521
- if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
522
- return /** @type {HTMLElement} */ (found);
523
- }
524
- throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
525
- }
526
-
527
- /**
528
- * Generate a short unique ID for scoping styles.
529
- * @returns {string}
530
- */
531
- static #generateUID() {
532
- // Prefer crypto.randomUUID where available, otherwise fall back
533
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
534
- return crypto.randomUUID().slice(0, 8);
684
+ // ---------------------------------------------------------------------------
685
+ // Static Helpers
686
+ // ---------------------------------------------------------------------------
687
+
688
+ /**
689
+ * Convert legacy snake_case option keys to camelCase.
690
+ * @param {Object} opts
691
+ * @returns {Object}
692
+ */
693
+ static #normalizeOptions(opts) {
694
+ const result = {};
695
+ for (const [key, value] of Object.entries(opts)) {
696
+ const alias = ChameleonBackgrounds.#ALIASES[key];
697
+ result[alias ?? key] = value;
698
+ }
699
+ return result;
700
+ }
701
+
702
+ /**
703
+ * Resolve a CSS selector string or HTMLElement to an HTMLElement.
704
+ * @param {string|HTMLElement} el
705
+ * @returns {HTMLElement}
706
+ */
707
+ static #resolveElement(el) {
708
+ if (el instanceof HTMLElement) return el;
709
+ if (typeof el === 'string') {
710
+ if (el === 'body') return document.body;
711
+ const found = document.querySelector(el);
712
+ if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
713
+ return /** @type {HTMLElement} */ (found);
714
+ }
715
+ throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
716
+ }
717
+
718
+ /**
719
+ * Generate a short unique ID for scoping styles.
720
+ * @returns {string}
721
+ */
722
+ static #generateUID() {
723
+ // Prefer crypto.randomUUID where available, otherwise fall back
724
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
725
+ return crypto.randomUUID().slice(0, 8);
726
+ }
727
+ // Fallback: random alphanumeric string
728
+ return Array.from({ length: 8 }, () =>
729
+ 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
730
+ ).join('');
731
+ }
535
732
  }
536
- // Fallback: random alphanumeric string
537
- return Array.from({ length: 8 }, () =>
538
- 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
539
- ).join('');
540
- }
541
- }
542
733
 
543
734
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ChameleonBackgrounds);
544
735
 
545
736
 
546
- __webpack_exports__ = __webpack_exports__["default"];
737
+ __webpack_exports__ = __webpack_exports__["default"];
547
738
  /******/ return __webpack_exports__;
548
- /******/ })()
549
- ;
739
+ /******/
740
+ })()
741
+ ;
550
742
  });
551
743
  //# sourceMappingURL=chameleon-backgrounds.js.map