chameleon-backgrounds 2.1.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -11
- package/dist/chameleon-backgrounds.js +653 -467
- package/dist/chameleon-backgrounds.js.map +23 -1
- package/dist/chameleon-backgrounds.min.js +1 -1
- package/dist/chameleon-backgrounds.min.js.LICENSE.txt +1 -1
- package/package.json +1 -1
- package/src/chameleon-backgrounds.js +268 -98
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* |___/
|
|
15
15
|
*
|
|
16
16
|
* @module ChameleonBackgrounds
|
|
17
|
-
* @version
|
|
17
|
+
* @version 3.0.0
|
|
18
18
|
* @author Lennart van Ballegoij (https://weblenn.com/)
|
|
19
19
|
* @license MIT
|
|
20
20
|
* @see https://github.com/WebLenn/ChameleonBackgrounds
|
|
@@ -24,10 +24,15 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
+
* @typedef {Object} ChameleonImageConfig
|
|
28
|
+
* @property {string} url - The fallback URL.
|
|
29
|
+
* @property {string} [srcset] - Optional srcset.
|
|
30
|
+
* @property {string} [sizes] - Optional sizes.
|
|
31
|
+
*
|
|
27
32
|
* @typedef {Object} ChameleonOptions
|
|
28
33
|
* @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
|
|
29
34
|
* @property {'single'|'slider'} type - Background mode.
|
|
30
|
-
* @property {string|string[]}
|
|
35
|
+
* @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
|
|
31
36
|
* @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
|
|
32
37
|
* @property {string} [overlayImage] - Optional overlay pattern image URL.
|
|
33
38
|
* @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
|
|
@@ -35,6 +40,8 @@
|
|
|
35
40
|
* @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
|
|
36
41
|
* @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
|
|
37
42
|
* @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
|
|
43
|
+
* @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.
|
|
44
|
+
* @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).
|
|
38
45
|
*/
|
|
39
46
|
|
|
40
47
|
class ChameleonBackgrounds {
|
|
@@ -50,6 +57,9 @@ class ChameleonBackgrounds {
|
|
|
50
57
|
sliderDuration: 8000,
|
|
51
58
|
sliderLoop: false,
|
|
52
59
|
fetchPriority: 'auto',
|
|
60
|
+
lazyLoad: true,
|
|
61
|
+
transitionMode: 'solid',
|
|
62
|
+
respectReducedMotion: false,
|
|
53
63
|
});
|
|
54
64
|
|
|
55
65
|
// Legacy snake_case → camelCase alias map
|
|
@@ -61,8 +71,14 @@ class ChameleonBackgrounds {
|
|
|
61
71
|
overlay_color: 'overlayColor',
|
|
62
72
|
overlay_image: 'overlayImage',
|
|
63
73
|
fetch_priority: 'fetchPriority',
|
|
74
|
+
lazy_load: 'lazyLoad',
|
|
75
|
+
transition_mode: 'transitionMode',
|
|
76
|
+
respect_reduced_motion: 'respectReducedMotion',
|
|
64
77
|
});
|
|
65
78
|
|
|
79
|
+
/** @type {boolean} */
|
|
80
|
+
static #globalStylesInjected = false;
|
|
81
|
+
|
|
66
82
|
/** @type {ChameleonOptions} */
|
|
67
83
|
#options;
|
|
68
84
|
|
|
@@ -75,12 +91,18 @@ class ChameleonBackgrounds {
|
|
|
75
91
|
/** @type {string} */
|
|
76
92
|
#originalHTML;
|
|
77
93
|
|
|
78
|
-
/** @type {HTMLStyleElement|null} */
|
|
79
|
-
#styleElement = null;
|
|
80
|
-
|
|
81
94
|
/** @type {number|null} */
|
|
82
95
|
#sliderIntervalId = null;
|
|
83
96
|
|
|
97
|
+
/** @type {number} */
|
|
98
|
+
#currentSlideIndex = 0;
|
|
99
|
+
|
|
100
|
+
/** @type {boolean} */
|
|
101
|
+
#isPaused = false;
|
|
102
|
+
|
|
103
|
+
/** @type {IntersectionObserver|null} */
|
|
104
|
+
#observer = null;
|
|
105
|
+
|
|
84
106
|
/** @type {boolean} */
|
|
85
107
|
#destroyed = false;
|
|
86
108
|
|
|
@@ -116,12 +138,14 @@ class ChameleonBackgrounds {
|
|
|
116
138
|
this.#sliderIntervalId = null;
|
|
117
139
|
}
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.#
|
|
122
|
-
this.#styleElement = null;
|
|
141
|
+
if (this.#observer) {
|
|
142
|
+
this.#observer.disconnect();
|
|
143
|
+
this.#observer = null;
|
|
123
144
|
}
|
|
124
145
|
|
|
146
|
+
// Clean up dynamic classes on host
|
|
147
|
+
this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
148
|
+
|
|
125
149
|
// Unwrap the original content instead of resetting innerHTML
|
|
126
150
|
const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
|
|
127
151
|
if (wrapper && wrapper.parentNode === this.#element) {
|
|
@@ -137,8 +161,16 @@ class ChameleonBackgrounds {
|
|
|
137
161
|
loader.remove();
|
|
138
162
|
}
|
|
139
163
|
|
|
140
|
-
// Remove
|
|
164
|
+
// Remove temporary crossfade elements
|
|
165
|
+
const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
|
|
166
|
+
crossfadeElements.forEach(el => el.remove());
|
|
167
|
+
|
|
168
|
+
// Remove inline CSS Variables and background-image
|
|
141
169
|
this.#element.style.backgroundImage = '';
|
|
170
|
+
this.#element.style.removeProperty('--cbg-duration');
|
|
171
|
+
this.#element.style.removeProperty('--cbg-overlay-color');
|
|
172
|
+
this.#element.style.removeProperty('--cbg-overlay-image');
|
|
173
|
+
this.#element.style.removeProperty('--cbg-min-overlay');
|
|
142
174
|
}
|
|
143
175
|
|
|
144
176
|
/**
|
|
@@ -154,71 +186,145 @@ class ChameleonBackgrounds {
|
|
|
154
186
|
// ---------------------------------------------------------------------------
|
|
155
187
|
|
|
156
188
|
#init() {
|
|
157
|
-
this.#
|
|
189
|
+
this.#injectGlobalStyles();
|
|
190
|
+
this.#updateCSSVariables();
|
|
158
191
|
this.#buildDOM();
|
|
159
192
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
// Check for prefers-reduced-motion to auto-pause slider if enabled
|
|
194
|
+
if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
195
|
+
this.#isPaused = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
|
|
199
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
200
|
+
entries.forEach((entry) => {
|
|
201
|
+
if (entry.isIntersecting) {
|
|
202
|
+
this.#observer.disconnect();
|
|
203
|
+
this.#observer = null;
|
|
204
|
+
this.#retrieveBackground();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
this.#observer.observe(this.#element);
|
|
166
209
|
} else {
|
|
167
|
-
if (
|
|
168
|
-
this.#retrieveBackground();
|
|
210
|
+
if (this.#element.matches('body')) {
|
|
211
|
+
queueMicrotask(() => this.#retrieveBackground());
|
|
169
212
|
} else {
|
|
170
|
-
|
|
213
|
+
if (document.readyState === 'complete') {
|
|
214
|
+
this.#retrieveBackground();
|
|
215
|
+
} else {
|
|
216
|
+
window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
|
|
217
|
+
}
|
|
171
218
|
}
|
|
172
219
|
}
|
|
173
220
|
}
|
|
174
221
|
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Play / Pause API
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pause the slideshow.
|
|
228
|
+
*/
|
|
229
|
+
pause() {
|
|
230
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
231
|
+
this.#isPaused = true;
|
|
232
|
+
if (this.#sliderIntervalId !== null) {
|
|
233
|
+
clearInterval(this.#sliderIntervalId);
|
|
234
|
+
this.#sliderIntervalId = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resume the slideshow.
|
|
240
|
+
*/
|
|
241
|
+
play() {
|
|
242
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
243
|
+
this.#isPaused = false;
|
|
244
|
+
// Only restart if not currently running
|
|
245
|
+
if (this.#sliderIntervalId === null) {
|
|
246
|
+
this.#startSliderLoop();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
175
250
|
// ---------------------------------------------------------------------------
|
|
176
251
|
// DOM Construction
|
|
177
252
|
// ---------------------------------------------------------------------------
|
|
178
253
|
|
|
179
254
|
/**
|
|
180
|
-
* Inject
|
|
255
|
+
* Inject global styles once.
|
|
181
256
|
*/
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
? 'body'
|
|
186
|
-
: this.#options.element;
|
|
187
|
-
const duration = this.#options.transitionDuration / 1000; // ms → s
|
|
188
|
-
const position = this.#element.matches('body') ? 'fixed' : 'absolute';
|
|
189
|
-
const overlayBg = this.#options.overlayImage
|
|
190
|
-
? `url(${this.#options.overlayImage})`
|
|
191
|
-
: 'none';
|
|
257
|
+
#injectGlobalStyles() {
|
|
258
|
+
if (ChameleonBackgrounds.#globalStylesInjected) return;
|
|
259
|
+
ChameleonBackgrounds.#globalStylesInjected = true;
|
|
192
260
|
|
|
193
261
|
const css = `
|
|
194
|
-
|
|
262
|
+
.cbg-host {
|
|
195
263
|
position: relative;
|
|
196
264
|
}
|
|
197
|
-
|
|
198
|
-
|
|
265
|
+
body.cbg-host {
|
|
266
|
+
/* body naturally acts as relative for background sizing if not explicitly positioned differently */
|
|
267
|
+
}
|
|
268
|
+
.cbg-inner {
|
|
199
269
|
z-index: 2;
|
|
200
270
|
position: relative;
|
|
201
271
|
}
|
|
202
|
-
|
|
203
|
-
.cbg-loader-${uid} {
|
|
272
|
+
.cbg-loader {
|
|
204
273
|
height: 100%;
|
|
205
274
|
width: 100%;
|
|
206
|
-
position:
|
|
207
|
-
|
|
208
|
-
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: 0;
|
|
277
|
+
left: 0;
|
|
278
|
+
background-image: var(--cbg-overlay-image, none);
|
|
279
|
+
background-color: var(--cbg-overlay-color, transparent);
|
|
209
280
|
opacity: 1;
|
|
210
281
|
z-index: 1;
|
|
211
|
-
transition: opacity
|
|
282
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
283
|
+
pointer-events: none;
|
|
284
|
+
}
|
|
285
|
+
body.cbg-host > .cbg-loader {
|
|
286
|
+
position: fixed;
|
|
287
|
+
}
|
|
288
|
+
.cbg-crossfade-el {
|
|
289
|
+
position: absolute;
|
|
212
290
|
top: 0;
|
|
213
291
|
left: 0;
|
|
292
|
+
height: 100%;
|
|
293
|
+
width: 100%;
|
|
294
|
+
background-size: cover;
|
|
295
|
+
background-position: center;
|
|
296
|
+
background-repeat: no-repeat;
|
|
297
|
+
z-index: 0;
|
|
298
|
+
opacity: 0;
|
|
299
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
300
|
+
pointer-events: none;
|
|
301
|
+
}
|
|
302
|
+
body.cbg-host > .cbg-crossfade-el {
|
|
303
|
+
position: fixed;
|
|
214
304
|
}
|
|
215
305
|
`;
|
|
216
306
|
|
|
217
307
|
const style = document.createElement('style');
|
|
218
|
-
style.
|
|
308
|
+
style.id = 'chameleon-global-styles';
|
|
219
309
|
style.textContent = css;
|
|
220
310
|
document.head.appendChild(style);
|
|
221
|
-
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Apply CSS custom properties to the host element.
|
|
315
|
+
*/
|
|
316
|
+
#updateCSSVariables() {
|
|
317
|
+
this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
318
|
+
this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
|
|
319
|
+
this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
|
|
320
|
+
|
|
321
|
+
if (this.#options.overlayImage) {
|
|
322
|
+
this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
|
|
323
|
+
} else {
|
|
324
|
+
this.#element.style.setProperty('--cbg-overlay-image', 'none');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
|
|
222
328
|
}
|
|
223
329
|
|
|
224
330
|
/**
|
|
@@ -229,6 +335,7 @@ class ChameleonBackgrounds {
|
|
|
229
335
|
|
|
230
336
|
const wrapper = document.createElement('div');
|
|
231
337
|
wrapper.id = `cbg-inner-${uid}`;
|
|
338
|
+
wrapper.classList.add('cbg-inner');
|
|
232
339
|
|
|
233
340
|
// Move all child nodes from the element into the wrapper
|
|
234
341
|
while (this.#element.firstChild) {
|
|
@@ -236,7 +343,7 @@ class ChameleonBackgrounds {
|
|
|
236
343
|
}
|
|
237
344
|
|
|
238
345
|
const loader = document.createElement('div');
|
|
239
|
-
loader.classList.add(`cbg-loader-${uid}`);
|
|
346
|
+
loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
|
|
240
347
|
|
|
241
348
|
// Append wrapper and loader
|
|
242
349
|
this.#element.appendChild(wrapper);
|
|
@@ -254,7 +361,8 @@ class ChameleonBackgrounds {
|
|
|
254
361
|
if (this.#destroyed) return;
|
|
255
362
|
|
|
256
363
|
if (this.#options.type === 'single') {
|
|
257
|
-
|
|
364
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
365
|
+
this.#loadBackground(singleSrc);
|
|
258
366
|
} else if (this.#options.type === 'slider') {
|
|
259
367
|
this.#startSlider();
|
|
260
368
|
}
|
|
@@ -262,39 +370,87 @@ class ChameleonBackgrounds {
|
|
|
262
370
|
|
|
263
371
|
/**
|
|
264
372
|
* Preload an image and apply it as the element's background.
|
|
265
|
-
*
|
|
373
|
+
* Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
|
|
374
|
+
* @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
|
|
266
375
|
* @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
|
|
376
|
+
* @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
|
|
267
377
|
* @returns {Promise<void>}
|
|
268
378
|
*/
|
|
269
|
-
#loadBackground(
|
|
379
|
+
#loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
|
|
270
380
|
if (this.#destroyed) return Promise.resolve();
|
|
271
381
|
|
|
272
382
|
return new Promise((resolve) => {
|
|
273
383
|
const img = new Image();
|
|
384
|
+
|
|
385
|
+
let urlStr = srcConfig;
|
|
386
|
+
if (typeof srcConfig === 'object' && srcConfig.url) {
|
|
387
|
+
urlStr = srcConfig.url;
|
|
388
|
+
if (srcConfig.srcset) img.srcset = srcConfig.srcset;
|
|
389
|
+
if (srcConfig.sizes) img.sizes = srcConfig.sizes;
|
|
390
|
+
}
|
|
391
|
+
|
|
274
392
|
if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
|
|
275
393
|
img.fetchPriority = this.#options.fetchPriority;
|
|
276
394
|
}
|
|
277
395
|
|
|
396
|
+
// Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
|
|
397
|
+
img.style.position = 'absolute';
|
|
398
|
+
img.style.visibility = 'hidden';
|
|
399
|
+
img.style.width = '0';
|
|
400
|
+
img.style.height = '0';
|
|
401
|
+
this.#element.appendChild(img);
|
|
402
|
+
|
|
278
403
|
img.onload = () => {
|
|
404
|
+
img.remove();
|
|
279
405
|
if (this.#destroyed) return resolve();
|
|
280
406
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
407
|
+
// `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
|
|
408
|
+
const finalUrl = img.currentSrc || urlStr;
|
|
409
|
+
|
|
410
|
+
if (isCrossfade && !isFirst) {
|
|
411
|
+
// --- Crossfade Transition Mode ---
|
|
412
|
+
const crossfadeDiv = document.createElement('div');
|
|
413
|
+
crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
|
|
414
|
+
crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
|
|
415
|
+
|
|
416
|
+
// Insert right behind the loader (or directly at start of host)
|
|
417
|
+
this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
|
|
418
|
+
|
|
419
|
+
// Trigger layout so the transition works
|
|
420
|
+
void crossfadeDiv.offsetWidth;
|
|
421
|
+
|
|
422
|
+
crossfadeDiv.style.opacity = '1';
|
|
423
|
+
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
if (this.#destroyed) return resolve();
|
|
426
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
427
|
+
this.#element.style.backgroundSize = 'cover';
|
|
428
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
429
|
+
crossfadeDiv.remove();
|
|
430
|
+
resolve();
|
|
431
|
+
}, this.#options.transitionDuration);
|
|
432
|
+
|
|
433
|
+
} else {
|
|
434
|
+
// --- Solid Transition Mode or First Load ---
|
|
435
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
436
|
+
this.#element.style.backgroundSize = 'cover';
|
|
437
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
438
|
+
|
|
439
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
440
|
+
if (loader) {
|
|
441
|
+
loader.style.opacity = String(this.#options.minOverlay);
|
|
442
|
+
}
|
|
443
|
+
resolve();
|
|
287
444
|
}
|
|
288
|
-
|
|
289
|
-
resolve();
|
|
290
445
|
};
|
|
291
446
|
|
|
292
447
|
img.onerror = () => {
|
|
293
|
-
|
|
294
|
-
|
|
448
|
+
img.remove();
|
|
449
|
+
console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
|
|
450
|
+
resolve(); // resolve anyway to keep logic flowing
|
|
295
451
|
};
|
|
296
452
|
|
|
297
|
-
img.src =
|
|
453
|
+
img.src = urlStr;
|
|
298
454
|
});
|
|
299
455
|
}
|
|
300
456
|
|
|
@@ -309,7 +465,7 @@ class ChameleonBackgrounds {
|
|
|
309
465
|
|
|
310
466
|
if (src !== undefined) {
|
|
311
467
|
if (this.#options.type === 'slider' && typeof src === 'string') {
|
|
312
|
-
// If the user passes a comma-separated string, split it.
|
|
468
|
+
// If the user passes a comma-separated string, split it.
|
|
313
469
|
this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
|
|
314
470
|
} else {
|
|
315
471
|
this.#options.src = src;
|
|
@@ -322,23 +478,25 @@ class ChameleonBackgrounds {
|
|
|
322
478
|
this.#sliderIntervalId = null;
|
|
323
479
|
}
|
|
324
480
|
|
|
481
|
+
// For solid mode, we fade the loader up. For crossfade, we don't.
|
|
325
482
|
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
326
|
-
if (loader) {
|
|
483
|
+
if (loader && this.#options.transitionMode === 'solid') {
|
|
327
484
|
loader.style.opacity = '1';
|
|
328
485
|
}
|
|
329
486
|
|
|
330
487
|
return new Promise((resolve) => {
|
|
488
|
+
const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;
|
|
331
489
|
setTimeout(() => {
|
|
332
490
|
if (this.#destroyed) return resolve();
|
|
333
491
|
|
|
334
492
|
if (this.#options.type === 'single') {
|
|
335
|
-
const singleSrc =
|
|
336
|
-
this.#loadBackground(singleSrc, false).then(resolve);
|
|
493
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
494
|
+
this.#loadBackground(singleSrc, false, this.#options.transitionMode === 'crossfade').then(resolve);
|
|
337
495
|
} else if (this.#options.type === 'slider') {
|
|
338
496
|
this.#startSlider();
|
|
339
497
|
resolve();
|
|
340
498
|
}
|
|
341
|
-
},
|
|
499
|
+
}, waitTime);
|
|
342
500
|
});
|
|
343
501
|
}
|
|
344
502
|
|
|
@@ -352,14 +510,11 @@ class ChameleonBackgrounds {
|
|
|
352
510
|
const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
|
|
353
511
|
this.#options = { ...this.#options, ...normalized };
|
|
354
512
|
|
|
355
|
-
|
|
356
|
-
if
|
|
357
|
-
|
|
358
|
-
this.#
|
|
513
|
+
this.#updateCSSVariables();
|
|
514
|
+
// Restart slider if needed
|
|
515
|
+
if (this.#options.type === 'slider' && !this.#isPaused && !this.#sliderIntervalId) {
|
|
516
|
+
this.#startSlider();
|
|
359
517
|
}
|
|
360
|
-
|
|
361
|
-
// Inject updated styles
|
|
362
|
-
this.#injectStyles();
|
|
363
518
|
}
|
|
364
519
|
|
|
365
520
|
// ---------------------------------------------------------------------------
|
|
@@ -379,52 +534,67 @@ class ChameleonBackgrounds {
|
|
|
379
534
|
}
|
|
380
535
|
|
|
381
536
|
// Load the first slide immediately
|
|
382
|
-
|
|
383
|
-
this.#loadBackground(sources[
|
|
537
|
+
this.#currentSlideIndex = 0;
|
|
538
|
+
this.#loadBackground(sources[this.#currentSlideIndex]).then(() => {
|
|
384
539
|
if (this.#destroyed) return;
|
|
385
540
|
|
|
386
|
-
|
|
541
|
+
this.#currentSlideIndex = 1;
|
|
387
542
|
if (sources.length === 1) return; // only one slide, nothing to rotate
|
|
388
543
|
|
|
389
|
-
|
|
544
|
+
this.#startSliderLoop();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
390
547
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
clearInterval(this.#sliderIntervalId);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
548
|
+
#startSliderLoop() {
|
|
549
|
+
const sources = this.#options.src;
|
|
396
550
|
|
|
397
|
-
|
|
398
|
-
|
|
551
|
+
// Parse as integers to prevent string concatenation bugs if users passed strings
|
|
552
|
+
const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
|
|
553
|
+
const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;
|
|
554
|
+
const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);
|
|
399
555
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
556
|
+
this.#sliderIntervalId = setInterval(() => {
|
|
557
|
+
if (this.#destroyed || this.#isPaused) {
|
|
558
|
+
clearInterval(this.#sliderIntervalId);
|
|
559
|
+
this.#sliderIntervalId = null;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.#cycleSliderSlide(sources[this.#currentSlideIndex]);
|
|
564
|
+
this.#currentSlideIndex++;
|
|
565
|
+
|
|
566
|
+
if (this.#currentSlideIndex >= sources.length) {
|
|
567
|
+
if (this.#options.sliderLoop) {
|
|
568
|
+
this.#currentSlideIndex = 0;
|
|
569
|
+
} else {
|
|
570
|
+
clearInterval(this.#sliderIntervalId);
|
|
571
|
+
this.#sliderIntervalId = null;
|
|
407
572
|
}
|
|
408
|
-
}
|
|
409
|
-
});
|
|
573
|
+
}
|
|
574
|
+
}, interval);
|
|
410
575
|
}
|
|
411
576
|
|
|
412
577
|
/**
|
|
413
578
|
* Cycle to a specific slide without resetting the slider instance.
|
|
414
|
-
* @param {string} src - The image URL to load.
|
|
579
|
+
* @param {string|ChameleonImageConfig} src - The image URL/config to load.
|
|
415
580
|
*/
|
|
416
581
|
#cycleSliderSlide(src) {
|
|
417
582
|
if (this.#destroyed) return;
|
|
418
583
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
loader
|
|
422
|
-
|
|
584
|
+
if (this.#options.transitionMode === 'solid') {
|
|
585
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
586
|
+
if (loader) {
|
|
587
|
+
loader.style.opacity = '1';
|
|
588
|
+
}
|
|
423
589
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
590
|
+
setTimeout(() => {
|
|
591
|
+
if (this.#destroyed) return;
|
|
592
|
+
this.#loadBackground(src, false, false);
|
|
593
|
+
}, this.#options.transitionDuration);
|
|
594
|
+
} else {
|
|
595
|
+
// Crossfade mode triggers immediately
|
|
596
|
+
this.#loadBackground(src, false, true);
|
|
597
|
+
}
|
|
428
598
|
}
|
|
429
599
|
|
|
430
600
|
// ---------------------------------------------------------------------------
|