chameleon-backgrounds 2.1.0 → 2.1.2

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,3 +1,72 @@
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();
10
+ })(this, () => {
11
+ return /******/ (() => { // webpackBootstrap
12
+ /******/ "use strict";
13
+ /******/ // The require scope
14
+ /******/ const __webpack_require__ = {};
15
+ /******/
16
+ /************************************************************************/
17
+ /******/ /* webpack/runtime/define property getters */
18
+ /******/ (() => {
19
+ /******/ // define getter/value functions for harmony exports
20
+ /******/ __webpack_require__.d = (exports, definition) => {
21
+ /******/ if(Array.isArray(definition)) {
22
+ /******/ var i = 0;
23
+ /******/ while(i < definition.length) {
24
+ /******/ var key = definition[i++];
25
+ /******/ var binding = definition[i++];
26
+ /******/ if(!__webpack_require__.o(exports, key)) {
27
+ /******/ if(binding === 0) {
28
+ /******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
29
+ /******/ } else {
30
+ /******/ 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)) {
37
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
38
+ /******/ }
39
+ /******/ }
40
+ /******/ }
41
+ /******/ };
42
+ /******/ })();
43
+ /******/
44
+ /******/ /* webpack/runtime/hasOwnProperty shorthand */
45
+ /******/ (() => {
46
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
47
+ /******/ })();
48
+ /******/
49
+ /******/ /* webpack/runtime/make namespace object */
50
+ /******/ (() => {
51
+ /******/ // define __esModule on exports
52
+ /******/ __webpack_require__.r = (exports) => {
53
+ /******/ if(Symbol.toStringTag) {
54
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
55
+ /******/ }
56
+ /******/ 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__);
66
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
67
+ /* harmony export */ ChameleonBackgrounds: () => (/* binding */ ChameleonBackgrounds),
68
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
69
+ /* harmony export */ });
1
70
  /**
2
71
  * _____ _ _
3
72
  * / ____| | | |
@@ -13,42 +82,32 @@
13
82
  * __/ |
14
83
  * |___/
15
84
  *
16
- * ChameleonBackgrounds v2.0.0 — UMD Bundle
17
- * A zero-dependency JavaScript library to dynamically load background
18
- * images with elegant fade-in transitions and slideshow support.
19
- *
85
+ * @module ChameleonBackgrounds
86
+ * @version 2.1.2
20
87
  * @author Lennart van Ballegoij (https://weblenn.com/)
21
88
  * @license MIT
22
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.
23
93
  */
24
- (function (root, factory) {
25
- if (typeof define === 'function' && define.amd) {
26
- // AMD
27
- define([], factory);
28
- } else if (typeof module === 'object' && module.exports) {
29
- // CommonJS
30
- module.exports = factory();
31
- } else {
32
- // Browser global
33
- root.ChameleonBackgrounds = factory();
34
- }
35
- }(typeof self !== 'undefined' ? self : this, function () {
36
- 'use strict';
37
94
 
38
- /**
39
- * @typedef {Object} ChameleonOptions
40
- * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
41
- * @property {'single'|'slider'} type - Background mode.
42
- * @property {string|string[]} src - Image URL(s). String for 'single', array for 'slider'.
43
- * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
44
- * @property {string} [overlayImage] - Optional overlay pattern image URL.
45
- * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
46
- * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
47
- * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
48
- * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
49
- */
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
+ */
50
107
 
51
- const DEFAULTS = Object.freeze({
108
+ class ChameleonBackgrounds {
109
+ /** @type {ChameleonOptions} */
110
+ static #DEFAULTS = Object.freeze({
52
111
  element: 'body',
53
112
  type: 'single',
54
113
  src: '',
@@ -60,7 +119,8 @@
60
119
  sliderLoop: false,
61
120
  });
62
121
 
63
- const ALIASES = Object.freeze({
122
+ // Legacy snake_case → camelCase alias map
123
+ static #ALIASES = Object.freeze({
64
124
  transition_duration: 'transitionDuration',
65
125
  slider_duration: 'sliderDuration',
66
126
  slider_loop: 'sliderLoop',
@@ -69,288 +129,423 @@
69
129
  overlay_image: 'overlayImage',
70
130
  });
71
131
 
72
- /**
73
- * Convert legacy snake_case option keys to camelCase.
74
- */
75
- function normalizeOptions(opts) {
76
- const result = {};
77
- for (const key of Object.keys(opts)) {
78
- const alias = ALIASES[key];
79
- result[alias !== undefined ? alias : key] = opts[key];
80
- }
81
- return result;
82
- }
132
+ /** @type {ChameleonOptions} */
133
+ #options;
83
134
 
84
- /**
85
- * Resolve a CSS selector string or HTMLElement to an HTMLElement.
86
- */
87
- function resolveElement(el) {
88
- if (el instanceof HTMLElement) return el;
89
- if (typeof el === 'string') {
90
- if (el === 'body') return document.body;
91
- var found = document.querySelector(el);
92
- if (!found) throw new Error('[ChameleonBackgrounds] Element not found: ' + el);
93
- return found;
94
- }
95
- throw new TypeError('[ChameleonBackgrounds] Invalid element: ' + el);
96
- }
135
+ /** @type {HTMLElement} */
136
+ #element;
97
137
 
98
- /**
99
- * Generate a short unique ID for scoping styles.
100
- */
101
- function generateUID() {
102
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
103
- return crypto.randomUUID().slice(0, 8);
104
- }
105
- return Array.from({ length: 8 }, function () {
106
- return 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36));
107
- }).join('');
108
- }
138
+ /** @type {string} */
139
+ #uid;
109
140
 
110
- // -------------------------------------------------------------------------
111
- // Constructor
112
- // -------------------------------------------------------------------------
141
+ /** @type {string} */
142
+ #originalHTML;
113
143
 
114
- function ChameleonBackgrounds(options) {
115
- if (!(this instanceof ChameleonBackgrounds)) {
116
- return new ChameleonBackgrounds(options);
117
- }
144
+ /** @type {HTMLStyleElement|null} */
145
+ #styleElement = null;
118
146
 
119
- var normalized = normalizeOptions(options || {});
120
- this._options = {};
121
- for (var k in DEFAULTS) {
122
- if (DEFAULTS.hasOwnProperty(k)) {
123
- this._options[k] = normalized[k] !== undefined ? normalized[k] : DEFAULTS[k];
124
- }
125
- }
147
+ /** @type {number|null} */
148
+ #sliderIntervalId = null;
126
149
 
127
- this._uid = generateUID();
128
- this._element = resolveElement(this._options.element);
129
- this._originalHTML = this._element.innerHTML;
130
- this._styleElement = null;
131
- this._sliderIntervalId = null;
132
- this._destroyed = false;
150
+ /** @type {boolean} */
151
+ #destroyed = false;
133
152
 
134
- this._init();
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();
135
165
  }
136
166
 
137
- // -------------------------------------------------------------------------
167
+ // ---------------------------------------------------------------------------
138
168
  // Public API
139
- // -------------------------------------------------------------------------
169
+ // ---------------------------------------------------------------------------
140
170
 
141
171
  /**
142
172
  * Clean up all injected DOM, styles, and intervals.
143
173
  * Restores the target element to its original state.
144
174
  */
145
- ChameleonBackgrounds.prototype.destroy = function () {
146
- if (this._destroyed) return;
147
- this._destroyed = true;
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
+ }
148
184
 
149
- if (this._sliderIntervalId !== null) {
150
- clearInterval(this._sliderIntervalId);
151
- this._sliderIntervalId = null;
185
+ // Remove injected style element
186
+ if (this.#styleElement?.parentNode) {
187
+ this.#styleElement.parentNode.removeChild(this.#styleElement);
188
+ this.#styleElement = null;
152
189
  }
153
190
 
154
- if (this._styleElement && this._styleElement.parentNode) {
155
- this._styleElement.parentNode.removeChild(this._styleElement);
156
- this._styleElement = null;
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);
196
+ }
197
+ this.#element.removeChild(wrapper);
157
198
  }
158
199
 
159
- this._element.innerHTML = this._originalHTML;
160
- this._element.style.backgroundImage = '';
161
- };
200
+ // Remove loader
201
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
202
+ if (loader) {
203
+ loader.remove();
204
+ }
205
+
206
+ // Remove inline background-image
207
+ this.#element.style.backgroundImage = '';
208
+ }
162
209
 
163
210
  /**
164
- * Readonly copy of the resolved options.
211
+ * The resolved options (read-only copy).
212
+ * @returns {ChameleonOptions}
165
213
  */
166
- ChameleonBackgrounds.prototype.getOptions = function () {
167
- var copy = {};
168
- for (var k in this._options) {
169
- if (this._options.hasOwnProperty(k)) {
170
- copy[k] = this._options[k];
171
- }
172
- }
173
- return copy;
174
- };
175
-
176
- // -------------------------------------------------------------------------
177
- // Initialization (private by convention)
178
- // -------------------------------------------------------------------------
214
+ get options() {
215
+ return { ...this.#options };
216
+ }
179
217
 
180
- ChameleonBackgrounds.prototype._init = function () {
181
- this._injectStyles();
182
- this._buildDOM();
218
+ // ---------------------------------------------------------------------------
219
+ // Initialization
220
+ // ---------------------------------------------------------------------------
183
221
 
184
- var self = this;
222
+ #init() {
223
+ this.#injectStyles();
224
+ this.#buildDOM();
185
225
 
186
- if (this._element === document.body || this._element.matches('body')) {
187
- // Use a microtask so the DOM changes settle first
188
- Promise.resolve().then(function () { self._retrieveBackground(); });
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());
189
232
  } else {
190
233
  if (document.readyState === 'complete') {
191
- this._retrieveBackground();
234
+ this.#retrieveBackground();
192
235
  } else {
193
- window.addEventListener('load', function onLoad() {
194
- window.removeEventListener('load', onLoad);
195
- self._retrieveBackground();
196
- });
236
+ window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
197
237
  }
198
238
  }
199
- };
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // DOM Construction
243
+ // ---------------------------------------------------------------------------
200
244
 
201
- ChameleonBackgrounds.prototype._injectStyles = function () {
202
- var uid = this._uid;
203
- var selector = this._options.element === 'body' || this._element.matches('body')
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')
204
251
  ? 'body'
205
- : this._options.element;
206
- var duration = this._options.transitionDuration / 1000;
207
- var position = this._element.matches('body') ? 'fixed' : 'absolute';
208
- var overlayBg = this._options.overlayImage
209
- ? 'url(' + this._options.overlayImage + ')'
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})`
210
257
  : 'none';
211
258
 
212
- var selectorStr = typeof selector === 'string' ? selector : '.cbg-host-' + uid;
213
-
214
- var css =
215
- selectorStr + ' { position: relative; } ' +
216
- '#cbg-inner-' + uid + ' { z-index: 2; position: relative; } ' +
217
- '.cbg-loader-' + uid + ' { ' +
218
- 'height: 100%; width: 100%; ' +
219
- 'position: ' + position + '; ' +
220
- 'background-image: ' + overlayBg + '; ' +
221
- 'background-color: ' + this._options.overlayColor + '; ' +
222
- 'opacity: 1; z-index: 1; ' +
223
- 'transition: opacity ' + duration + 's ease; ' +
224
- 'top: 0; left: 0; ' +
225
- '}';
226
-
227
- var style = document.createElement('style');
228
- style.setAttribute('data-chameleon-uid', uid);
259
+ const css = `
260
+ ${typeof selector === 'string' ? selector : `.cbg-host-${uid}`} {
261
+ position: relative;
262
+ }
263
+
264
+ #cbg-inner-${uid} {
265
+ z-index: 2;
266
+ position: relative;
267
+ }
268
+
269
+ .cbg-loader-${uid} {
270
+ height: 100%;
271
+ width: 100%;
272
+ position: ${position};
273
+ background-image: ${overlayBg};
274
+ background-color: ${this.#options.overlayColor};
275
+ opacity: 1;
276
+ z-index: 1;
277
+ transition: opacity ${duration}s ease;
278
+ top: 0;
279
+ left: 0;
280
+ }
281
+ `;
282
+
283
+ const style = document.createElement('style');
284
+ style.dataset.chameleonUid = uid;
229
285
  style.textContent = css;
230
286
  document.head.appendChild(style);
231
- this._styleElement = style;
232
- };
287
+ this.#styleElement = style;
288
+ }
233
289
 
234
- ChameleonBackgrounds.prototype._buildDOM = function () {
235
- var uid = this._uid;
236
- var content = this._element.innerHTML;
290
+ /**
291
+ * Wrap existing content and insert the overlay loader.
292
+ */
293
+ #buildDOM() {
294
+ const uid = this.#uid;
237
295
 
238
- var wrapper = document.createElement('div');
239
- wrapper.id = 'cbg-inner-' + uid;
240
- wrapper.innerHTML = content;
296
+ const wrapper = document.createElement('div');
297
+ wrapper.id = `cbg-inner-${uid}`;
241
298
 
242
- var loader = document.createElement('div');
243
- loader.className = 'cbg-loader-' + uid;
299
+ // Move all child nodes from the element into the wrapper
300
+ while (this.#element.firstChild) {
301
+ wrapper.appendChild(this.#element.firstChild);
302
+ }
303
+
304
+ const loader = document.createElement('div');
305
+ loader.classList.add(`cbg-loader-${uid}`);
244
306
 
245
- this._element.innerHTML = '';
246
- this._element.appendChild(wrapper);
247
- this._element.appendChild(loader);
248
- };
307
+ // Append wrapper and loader
308
+ this.#element.appendChild(wrapper);
309
+ this.#element.appendChild(loader);
310
+ }
249
311
 
250
- // -------------------------------------------------------------------------
312
+ // ---------------------------------------------------------------------------
251
313
  // Background Loading
252
- // -------------------------------------------------------------------------
253
-
254
- ChameleonBackgrounds.prototype._retrieveBackground = function () {
255
- if (this._destroyed) return;
256
-
257
- if (this._options.type === 'single') {
258
- var src = typeof this._options.src === 'string'
259
- ? this._options.src
260
- : this._options.src[0];
261
- this._loadBackground(src);
262
- } else if (this._options.type === 'slider') {
263
- this._startSlider();
314
+ // ---------------------------------------------------------------------------
315
+
316
+ /**
317
+ * Route to the correct loader based on type.
318
+ */
319
+ #retrieveBackground() {
320
+ if (this.#destroyed) return;
321
+
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();
264
326
  }
265
- };
327
+ }
266
328
 
267
- ChameleonBackgrounds.prototype._loadBackground = function (src, callback) {
268
- if (this._destroyed) { if (callback) callback(); return; }
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();
269
337
 
270
- var self = this;
271
- var img = new Image();
338
+ return new Promise((resolve) => {
339
+ const img = new Image();
272
340
 
273
- img.onload = function () {
274
- if (self._destroyed) { if (callback) callback(); return; }
341
+ img.onload = () => {
342
+ if (this.#destroyed) return resolve();
275
343
 
276
- self._element.style.backgroundImage = 'url(' + src + ')';
277
- self._element.style.backgroundSize = 'cover';
344
+ this.#element.style.backgroundImage = `url(${src})`;
345
+ this.#element.style.backgroundSize = 'cover';
278
346
 
279
- var loader = self._element.querySelector('.cbg-loader-' + self._uid);
280
- if (loader) {
281
- loader.style.opacity = String(self._options.minOverlay);
282
- }
347
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
348
+ if (loader) {
349
+ loader.style.opacity = String(this.#options.minOverlay);
350
+ }
283
351
 
284
- if (callback) callback();
285
- };
352
+ resolve();
353
+ };
286
354
 
287
- img.onerror = function () {
288
- console.warn('[ChameleonBackgrounds] Failed to load image: ' + src);
289
- if (callback) callback();
290
- };
355
+ img.onerror = () => {
356
+ console.warn(`[ChameleonBackgrounds] Failed to load image: ${src}`);
357
+ resolve();
358
+ };
291
359
 
292
- img.src = src;
293
- };
360
+ img.src = src;
361
+ });
362
+ }
294
363
 
295
- ChameleonBackgrounds.prototype.reloadBackground = function (src, callback) {
296
- if (this._destroyed) { if (callback) callback(); return; }
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();
297
372
 
298
- var loader = this._element.querySelector('.cbg-loader-' + this._uid);
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;
379
+ }
380
+ }
381
+
382
+ // Stop any running slider
383
+ if (this.#sliderIntervalId !== null) {
384
+ clearInterval(this.#sliderIntervalId);
385
+ this.#sliderIntervalId = null;
386
+ }
387
+
388
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
299
389
  if (loader) {
300
390
  loader.style.opacity = '1';
301
391
  }
302
392
 
303
- var self = this;
304
- setTimeout(function () {
305
- if (self._destroyed) { if (callback) callback(); return; }
306
- self._loadBackground(src, callback);
307
- }, this._options.transitionDuration);
308
- };
393
+ return new Promise((resolve) => {
394
+ setTimeout(() => {
395
+ if (this.#destroyed) return resolve();
396
+
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();
403
+ }
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
+
424
+ // Inject updated styles
425
+ this.#injectStyles();
426
+ }
309
427
 
310
- // -------------------------------------------------------------------------
428
+ // ---------------------------------------------------------------------------
311
429
  // Slider
312
- // -------------------------------------------------------------------------
430
+ // ---------------------------------------------------------------------------
313
431
 
314
- ChameleonBackgrounds.prototype._startSlider = function () {
315
- if (this._destroyed) return;
432
+ /**
433
+ * Start the background slideshow.
434
+ */
435
+ #startSlider() {
436
+ if (this.#destroyed) return;
316
437
 
317
- var sources = this._options.src;
438
+ const sources = this.#options.src;
318
439
  if (!Array.isArray(sources) || sources.length === 0) {
319
440
  console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
320
441
  return;
321
442
  }
322
443
 
323
- var self = this;
324
- var index = 0;
325
-
326
- this._loadBackground(sources[index], function () {
327
- if (self._destroyed) return;
444
+ // Load the first slide immediately
445
+ let index = 0;
446
+ this.#loadBackground(sources[index]).then(() => {
447
+ if (this.#destroyed) return;
328
448
 
329
449
  index = 1;
330
- if (sources.length === 1) return;
450
+ if (sources.length === 1) return; // only one slide, nothing to rotate
331
451
 
332
- var interval = self._options.sliderDuration + self._options.transitionDuration * 2;
452
+ const interval = this.#options.sliderDuration + this.#options.transitionDuration * 2;
333
453
 
334
- self._sliderIntervalId = setInterval(function () {
335
- if (self._destroyed) {
336
- clearInterval(self._sliderIntervalId);
454
+ this.#sliderIntervalId = setInterval(() => {
455
+ if (this.#destroyed) {
456
+ clearInterval(this.#sliderIntervalId);
337
457
  return;
338
458
  }
339
459
 
340
- self.reloadBackground(sources[index]);
460
+ this.#cycleSliderSlide(sources[index]);
341
461
  index++;
342
462
 
343
463
  if (index >= sources.length) {
344
- if (self._options.sliderLoop) {
464
+ if (this.#options.sliderLoop) {
345
465
  index = 0;
346
466
  } else {
347
- clearInterval(self._sliderIntervalId);
348
- self._sliderIntervalId = null;
467
+ clearInterval(this.#sliderIntervalId);
468
+ this.#sliderIntervalId = null;
349
469
  }
350
470
  }
351
471
  }, interval);
352
472
  });
353
- };
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
+ }
486
+
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);
535
+ }
536
+ // Fallback: random alphanumeric string
537
+ return Array.from({ length: 8 }, () =>
538
+ 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
539
+ ).join('');
540
+ }
541
+ }
542
+
543
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ChameleonBackgrounds);
544
+
354
545
 
355
- return ChameleonBackgrounds;
356
- }));
546
+ __webpack_exports__ = __webpack_exports__["default"];
547
+ /******/ return __webpack_exports__;
548
+ /******/ })()
549
+ ;
550
+ });
551
+ //# sourceMappingURL=chameleon-backgrounds.js.map
@@ -0,0 +1 @@
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,0BAA0B;AACxC,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,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH,aAAa,kBAAkB;AAC/B;;AAEA,aAAa,aAAa;AAC1B;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,QAAQ;AACrB;;AAEA,aAAa,uBAAuB;AACpC;;AAEA,aAAa,aAAa;AAC1B;;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;AACA;;AAEA;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA,aAAa;AACb;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA,QAAQ;AACR,4EAA4E,YAAY;AACxF;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8DAA8D;AAC9D;AACA;AACA,eAAe,2BAA2B;AAC1C;;AAEA;AACA,QAAQ,uDAAuD,IAAI;AACnE;AACA;;AAEA,mBAAmB;AACnB;AACA;AACA;;AAEA,oBAAoB;AACpB;AACA;AACA,oBAAoB;AACpB,4BAA4B;AAC5B,4BAA4B;AAC5B;AACA;AACA,8BAA8B,SAAS;AACvC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,8BAA8B,IAAI;;AAElC;AACA;AACA;AACA;;AAEA;AACA,uCAAuC,IAAI;;AAE3C;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN;AACA;AACA;;AAEA;AACA;AACA,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,eAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA,qDAAqD,IAAI;AACzD;;AAEA,kEAAkE,UAAU;AAC5E;AACA;AACA;;AAEA;AACA;;AAEA;AACA,qEAAqE,IAAI;AACzE;AACA;;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,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;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;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,wCAAwC;;AAExC;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,YAAY;AACZ;AACA;AACA;AACA;AACA,OAAO;AACP,KAAK;AACL;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;;AAEA,8DAA8D,UAAU;AACxE;AACA;AACA;;AAEA;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 2.1.2\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} 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[]} src - Image URL(s). String for 'single', array for 'slider'.\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 */\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 });\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 });\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 {HTMLStyleElement|null} */\n #styleElement = null;\n\n /** @type {number|null} */\n #sliderIntervalId = 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 // Remove injected style element\n if (this.#styleElement?.parentNode) {\n this.#styleElement.parentNode.removeChild(this.#styleElement);\n this.#styleElement = null;\n }\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 inline background-image\n this.#element.style.backgroundImage = '';\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.#injectStyles();\n this.#buildDOM();\n\n // If the element is NOT <body>, wait for window load to start loading images.\n // For <body>, the loader is already visible, so we start immediately to avoid\n // the re-execution issue the legacy lib had with body scripts.\n if (this.#element.matches('body')) {\n // Use a microtask so the DOM changes above settle first.\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 // DOM Construction\n // ---------------------------------------------------------------------------\n\n /**\n * Inject a scoped <style> element into the <head>.\n */\n #injectStyles() {\n const uid = this.#uid;\n const selector = this.#options.element === 'body' || this.#element.matches('body')\n ? 'body'\n : this.#options.element;\n const duration = this.#options.transitionDuration / 1000; // ms → s\n const position = this.#element.matches('body') ? 'fixed' : 'absolute';\n const overlayBg = this.#options.overlayImage\n ? `url(${this.#options.overlayImage})`\n : 'none';\n\n const css = `\n ${typeof selector === 'string' ? selector : `.cbg-host-${uid}`} {\n position: relative;\n }\n\n #cbg-inner-${uid} {\n z-index: 2;\n position: relative;\n }\n\n .cbg-loader-${uid} {\n height: 100%;\n width: 100%;\n position: ${position};\n background-image: ${overlayBg};\n background-color: ${this.#options.overlayColor};\n opacity: 1;\n z-index: 1;\n transition: opacity ${duration}s ease;\n top: 0;\n left: 0;\n }\n `;\n\n const style = document.createElement('style');\n style.dataset.chameleonUid = uid;\n style.textContent = css;\n document.head.appendChild(style);\n this.#styleElement = style;\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\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-${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 this.#loadBackground(typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0]);\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 * @param {string} src - Image URL to load.\n * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).\n * @returns {Promise<void>}\n */\n #loadBackground(src, isFirst = true) {\n if (this.#destroyed) return Promise.resolve();\n\n return new Promise((resolve) => {\n const img = new Image();\n\n img.onload = () => {\n if (this.#destroyed) return resolve();\n\n this.#element.style.backgroundImage = `url(${src})`;\n this.#element.style.backgroundSize = 'cover';\n\n const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = String(this.#options.minOverlay);\n }\n\n resolve();\n };\n\n img.onerror = () => {\n console.warn(`[ChameleonBackgrounds] Failed to load image: ${src}`);\n resolve();\n };\n\n img.src = src;\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. Otherwise wrap 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 const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);\n if (loader) {\n loader.style.opacity = '1';\n }\n\n return new Promise((resolve) => {\n setTimeout(() => {\n if (this.#destroyed) return resolve();\n\n if (this.#options.type === 'single') {\n const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];\n this.#loadBackground(singleSrc, false).then(resolve);\n } else if (this.#options.type === 'slider') {\n this.#startSlider();\n resolve();\n }\n }, this.#options.transitionDuration);\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 // Remove old styles\n if (this.#styleElement?.parentNode) {\n this.#styleElement.parentNode.removeChild(this.#styleElement);\n this.#styleElement = null;\n }\n\n // Inject updated styles\n this.#injectStyles();\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 let index = 0;\n this.#loadBackground(sources[index]).then(() => {\n if (this.#destroyed) return;\n\n index = 1;\n if (sources.length === 1) return; // only one slide, nothing to rotate\n\n const interval = this.#options.sliderDuration + this.#options.transitionDuration * 2;\n\n this.#sliderIntervalId = setInterval(() => {\n if (this.#destroyed) {\n clearInterval(this.#sliderIntervalId);\n return;\n }\n\n this.#cycleSliderSlide(sources[index]);\n index++;\n\n if (index >= sources.length) {\n if (this.#options.sliderLoop) {\n index = 0;\n } else {\n clearInterval(this.#sliderIntervalId);\n this.#sliderIntervalId = null;\n }\n }\n }, interval);\n });\n }\n\n /**\n * Cycle to a specific slide without resetting the slider instance.\n * @param {string} src - The image URL to load.\n */\n #cycleSliderSlide(src) {\n if (this.#destroyed) return;\n\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);\n }, this.#options.transitionDuration);\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 n=0;n<i.length;){var o=i[n++],r=i[n++];e.o(t,o)?0===r&&n++:0===r?Object.defineProperty(t,o,{enumerable:!0,value:i[n++]}):Object.defineProperty(t,o,{enumerable:!0,get:r})}else for(var o in i)e.o(i,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:i[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};let t={};class i{static#e=Object.freeze({element:"body",type:"single",src:"",overlayColor:"#0f1e25",overlayImage:null,minOverlay:0,transitionDuration:2e3,sliderDuration:8e3,sliderLoop:!1});static#t=Object.freeze({transition_duration:"transitionDuration",slider_duration:"sliderDuration",slider_loop:"sliderLoop",min_overlay:"minOverlay",overlay_color:"overlayColor",overlay_image:"overlayImage"});#i;#n;#o;#r;#s=null;#l=null;#d=!1;constructor(e={}){const t=i.#a(e);this.#i={...i.#e,...t},this.#o=i.#h(),this.#n=i.#c(this.#i.element),this.#r=this.#n.innerHTML,this.#u()}destroy(){if(this.#d)return;this.#d=!0,null!==this.#l&&(clearInterval(this.#l),this.#l=null),this.#s?.parentNode&&(this.#s.parentNode.removeChild(this.#s),this.#s=null);const e=this.#n.querySelector(`#cbg-inner-${this.#o}`);if(e&&e.parentNode===this.#n){for(;e.firstChild;)this.#n.insertBefore(e.firstChild,e);this.#n.removeChild(e)}const t=this.#n.querySelector(`.cbg-loader-${this.#o}`);t&&t.remove(),this.#n.style.backgroundImage=""}get options(){return{...this.#i}}#u(){this.#m(),this.#p(),this.#n.matches("body")?queueMicrotask(()=>this.#y()):"complete"===document.readyState?this.#y():window.addEventListener("load",()=>this.#y(),{once:!0})}#m(){const e=this.#o,t="body"===this.#i.element||this.#n.matches("body")?"body":this.#i.element,i=this.#i.transitionDuration/1e3,n=`\n ${"string"==typeof t?t:`.cbg-host-${e}`} {\n position: relative;\n }\n\n #cbg-inner-${e} {\n z-index: 2;\n position: relative;\n }\n\n .cbg-loader-${e} {\n height: 100%;\n width: 100%;\n position: ${this.#n.matches("body")?"fixed":"absolute"};\n background-image: ${this.#i.overlayImage?`url(${this.#i.overlayImage})`:"none"};\n background-color: ${this.#i.overlayColor};\n opacity: 1;\n z-index: 1;\n transition: opacity ${i}s ease;\n top: 0;\n left: 0;\n }\n `,o=document.createElement("style");o.dataset.chameleonUid=e,o.textContent=n,document.head.appendChild(o),this.#s=o}#p(){const e=this.#o,t=document.createElement("div");for(t.id=`cbg-inner-${e}`;this.#n.firstChild;)t.appendChild(this.#n.firstChild);const i=document.createElement("div");i.classList.add(`cbg-loader-${e}`),this.#n.appendChild(t),this.#n.appendChild(i)}#y(){this.#d||("single"===this.#i.type?this.#g("string"==typeof this.#i.src?this.#i.src:this.#i.src[0]):"slider"===this.#i.type&&this.#f())}#g(e,t=!0){return this.#d?Promise.resolve():new Promise(t=>{const i=new Image;i.onload=()=>{if(this.#d)return t();this.#n.style.backgroundImage=`url(${e})`,this.#n.style.backgroundSize="cover";const i=this.#n.querySelector(`.cbg-loader-${this.#o}`);i&&(i.style.opacity=String(this.#i.minOverlay)),t()},i.onerror=()=>{console.warn(`[ChameleonBackgrounds] Failed to load image: ${e}`),t()},i.src=e})}reloadBackground(e){if(this.#d)return Promise.resolve();void 0!==e&&("slider"===this.#i.type&&"string"==typeof e?this.#i.src=e.includes(",")?e.split(",").map(e=>e.trim()):[e]:this.#i.src=e),null!==this.#l&&(clearInterval(this.#l),this.#l=null);const t=this.#n.querySelector(`.cbg-loader-${this.#o}`);return t&&(t.style.opacity="1"),new Promise(e=>{setTimeout(()=>{if(this.#d)return e();if("single"===this.#i.type){const t="string"==typeof this.#i.src?this.#i.src:this.#i.src[0];this.#g(t,!1).then(e)}else"slider"===this.#i.type&&(this.#f(),e())},this.#i.transitionDuration)})}reloadOptions(e){if(this.#d)return;const t=i.#a(e);this.#i={...this.#i,...t},this.#s?.parentNode&&(this.#s.parentNode.removeChild(this.#s),this.#s=null),this.#m()}#f(){if(this.#d)return;const e=this.#i.src;if(!Array.isArray(e)||0===e.length)return void console.warn("[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.");let t=0;this.#g(e[t]).then(()=>{if(this.#d)return;if(t=1,1===e.length)return;const i=this.#i.sliderDuration+2*this.#i.transitionDuration;this.#l=setInterval(()=>{this.#d?clearInterval(this.#l):(this.reloadBackground(e[t]),t++,t>=e.length&&(this.#i.sliderLoop?t=0:(clearInterval(this.#l),this.#l=null)))},i)})}static#a(e){const t={};for(const[n,o]of Object.entries(e))t[i.#t[n]??n]=o;return t}static#c(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#h(){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 n=i;return e.d(t,["default",0,n]),t=t.default,t})());
2
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ChameleonBackgrounds=t():e.ChameleonBackgrounds=t()}(this,()=>(()=>{"use strict";const e={d:(t,i)=>{if(Array.isArray(i))for(var n=0;n<i.length;){var o=i[n++],r=i[n++];e.o(t,o)?0===r&&n++:0===r?Object.defineProperty(t,o,{enumerable:!0,value:i[n++]}):Object.defineProperty(t,o,{enumerable:!0,get:r})}else for(var o in i)e.o(i,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:i[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};let t={};class i{static#e=Object.freeze({element:"body",type:"single",src:"",overlayColor:"#0f1e25",overlayImage:null,minOverlay:0,transitionDuration:2e3,sliderDuration:8e3,sliderLoop:!1});static#t=Object.freeze({transition_duration:"transitionDuration",slider_duration:"sliderDuration",slider_loop:"sliderLoop",min_overlay:"minOverlay",overlay_color:"overlayColor",overlay_image:"overlayImage"});#i;#n;#o;#r;#s=null;#l=null;#d=!1;constructor(e={}){const t=i.#a(e);this.#i={...i.#e,...t},this.#o=i.#h(),this.#n=i.#c(this.#i.element),this.#r=this.#n.innerHTML,this.#u()}destroy(){if(this.#d)return;this.#d=!0,null!==this.#l&&(clearInterval(this.#l),this.#l=null),this.#s?.parentNode&&(this.#s.parentNode.removeChild(this.#s),this.#s=null);const e=this.#n.querySelector(`#cbg-inner-${this.#o}`);if(e&&e.parentNode===this.#n){for(;e.firstChild;)this.#n.insertBefore(e.firstChild,e);this.#n.removeChild(e)}const t=this.#n.querySelector(`.cbg-loader-${this.#o}`);t&&t.remove(),this.#n.style.backgroundImage=""}get options(){return{...this.#i}}#u(){this.#m(),this.#y(),this.#n.matches("body")?queueMicrotask(()=>this.#p()):"complete"===document.readyState?this.#p():window.addEventListener("load",()=>this.#p(),{once:!0})}#m(){const e=this.#o,t="body"===this.#i.element||this.#n.matches("body")?"body":this.#i.element,i=this.#i.transitionDuration/1e3,n=`\n ${"string"==typeof t?t:`.cbg-host-${e}`} {\n position: relative;\n }\n\n #cbg-inner-${e} {\n z-index: 2;\n position: relative;\n }\n\n .cbg-loader-${e} {\n height: 100%;\n width: 100%;\n position: ${this.#n.matches("body")?"fixed":"absolute"};\n background-image: ${this.#i.overlayImage?`url(${this.#i.overlayImage})`:"none"};\n background-color: ${this.#i.overlayColor};\n opacity: 1;\n z-index: 1;\n transition: opacity ${i}s ease;\n top: 0;\n left: 0;\n }\n `,o=document.createElement("style");o.dataset.chameleonUid=e,o.textContent=n,document.head.appendChild(o),this.#s=o}#y(){const e=this.#o,t=document.createElement("div");for(t.id=`cbg-inner-${e}`;this.#n.firstChild;)t.appendChild(this.#n.firstChild);const i=document.createElement("div");i.classList.add(`cbg-loader-${e}`),this.#n.appendChild(t),this.#n.appendChild(i)}#p(){this.#d||("single"===this.#i.type?this.#g("string"==typeof this.#i.src?this.#i.src:this.#i.src[0]):"slider"===this.#i.type&&this.#f())}#g(e,t=!0){return this.#d?Promise.resolve():new Promise(t=>{const i=new Image;i.onload=()=>{if(this.#d)return t();this.#n.style.backgroundImage=`url(${e})`,this.#n.style.backgroundSize="cover";const i=this.#n.querySelector(`.cbg-loader-${this.#o}`);i&&(i.style.opacity=String(this.#i.minOverlay)),t()},i.onerror=()=>{console.warn(`[ChameleonBackgrounds] Failed to load image: ${e}`),t()},i.src=e})}reloadBackground(e){if(this.#d)return Promise.resolve();void 0!==e&&("slider"===this.#i.type&&"string"==typeof e?this.#i.src=e.includes(",")?e.split(",").map(e=>e.trim()):[e]:this.#i.src=e),null!==this.#l&&(clearInterval(this.#l),this.#l=null);const t=this.#n.querySelector(`.cbg-loader-${this.#o}`);return t&&(t.style.opacity="1"),new Promise(e=>{setTimeout(()=>{if(this.#d)return e();if("single"===this.#i.type){const t="string"==typeof this.#i.src?this.#i.src:this.#i.src[0];this.#g(t,!1).then(e)}else"slider"===this.#i.type&&(this.#f(),e())},this.#i.transitionDuration)})}reloadOptions(e){if(this.#d)return;const t=i.#a(e);this.#i={...this.#i,...t},this.#s?.parentNode&&(this.#s.parentNode.removeChild(this.#s),this.#s=null),this.#m()}#f(){if(this.#d)return;const e=this.#i.src;if(!Array.isArray(e)||0===e.length)return void console.warn("[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.");let t=0;this.#g(e[t]).then(()=>{if(this.#d)return;if(t=1,1===e.length)return;const i=this.#i.sliderDuration+2*this.#i.transitionDuration;this.#l=setInterval(()=>{this.#d?clearInterval(this.#l):(this.#v(e[t]),t++,t>=e.length&&(this.#i.sliderLoop?t=0:(clearInterval(this.#l),this.#l=null)))},i)})}#v(e){if(this.#d)return;const t=this.#n.querySelector(`.cbg-loader-${this.#o}`);t&&(t.style.opacity="1"),setTimeout(()=>{this.#d||this.#g(e,!1)},this.#i.transitionDuration)}static#a(e){const t={};for(const[n,o]of Object.entries(e))t[i.#t[n]??n]=o;return t}static#c(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#h(){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 n=i;return e.d(t,["default",0,n]),t=t.default,t})());
@@ -14,7 +14,7 @@
14
14
  * |___/
15
15
  *
16
16
  * @module ChameleonBackgrounds
17
- * @version 2.0.0
17
+ * @version 2.1.2
18
18
  * @author Lennart van Ballegoij (https://weblenn.com/)
19
19
  * @license MIT
20
20
  * @see https://github.com/WebLenn/ChameleonBackgrounds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chameleon-backgrounds",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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",
@@ -51,4 +51,4 @@
51
51
  "webpack": "^5.109.0",
52
52
  "webpack-cli": "^7.2.1"
53
53
  }
54
- }
54
+ }
@@ -14,7 +14,7 @@
14
14
  * |___/
15
15
  *
16
16
  * @module ChameleonBackgrounds
17
- * @version 2.0.0
17
+ * @version 2.1.2
18
18
  * @author Lennart van Ballegoij (https://weblenn.com/)
19
19
  * @license MIT
20
20
  * @see https://github.com/WebLenn/ChameleonBackgrounds
@@ -324,7 +324,7 @@ class ChameleonBackgrounds {
324
324
  return new Promise((resolve) => {
325
325
  setTimeout(() => {
326
326
  if (this.#destroyed) return resolve();
327
-
327
+
328
328
  if (this.#options.type === 'single') {
329
329
  const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];
330
330
  this.#loadBackground(singleSrc, false).then(resolve);
@@ -388,7 +388,7 @@ class ChameleonBackgrounds {
388
388
  return;
389
389
  }
390
390
 
391
- this.reloadBackground(sources[index]);
391
+ this.#cycleSliderSlide(sources[index]);
392
392
  index++;
393
393
 
394
394
  if (index >= sources.length) {
@@ -403,6 +403,24 @@ class ChameleonBackgrounds {
403
403
  });
404
404
  }
405
405
 
406
+ /**
407
+ * Cycle to a specific slide without resetting the slider instance.
408
+ * @param {string} src - The image URL to load.
409
+ */
410
+ #cycleSliderSlide(src) {
411
+ if (this.#destroyed) return;
412
+
413
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
414
+ if (loader) {
415
+ loader.style.opacity = '1';
416
+ }
417
+
418
+ setTimeout(() => {
419
+ if (this.#destroyed) return;
420
+ this.#loadBackground(src, false);
421
+ }, this.#options.transitionDuration);
422
+ }
423
+
406
424
  // ---------------------------------------------------------------------------
407
425
  // Static Helpers
408
426
  // ---------------------------------------------------------------------------