chameleon-backgrounds 2.1.1 → 2.1.3

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.
package/README.md CHANGED
@@ -127,6 +127,7 @@ bg.destroy();
127
127
  | `transitionDuration` | `number` | `2000` | yes | Fade duration in milliseconds |
128
128
  | `sliderDuration` | `number` | `8000` | slider only | Time each slide is shown (ms) |
129
129
  | `sliderLoop` | `boolean` | `false` | slider only | Restart slider after last slide |
130
+ | `fetchPriority` | `string` | `'auto'` | no | Network priority for the initial image load. Set to `'high'` for LCP optimization. |
130
131
 
131
132
  ### Legacy Option Names
132
133
 
@@ -143,6 +144,18 @@ For backward compatibility, v1 snake_case option names are still supported:
143
144
 
144
145
  ---
145
146
 
147
+ ## Optimize for Largest Contentful Paint (LCP)
148
+
149
+ To get a great LCP score on PageSpeed Insights, you should set `fetchPriority: 'high'` on your main above-the-fold background instance.
150
+
151
+ Additionally, because browsers cannot discover JavaScript-loaded images in the initial HTML document parse, you should add a preload link to your `<head>` for the hero image:
152
+
153
+ ```html
154
+ <link rel="preload" as="image" href="path/to/hero.jpg" fetchpriority="high">
155
+ ```
156
+
157
+ ---
158
+
146
159
  ## API
147
160
 
148
161
  ### `new ChameleonBackgrounds(options)`
@@ -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,33 @@
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.3
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
+ * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
107
+ */
50
108
 
51
- const DEFAULTS = Object.freeze({
109
+ class ChameleonBackgrounds {
110
+ /** @type {ChameleonOptions} */
111
+ static #DEFAULTS = Object.freeze({
52
112
  element: 'body',
53
113
  type: 'single',
54
114
  src: '',
@@ -58,299 +118,440 @@
58
118
  transitionDuration: 2000,
59
119
  sliderDuration: 8000,
60
120
  sliderLoop: false,
121
+ fetchPriority: 'auto',
61
122
  });
62
123
 
63
- const ALIASES = Object.freeze({
124
+ // Legacy snake_case → camelCase alias map
125
+ static #ALIASES = Object.freeze({
64
126
  transition_duration: 'transitionDuration',
65
127
  slider_duration: 'sliderDuration',
66
128
  slider_loop: 'sliderLoop',
67
129
  min_overlay: 'minOverlay',
68
130
  overlay_color: 'overlayColor',
69
131
  overlay_image: 'overlayImage',
132
+ fetch_priority: 'fetchPriority',
70
133
  });
71
134
 
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
- }
135
+ /** @type {ChameleonOptions} */
136
+ #options;
83
137
 
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
- }
138
+ /** @type {HTMLElement} */
139
+ #element;
97
140
 
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
- }
141
+ /** @type {string} */
142
+ #uid;
109
143
 
110
- // -------------------------------------------------------------------------
111
- // Constructor
112
- // -------------------------------------------------------------------------
144
+ /** @type {string} */
145
+ #originalHTML;
113
146
 
114
- function ChameleonBackgrounds(options) {
115
- if (!(this instanceof ChameleonBackgrounds)) {
116
- return new ChameleonBackgrounds(options);
117
- }
147
+ /** @type {HTMLStyleElement|null} */
148
+ #styleElement = null;
118
149
 
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
- }
150
+ /** @type {number|null} */
151
+ #sliderIntervalId = null;
126
152
 
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;
153
+ /** @type {boolean} */
154
+ #destroyed = false;
133
155
 
134
- this._init();
156
+ /**
157
+ * Create a new ChameleonBackgrounds instance.
158
+ * @param {Partial<ChameleonOptions>} [options]
159
+ */
160
+ constructor(options = {}) {
161
+ const normalized = ChameleonBackgrounds.#normalizeOptions(options);
162
+ this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
163
+ this.#uid = ChameleonBackgrounds.#generateUID();
164
+ this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
165
+ this.#originalHTML = this.#element.innerHTML;
166
+
167
+ this.#init();
135
168
  }
136
169
 
137
- // -------------------------------------------------------------------------
170
+ // ---------------------------------------------------------------------------
138
171
  // Public API
139
- // -------------------------------------------------------------------------
172
+ // ---------------------------------------------------------------------------
140
173
 
141
174
  /**
142
175
  * Clean up all injected DOM, styles, and intervals.
143
176
  * Restores the target element to its original state.
144
177
  */
145
- ChameleonBackgrounds.prototype.destroy = function () {
146
- if (this._destroyed) return;
147
- this._destroyed = true;
178
+ destroy() {
179
+ if (this.#destroyed) return;
180
+ this.#destroyed = true;
181
+
182
+ // Stop any running slider
183
+ if (this.#sliderIntervalId !== null) {
184
+ clearInterval(this.#sliderIntervalId);
185
+ this.#sliderIntervalId = null;
186
+ }
148
187
 
149
- if (this._sliderIntervalId !== null) {
150
- clearInterval(this._sliderIntervalId);
151
- this._sliderIntervalId = null;
188
+ // Remove injected style element
189
+ if (this.#styleElement?.parentNode) {
190
+ this.#styleElement.parentNode.removeChild(this.#styleElement);
191
+ this.#styleElement = null;
152
192
  }
153
193
 
154
- if (this._styleElement && this._styleElement.parentNode) {
155
- this._styleElement.parentNode.removeChild(this._styleElement);
156
- this._styleElement = null;
194
+ // Unwrap the original content instead of resetting innerHTML
195
+ const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
196
+ if (wrapper && wrapper.parentNode === this.#element) {
197
+ while (wrapper.firstChild) {
198
+ this.#element.insertBefore(wrapper.firstChild, wrapper);
199
+ }
200
+ this.#element.removeChild(wrapper);
157
201
  }
158
202
 
159
- this._element.innerHTML = this._originalHTML;
160
- this._element.style.backgroundImage = '';
161
- };
203
+ // Remove loader
204
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
205
+ if (loader) {
206
+ loader.remove();
207
+ }
208
+
209
+ // Remove inline background-image
210
+ this.#element.style.backgroundImage = '';
211
+ }
162
212
 
163
213
  /**
164
- * Readonly copy of the resolved options.
214
+ * The resolved options (read-only copy).
215
+ * @returns {ChameleonOptions}
165
216
  */
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
- // -------------------------------------------------------------------------
217
+ get options() {
218
+ return { ...this.#options };
219
+ }
179
220
 
180
- ChameleonBackgrounds.prototype._init = function () {
181
- this._injectStyles();
182
- this._buildDOM();
221
+ // ---------------------------------------------------------------------------
222
+ // Initialization
223
+ // ---------------------------------------------------------------------------
183
224
 
184
- var self = this;
225
+ #init() {
226
+ this.#injectStyles();
227
+ this.#buildDOM();
185
228
 
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(); });
229
+ // If the element is NOT <body>, wait for window load to start loading images.
230
+ // For <body>, the loader is already visible, so we start immediately to avoid
231
+ // the re-execution issue the legacy lib had with body scripts.
232
+ if (this.#element.matches('body')) {
233
+ // Use a microtask so the DOM changes above settle first.
234
+ queueMicrotask(() => this.#retrieveBackground());
189
235
  } else {
190
236
  if (document.readyState === 'complete') {
191
- this._retrieveBackground();
237
+ this.#retrieveBackground();
192
238
  } else {
193
- window.addEventListener('load', function onLoad() {
194
- window.removeEventListener('load', onLoad);
195
- self._retrieveBackground();
196
- });
239
+ window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
197
240
  }
198
241
  }
199
- };
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // DOM Construction
246
+ // ---------------------------------------------------------------------------
200
247
 
201
- ChameleonBackgrounds.prototype._injectStyles = function () {
202
- var uid = this._uid;
203
- var selector = this._options.element === 'body' || this._element.matches('body')
248
+ /**
249
+ * Inject a scoped <style> element into the <head>.
250
+ */
251
+ #injectStyles() {
252
+ const uid = this.#uid;
253
+ const selector = this.#options.element === 'body' || this.#element.matches('body')
204
254
  ? '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 + ')'
255
+ : this.#options.element;
256
+ const duration = this.#options.transitionDuration / 1000; // ms → s
257
+ const position = this.#element.matches('body') ? 'fixed' : 'absolute';
258
+ const overlayBg = this.#options.overlayImage
259
+ ? `url(${this.#options.overlayImage})`
210
260
  : 'none';
211
261
 
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);
262
+ const css = `
263
+ ${typeof selector === 'string' ? selector : `.cbg-host-${uid}`} {
264
+ position: relative;
265
+ }
266
+
267
+ #cbg-inner-${uid} {
268
+ z-index: 2;
269
+ position: relative;
270
+ }
271
+
272
+ .cbg-loader-${uid} {
273
+ height: 100%;
274
+ width: 100%;
275
+ position: ${position};
276
+ background-image: ${overlayBg};
277
+ background-color: ${this.#options.overlayColor};
278
+ opacity: 1;
279
+ z-index: 1;
280
+ transition: opacity ${duration}s ease;
281
+ top: 0;
282
+ left: 0;
283
+ }
284
+ `;
285
+
286
+ const style = document.createElement('style');
287
+ style.dataset.chameleonUid = uid;
229
288
  style.textContent = css;
230
289
  document.head.appendChild(style);
231
- this._styleElement = style;
232
- };
290
+ this.#styleElement = style;
291
+ }
233
292
 
234
- ChameleonBackgrounds.prototype._buildDOM = function () {
235
- var uid = this._uid;
236
- var content = this._element.innerHTML;
293
+ /**
294
+ * Wrap existing content and insert the overlay loader.
295
+ */
296
+ #buildDOM() {
297
+ const uid = this.#uid;
237
298
 
238
- var wrapper = document.createElement('div');
239
- wrapper.id = 'cbg-inner-' + uid;
240
- wrapper.innerHTML = content;
299
+ const wrapper = document.createElement('div');
300
+ wrapper.id = `cbg-inner-${uid}`;
241
301
 
242
- var loader = document.createElement('div');
243
- loader.className = 'cbg-loader-' + uid;
302
+ // Move all child nodes from the element into the wrapper
303
+ while (this.#element.firstChild) {
304
+ wrapper.appendChild(this.#element.firstChild);
305
+ }
306
+
307
+ const loader = document.createElement('div');
308
+ loader.classList.add(`cbg-loader-${uid}`);
244
309
 
245
- this._element.innerHTML = '';
246
- this._element.appendChild(wrapper);
247
- this._element.appendChild(loader);
248
- };
310
+ // Append wrapper and loader
311
+ this.#element.appendChild(wrapper);
312
+ this.#element.appendChild(loader);
313
+ }
249
314
 
250
- // -------------------------------------------------------------------------
315
+ // ---------------------------------------------------------------------------
251
316
  // 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();
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /**
320
+ * Route to the correct loader based on type.
321
+ */
322
+ #retrieveBackground() {
323
+ if (this.#destroyed) return;
324
+
325
+ if (this.#options.type === 'single') {
326
+ this.#loadBackground(typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0]);
327
+ } else if (this.#options.type === 'slider') {
328
+ this.#startSlider();
264
329
  }
265
- };
330
+ }
266
331
 
267
- ChameleonBackgrounds.prototype._loadBackground = function (src, callback) {
268
- if (this._destroyed) { if (callback) callback(); return; }
332
+ /**
333
+ * Preload an image and apply it as the element's background.
334
+ * @param {string} src - Image URL to load.
335
+ * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
336
+ * @returns {Promise<void>}
337
+ */
338
+ #loadBackground(src, isFirst = true) {
339
+ if (this.#destroyed) return Promise.resolve();
269
340
 
270
- var self = this;
271
- var img = new Image();
341
+ return new Promise((resolve) => {
342
+ const img = new Image();
343
+ if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
344
+ img.fetchPriority = this.#options.fetchPriority;
345
+ }
272
346
 
273
- img.onload = function () {
274
- if (self._destroyed) { if (callback) callback(); return; }
347
+ img.onload = () => {
348
+ if (this.#destroyed) return resolve();
275
349
 
276
- self._element.style.backgroundImage = 'url(' + src + ')';
277
- self._element.style.backgroundSize = 'cover';
350
+ this.#element.style.backgroundImage = `url(${src})`;
351
+ this.#element.style.backgroundSize = 'cover';
278
352
 
279
- var loader = self._element.querySelector('.cbg-loader-' + self._uid);
280
- if (loader) {
281
- loader.style.opacity = String(self._options.minOverlay);
282
- }
353
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
354
+ if (loader) {
355
+ loader.style.opacity = String(this.#options.minOverlay);
356
+ }
283
357
 
284
- if (callback) callback();
285
- };
358
+ resolve();
359
+ };
360
+
361
+ img.onerror = () => {
362
+ console.warn(`[ChameleonBackgrounds] Failed to load image: ${src}`);
363
+ resolve();
364
+ };
365
+
366
+ img.src = src;
367
+ });
368
+ }
286
369
 
287
- img.onerror = function () {
288
- console.warn('[ChameleonBackgrounds] Failed to load image: ' + src);
289
- if (callback) callback();
290
- };
370
+ /**
371
+ /**
372
+ * Reload background with a fade-out → swap → fade-in cycle.
373
+ * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
374
+ * @returns {Promise<void>}
375
+ */
376
+ reloadBackground(src) {
377
+ if (this.#destroyed) return Promise.resolve();
291
378
 
292
- img.src = src;
293
- };
379
+ if (src !== undefined) {
380
+ if (this.#options.type === 'slider' && typeof src === 'string') {
381
+ // If the user passes a comma-separated string, split it. Otherwise wrap it.
382
+ this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
383
+ } else {
384
+ this.#options.src = src;
385
+ }
386
+ }
294
387
 
295
- ChameleonBackgrounds.prototype.reloadBackground = function (src, callback) {
296
- if (this._destroyed) { if (callback) callback(); return; }
388
+ // Stop any running slider
389
+ if (this.#sliderIntervalId !== null) {
390
+ clearInterval(this.#sliderIntervalId);
391
+ this.#sliderIntervalId = null;
392
+ }
297
393
 
298
- var loader = this._element.querySelector('.cbg-loader-' + this._uid);
394
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
299
395
  if (loader) {
300
396
  loader.style.opacity = '1';
301
397
  }
302
398
 
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
- };
399
+ return new Promise((resolve) => {
400
+ setTimeout(() => {
401
+ if (this.#destroyed) return resolve();
309
402
 
310
- // -------------------------------------------------------------------------
403
+ if (this.#options.type === 'single') {
404
+ const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];
405
+ this.#loadBackground(singleSrc, false).then(resolve);
406
+ } else if (this.#options.type === 'slider') {
407
+ this.#startSlider();
408
+ resolve();
409
+ }
410
+ }, this.#options.transitionDuration);
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Update options on the fly without destroying the instance.
416
+ * @param {Object} newOptions - The new options to merge.
417
+ */
418
+ reloadOptions(newOptions) {
419
+ if (this.#destroyed) return;
420
+
421
+ const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
422
+ this.#options = { ...this.#options, ...normalized };
423
+
424
+ // Remove old styles
425
+ if (this.#styleElement?.parentNode) {
426
+ this.#styleElement.parentNode.removeChild(this.#styleElement);
427
+ this.#styleElement = null;
428
+ }
429
+
430
+ // Inject updated styles
431
+ this.#injectStyles();
432
+ }
433
+
434
+ // ---------------------------------------------------------------------------
311
435
  // Slider
312
- // -------------------------------------------------------------------------
436
+ // ---------------------------------------------------------------------------
313
437
 
314
- ChameleonBackgrounds.prototype._startSlider = function () {
315
- if (this._destroyed) return;
438
+ /**
439
+ * Start the background slideshow.
440
+ */
441
+ #startSlider() {
442
+ if (this.#destroyed) return;
316
443
 
317
- var sources = this._options.src;
444
+ const sources = this.#options.src;
318
445
  if (!Array.isArray(sources) || sources.length === 0) {
319
446
  console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
320
447
  return;
321
448
  }
322
449
 
323
- var self = this;
324
- var index = 0;
325
-
326
- this._loadBackground(sources[index], function () {
327
- if (self._destroyed) return;
450
+ // Load the first slide immediately
451
+ let index = 0;
452
+ this.#loadBackground(sources[index]).then(() => {
453
+ if (this.#destroyed) return;
328
454
 
329
455
  index = 1;
330
- if (sources.length === 1) return;
456
+ if (sources.length === 1) return; // only one slide, nothing to rotate
331
457
 
332
- var interval = self._options.sliderDuration + self._options.transitionDuration * 2;
458
+ const interval = this.#options.sliderDuration + this.#options.transitionDuration * 2;
333
459
 
334
- self._sliderIntervalId = setInterval(function () {
335
- if (self._destroyed) {
336
- clearInterval(self._sliderIntervalId);
460
+ this.#sliderIntervalId = setInterval(() => {
461
+ if (this.#destroyed) {
462
+ clearInterval(this.#sliderIntervalId);
337
463
  return;
338
464
  }
339
465
 
340
- self.reloadBackground(sources[index]);
466
+ this.#cycleSliderSlide(sources[index]);
341
467
  index++;
342
468
 
343
469
  if (index >= sources.length) {
344
- if (self._options.sliderLoop) {
470
+ if (this.#options.sliderLoop) {
345
471
  index = 0;
346
472
  } else {
347
- clearInterval(self._sliderIntervalId);
348
- self._sliderIntervalId = null;
473
+ clearInterval(this.#sliderIntervalId);
474
+ this.#sliderIntervalId = null;
349
475
  }
350
476
  }
351
477
  }, interval);
352
478
  });
353
- };
479
+ }
480
+
481
+ /**
482
+ * Cycle to a specific slide without resetting the slider instance.
483
+ * @param {string} src - The image URL to load.
484
+ */
485
+ #cycleSliderSlide(src) {
486
+ if (this.#destroyed) return;
487
+
488
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
489
+ if (loader) {
490
+ loader.style.opacity = '1';
491
+ }
492
+
493
+ setTimeout(() => {
494
+ if (this.#destroyed) return;
495
+ this.#loadBackground(src, false);
496
+ }, this.#options.transitionDuration);
497
+ }
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Static Helpers
501
+ // ---------------------------------------------------------------------------
502
+
503
+ /**
504
+ * Convert legacy snake_case option keys to camelCase.
505
+ * @param {Object} opts
506
+ * @returns {Object}
507
+ */
508
+ static #normalizeOptions(opts) {
509
+ const result = {};
510
+ for (const [key, value] of Object.entries(opts)) {
511
+ const alias = ChameleonBackgrounds.#ALIASES[key];
512
+ result[alias ?? key] = value;
513
+ }
514
+ return result;
515
+ }
516
+
517
+ /**
518
+ * Resolve a CSS selector string or HTMLElement to an HTMLElement.
519
+ * @param {string|HTMLElement} el
520
+ * @returns {HTMLElement}
521
+ */
522
+ static #resolveElement(el) {
523
+ if (el instanceof HTMLElement) return el;
524
+ if (typeof el === 'string') {
525
+ if (el === 'body') return document.body;
526
+ const found = document.querySelector(el);
527
+ if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
528
+ return /** @type {HTMLElement} */ (found);
529
+ }
530
+ throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
531
+ }
532
+
533
+ /**
534
+ * Generate a short unique ID for scoping styles.
535
+ * @returns {string}
536
+ */
537
+ static #generateUID() {
538
+ // Prefer crypto.randomUUID where available, otherwise fall back
539
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
540
+ return crypto.randomUUID().slice(0, 8);
541
+ }
542
+ // Fallback: random alphanumeric string
543
+ return Array.from({ length: 8 }, () =>
544
+ 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
545
+ ).join('');
546
+ }
547
+ }
548
+
549
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ChameleonBackgrounds);
550
+
354
551
 
355
- return ChameleonBackgrounds;
356
- }));
552
+ __webpack_exports__ = __webpack_exports__["default"];
553
+ /******/ return __webpack_exports__;
554
+ /******/ })()
555
+ ;
556
+ });
557
+ //# 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,cAAc,yBAAyB;AACvC;;AAEA;AACA,aAAa,kBAAkB;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;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;AACA;AACA;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.3\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 * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).\n */\n\nclass ChameleonBackgrounds {\n /** @type {ChameleonOptions} */\n static #DEFAULTS = Object.freeze({\n element: 'body',\n type: 'single',\n src: '',\n overlayColor: '#0f1e25',\n overlayImage: null,\n minOverlay: 0,\n transitionDuration: 2000,\n sliderDuration: 8000,\n sliderLoop: false,\n fetchPriority: 'auto',\n });\n\n // Legacy snake_case → camelCase alias map\n static #ALIASES = Object.freeze({\n transition_duration: 'transitionDuration',\n slider_duration: 'sliderDuration',\n slider_loop: 'sliderLoop',\n min_overlay: 'minOverlay',\n overlay_color: 'overlayColor',\n overlay_image: 'overlayImage',\n fetch_priority: 'fetchPriority',\n });\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 if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {\n img.fetchPriority = this.#options.fetchPriority;\n }\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.#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})());
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 o=0;o<i.length;){var n=i[o++],r=i[o++];e.o(t,n)?0===r&&o++:0===r?Object.defineProperty(t,n,{enumerable:!0,value:i[o++]}):Object.defineProperty(t,n,{enumerable:!0,get:r})}else for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};let t={};class i{static#e=Object.freeze({element:"body",type:"single",src:"",overlayColor:"#0f1e25",overlayImage:null,minOverlay:0,transitionDuration:2e3,sliderDuration:8e3,sliderLoop:!1,fetchPriority:"auto"});static#t=Object.freeze({transition_duration:"transitionDuration",slider_duration:"sliderDuration",slider_loop:"sliderLoop",min_overlay:"minOverlay",overlay_color:"overlayColor",overlay_image:"overlayImage",fetch_priority:"fetchPriority"});#i;#o;#n;#r;#s=null;#l=null;#d=!1;constructor(e={}){const t=i.#a(e);this.#i={...i.#e,...t},this.#n=i.#h(),this.#o=i.#c(this.#i.element),this.#r=this.#o.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.#o.querySelector(`#cbg-inner-${this.#n}`);if(e&&e.parentNode===this.#o){for(;e.firstChild;)this.#o.insertBefore(e.firstChild,e);this.#o.removeChild(e)}const t=this.#o.querySelector(`.cbg-loader-${this.#n}`);t&&t.remove(),this.#o.style.backgroundImage=""}get options(){return{...this.#i}}#u(){this.#y(),this.#m(),this.#o.matches("body")?queueMicrotask(()=>this.#p()):"complete"===document.readyState?this.#p():window.addEventListener("load",()=>this.#p(),{once:!0})}#y(){const e=this.#n,t="body"===this.#i.element||this.#o.matches("body")?"body":this.#i.element,i=this.#i.transitionDuration/1e3,o=`\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.#o.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 `,n=document.createElement("style");n.dataset.chameleonUid=e,n.textContent=o,document.head.appendChild(n),this.#s=n}#m(){const e=this.#n,t=document.createElement("div");for(t.id=`cbg-inner-${e}`;this.#o.firstChild;)t.appendChild(this.#o.firstChild);const i=document.createElement("div");i.classList.add(`cbg-loader-${e}`),this.#o.appendChild(t),this.#o.appendChild(i)}#p(){this.#d||("single"===this.#i.type?this.#f("string"==typeof this.#i.src?this.#i.src:this.#i.src[0]):"slider"===this.#i.type&&this.#g())}#f(e,t=!0){return this.#d?Promise.resolve():new Promise(i=>{const o=new Image;t&&"fetchPriority"in o&&"auto"!==this.#i.fetchPriority&&(o.fetchPriority=this.#i.fetchPriority),o.onload=()=>{if(this.#d)return i();this.#o.style.backgroundImage=`url(${e})`,this.#o.style.backgroundSize="cover";const t=this.#o.querySelector(`.cbg-loader-${this.#n}`);t&&(t.style.opacity=String(this.#i.minOverlay)),i()},o.onerror=()=>{console.warn(`[ChameleonBackgrounds] Failed to load image: ${e}`),i()},o.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.#o.querySelector(`.cbg-loader-${this.#n}`);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.#f(t,!1).then(e)}else"slider"===this.#i.type&&(this.#g(),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.#y()}#g(){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.#f(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.#o.querySelector(`.cbg-loader-${this.#n}`);t&&(t.style.opacity="1"),setTimeout(()=>{this.#d||this.#f(e,!1)},this.#i.transitionDuration)}static#a(e){const t={};for(const[o,n]of Object.entries(e))t[i.#t[o]??o]=n;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 o=i;return e.d(t,["default",0,o]),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.3
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.1",
3
+ "version": "2.1.3",
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.3
18
18
  * @author Lennart van Ballegoij (https://weblenn.com/)
19
19
  * @license MIT
20
20
  * @see https://github.com/WebLenn/ChameleonBackgrounds
@@ -34,6 +34,7 @@
34
34
  * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
35
35
  * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
36
36
  * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
37
+ * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
37
38
  */
38
39
 
39
40
  class ChameleonBackgrounds {
@@ -48,6 +49,7 @@ class ChameleonBackgrounds {
48
49
  transitionDuration: 2000,
49
50
  sliderDuration: 8000,
50
51
  sliderLoop: false,
52
+ fetchPriority: 'auto',
51
53
  });
52
54
 
53
55
  // Legacy snake_case → camelCase alias map
@@ -58,6 +60,7 @@ class ChameleonBackgrounds {
58
60
  min_overlay: 'minOverlay',
59
61
  overlay_color: 'overlayColor',
60
62
  overlay_image: 'overlayImage',
63
+ fetch_priority: 'fetchPriority',
61
64
  });
62
65
 
63
66
  /** @type {ChameleonOptions} */
@@ -268,6 +271,9 @@ class ChameleonBackgrounds {
268
271
 
269
272
  return new Promise((resolve) => {
270
273
  const img = new Image();
274
+ if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
275
+ img.fetchPriority = this.#options.fetchPriority;
276
+ }
271
277
 
272
278
  img.onload = () => {
273
279
  if (this.#destroyed) return resolve();
@@ -324,7 +330,7 @@ class ChameleonBackgrounds {
324
330
  return new Promise((resolve) => {
325
331
  setTimeout(() => {
326
332
  if (this.#destroyed) return resolve();
327
-
333
+
328
334
  if (this.#options.type === 'single') {
329
335
  const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];
330
336
  this.#loadBackground(singleSrc, false).then(resolve);