chameleon-backgrounds 2.1.3 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -11
- package/dist/chameleon-backgrounds.js +299 -114
- package/dist/chameleon-backgrounds.js.map +1 -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 +299 -114
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* |___/
|
|
15
15
|
*
|
|
16
16
|
* @module ChameleonBackgrounds
|
|
17
|
-
* @version
|
|
17
|
+
* @version 3.0.1
|
|
18
18
|
* @author Lennart van Ballegoij (https://weblenn.com/)
|
|
19
19
|
* @license MIT
|
|
20
20
|
* @see https://github.com/WebLenn/ChameleonBackgrounds
|
|
@@ -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,24 @@ 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
|
+
#hasLoadedFirstBackground = false;
|
|
102
|
+
|
|
103
|
+
/** @type {number} */
|
|
104
|
+
#transitionId = 0;
|
|
105
|
+
|
|
106
|
+
/** @type {boolean} */
|
|
107
|
+
#isPaused = false;
|
|
108
|
+
|
|
109
|
+
/** @type {IntersectionObserver|null} */
|
|
110
|
+
#observer = null;
|
|
111
|
+
|
|
84
112
|
/** @type {boolean} */
|
|
85
113
|
#destroyed = false;
|
|
86
114
|
|
|
@@ -112,16 +140,18 @@ class ChameleonBackgrounds {
|
|
|
112
140
|
|
|
113
141
|
// Stop any running slider
|
|
114
142
|
if (this.#sliderIntervalId !== null) {
|
|
115
|
-
|
|
143
|
+
clearTimeout(this.#sliderIntervalId);
|
|
116
144
|
this.#sliderIntervalId = null;
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.#
|
|
122
|
-
this.#styleElement = null;
|
|
147
|
+
if (this.#observer) {
|
|
148
|
+
this.#observer.disconnect();
|
|
149
|
+
this.#observer = null;
|
|
123
150
|
}
|
|
124
151
|
|
|
152
|
+
// Clean up dynamic classes on host
|
|
153
|
+
this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
154
|
+
|
|
125
155
|
// Unwrap the original content instead of resetting innerHTML
|
|
126
156
|
const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
|
|
127
157
|
if (wrapper && wrapper.parentNode === this.#element) {
|
|
@@ -137,8 +167,16 @@ class ChameleonBackgrounds {
|
|
|
137
167
|
loader.remove();
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
// Remove
|
|
170
|
+
// Remove temporary crossfade elements
|
|
171
|
+
const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
|
|
172
|
+
crossfadeElements.forEach(el => el.remove());
|
|
173
|
+
|
|
174
|
+
// Remove inline CSS Variables and background-image
|
|
141
175
|
this.#element.style.backgroundImage = '';
|
|
176
|
+
this.#element.style.removeProperty('--cbg-duration');
|
|
177
|
+
this.#element.style.removeProperty('--cbg-overlay-color');
|
|
178
|
+
this.#element.style.removeProperty('--cbg-overlay-image');
|
|
179
|
+
this.#element.style.removeProperty('--cbg-min-overlay');
|
|
142
180
|
}
|
|
143
181
|
|
|
144
182
|
/**
|
|
@@ -154,71 +192,145 @@ class ChameleonBackgrounds {
|
|
|
154
192
|
// ---------------------------------------------------------------------------
|
|
155
193
|
|
|
156
194
|
#init() {
|
|
157
|
-
this.#
|
|
195
|
+
this.#injectGlobalStyles();
|
|
196
|
+
this.#updateCSSVariables();
|
|
158
197
|
this.#buildDOM();
|
|
159
198
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
// Check for prefers-reduced-motion to auto-pause slider if enabled
|
|
200
|
+
if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
201
|
+
this.#isPaused = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
|
|
205
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
206
|
+
entries.forEach((entry) => {
|
|
207
|
+
if (entry.isIntersecting) {
|
|
208
|
+
this.#observer.disconnect();
|
|
209
|
+
this.#observer = null;
|
|
210
|
+
this.#retrieveBackground();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
this.#observer.observe(this.#element);
|
|
166
215
|
} else {
|
|
167
|
-
if (
|
|
168
|
-
this.#retrieveBackground();
|
|
216
|
+
if (this.#element.matches('body')) {
|
|
217
|
+
queueMicrotask(() => this.#retrieveBackground());
|
|
169
218
|
} else {
|
|
170
|
-
|
|
219
|
+
if (document.readyState === 'complete') {
|
|
220
|
+
this.#retrieveBackground();
|
|
221
|
+
} else {
|
|
222
|
+
window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
|
|
223
|
+
}
|
|
171
224
|
}
|
|
172
225
|
}
|
|
173
226
|
}
|
|
174
227
|
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Play / Pause API
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Pause the slideshow.
|
|
234
|
+
*/
|
|
235
|
+
pause() {
|
|
236
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
237
|
+
this.#isPaused = true;
|
|
238
|
+
if (this.#sliderIntervalId !== null) {
|
|
239
|
+
clearTimeout(this.#sliderIntervalId);
|
|
240
|
+
this.#sliderIntervalId = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resume the slideshow.
|
|
246
|
+
*/
|
|
247
|
+
play() {
|
|
248
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
249
|
+
this.#isPaused = false;
|
|
250
|
+
// Only restart if not currently running
|
|
251
|
+
if (this.#sliderIntervalId === null) {
|
|
252
|
+
this.#startSliderLoop();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
175
256
|
// ---------------------------------------------------------------------------
|
|
176
257
|
// DOM Construction
|
|
177
258
|
// ---------------------------------------------------------------------------
|
|
178
259
|
|
|
179
260
|
/**
|
|
180
|
-
* Inject
|
|
261
|
+
* Inject global styles once.
|
|
181
262
|
*/
|
|
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';
|
|
263
|
+
#injectGlobalStyles() {
|
|
264
|
+
if (ChameleonBackgrounds.#globalStylesInjected) return;
|
|
265
|
+
ChameleonBackgrounds.#globalStylesInjected = true;
|
|
192
266
|
|
|
193
267
|
const css = `
|
|
194
|
-
|
|
268
|
+
.cbg-host {
|
|
195
269
|
position: relative;
|
|
196
270
|
}
|
|
197
|
-
|
|
198
|
-
|
|
271
|
+
body.cbg-host {
|
|
272
|
+
/* body naturally acts as relative for background sizing if not explicitly positioned differently */
|
|
273
|
+
}
|
|
274
|
+
.cbg-inner {
|
|
199
275
|
z-index: 2;
|
|
200
276
|
position: relative;
|
|
201
277
|
}
|
|
202
|
-
|
|
203
|
-
.cbg-loader-${uid} {
|
|
278
|
+
.cbg-loader {
|
|
204
279
|
height: 100%;
|
|
205
280
|
width: 100%;
|
|
206
|
-
position:
|
|
207
|
-
|
|
208
|
-
|
|
281
|
+
position: absolute;
|
|
282
|
+
top: 0;
|
|
283
|
+
left: 0;
|
|
284
|
+
background-image: var(--cbg-overlay-image, none);
|
|
285
|
+
background-color: var(--cbg-overlay-color, transparent);
|
|
209
286
|
opacity: 1;
|
|
210
287
|
z-index: 1;
|
|
211
|
-
transition: opacity
|
|
288
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
289
|
+
pointer-events: none;
|
|
290
|
+
}
|
|
291
|
+
body.cbg-host > .cbg-loader {
|
|
292
|
+
position: fixed;
|
|
293
|
+
}
|
|
294
|
+
.cbg-crossfade-el {
|
|
295
|
+
position: absolute;
|
|
212
296
|
top: 0;
|
|
213
297
|
left: 0;
|
|
298
|
+
height: 100%;
|
|
299
|
+
width: 100%;
|
|
300
|
+
background-size: cover;
|
|
301
|
+
background-position: center;
|
|
302
|
+
background-repeat: no-repeat;
|
|
303
|
+
z-index: 0;
|
|
304
|
+
opacity: 0;
|
|
305
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
306
|
+
pointer-events: none;
|
|
307
|
+
}
|
|
308
|
+
body.cbg-host > .cbg-crossfade-el {
|
|
309
|
+
position: fixed;
|
|
214
310
|
}
|
|
215
311
|
`;
|
|
216
312
|
|
|
217
313
|
const style = document.createElement('style');
|
|
218
|
-
style.
|
|
314
|
+
style.id = 'chameleon-global-styles';
|
|
219
315
|
style.textContent = css;
|
|
220
316
|
document.head.appendChild(style);
|
|
221
|
-
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Apply CSS custom properties to the host element.
|
|
321
|
+
*/
|
|
322
|
+
#updateCSSVariables() {
|
|
323
|
+
this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
324
|
+
this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
|
|
325
|
+
this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
|
|
326
|
+
|
|
327
|
+
if (this.#options.overlayImage) {
|
|
328
|
+
this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
|
|
329
|
+
} else {
|
|
330
|
+
this.#element.style.setProperty('--cbg-overlay-image', 'none');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
|
|
222
334
|
}
|
|
223
335
|
|
|
224
336
|
/**
|
|
@@ -229,6 +341,7 @@ class ChameleonBackgrounds {
|
|
|
229
341
|
|
|
230
342
|
const wrapper = document.createElement('div');
|
|
231
343
|
wrapper.id = `cbg-inner-${uid}`;
|
|
344
|
+
wrapper.classList.add('cbg-inner');
|
|
232
345
|
|
|
233
346
|
// Move all child nodes from the element into the wrapper
|
|
234
347
|
while (this.#element.firstChild) {
|
|
@@ -236,7 +349,7 @@ class ChameleonBackgrounds {
|
|
|
236
349
|
}
|
|
237
350
|
|
|
238
351
|
const loader = document.createElement('div');
|
|
239
|
-
loader.classList.add(`cbg-loader-${uid}`);
|
|
352
|
+
loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
|
|
240
353
|
|
|
241
354
|
// Append wrapper and loader
|
|
242
355
|
this.#element.appendChild(wrapper);
|
|
@@ -254,7 +367,8 @@ class ChameleonBackgrounds {
|
|
|
254
367
|
if (this.#destroyed) return;
|
|
255
368
|
|
|
256
369
|
if (this.#options.type === 'single') {
|
|
257
|
-
|
|
370
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
371
|
+
this.#loadBackground(singleSrc);
|
|
258
372
|
} else if (this.#options.type === 'slider') {
|
|
259
373
|
this.#startSlider();
|
|
260
374
|
}
|
|
@@ -262,39 +376,90 @@ class ChameleonBackgrounds {
|
|
|
262
376
|
|
|
263
377
|
/**
|
|
264
378
|
* Preload an image and apply it as the element's background.
|
|
265
|
-
*
|
|
379
|
+
* Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
|
|
380
|
+
* @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
|
|
266
381
|
* @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
|
|
382
|
+
* @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
|
|
267
383
|
* @returns {Promise<void>}
|
|
268
384
|
*/
|
|
269
|
-
#loadBackground(
|
|
385
|
+
#loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
|
|
270
386
|
if (this.#destroyed) return Promise.resolve();
|
|
271
387
|
|
|
272
388
|
return new Promise((resolve) => {
|
|
273
389
|
const img = new Image();
|
|
390
|
+
|
|
391
|
+
let urlStr = srcConfig;
|
|
392
|
+
if (typeof srcConfig === 'object' && srcConfig.url) {
|
|
393
|
+
urlStr = srcConfig.url;
|
|
394
|
+
if (srcConfig.srcset) img.srcset = srcConfig.srcset;
|
|
395
|
+
if (srcConfig.sizes) img.sizes = srcConfig.sizes;
|
|
396
|
+
}
|
|
397
|
+
|
|
274
398
|
if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
|
|
275
399
|
img.fetchPriority = this.#options.fetchPriority;
|
|
276
400
|
}
|
|
277
401
|
|
|
402
|
+
// Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
|
|
403
|
+
img.style.position = 'absolute';
|
|
404
|
+
img.style.visibility = 'hidden';
|
|
405
|
+
img.style.width = '0';
|
|
406
|
+
img.style.height = '0';
|
|
407
|
+
this.#element.appendChild(img);
|
|
408
|
+
|
|
278
409
|
img.onload = () => {
|
|
410
|
+
img.remove();
|
|
279
411
|
if (this.#destroyed) return resolve();
|
|
280
412
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
413
|
+
// `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
|
|
414
|
+
const finalUrl = img.currentSrc || urlStr;
|
|
415
|
+
|
|
416
|
+
if (isCrossfade && !isFirst) {
|
|
417
|
+
// --- Crossfade Transition Mode ---
|
|
418
|
+
const crossfadeDiv = document.createElement('div');
|
|
419
|
+
crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
|
|
420
|
+
crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
|
|
421
|
+
|
|
422
|
+
// Insert right behind the loader (or directly at start of host)
|
|
423
|
+
this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
|
|
424
|
+
|
|
425
|
+
// Trigger layout so the transition works
|
|
426
|
+
void crossfadeDiv.offsetWidth;
|
|
427
|
+
|
|
428
|
+
crossfadeDiv.style.opacity = '1';
|
|
429
|
+
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
if (this.#destroyed) return resolve();
|
|
432
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
433
|
+
this.#element.style.backgroundSize = 'cover';
|
|
434
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
435
|
+
crossfadeDiv.remove();
|
|
436
|
+
resolve();
|
|
437
|
+
}, this.#options.transitionDuration);
|
|
438
|
+
|
|
439
|
+
} else {
|
|
440
|
+
// --- Solid Transition Mode or First Load ---
|
|
441
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
442
|
+
this.#element.style.backgroundSize = 'cover';
|
|
443
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
444
|
+
|
|
445
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
446
|
+
if (loader) {
|
|
447
|
+
loader.style.opacity = String(this.#options.minOverlay);
|
|
448
|
+
}
|
|
449
|
+
this.#hasLoadedFirstBackground = true;
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
resolve();
|
|
452
|
+
}, this.#options.transitionDuration);
|
|
287
453
|
}
|
|
288
|
-
|
|
289
|
-
resolve();
|
|
290
454
|
};
|
|
291
455
|
|
|
292
456
|
img.onerror = () => {
|
|
293
|
-
|
|
294
|
-
|
|
457
|
+
img.remove();
|
|
458
|
+
console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
|
|
459
|
+
resolve(); // resolve anyway to keep logic flowing
|
|
295
460
|
};
|
|
296
461
|
|
|
297
|
-
img.src =
|
|
462
|
+
img.src = urlStr;
|
|
298
463
|
});
|
|
299
464
|
}
|
|
300
465
|
|
|
@@ -307,9 +472,11 @@ class ChameleonBackgrounds {
|
|
|
307
472
|
reloadBackground(src) {
|
|
308
473
|
if (this.#destroyed) return Promise.resolve();
|
|
309
474
|
|
|
475
|
+
this.#transitionId++;
|
|
476
|
+
|
|
310
477
|
if (src !== undefined) {
|
|
311
478
|
if (this.#options.type === 'slider' && typeof src === 'string') {
|
|
312
|
-
// If the user passes a comma-separated string, split it.
|
|
479
|
+
// If the user passes a comma-separated string, split it.
|
|
313
480
|
this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
|
|
314
481
|
} else {
|
|
315
482
|
this.#options.src = src;
|
|
@@ -318,28 +485,17 @@ class ChameleonBackgrounds {
|
|
|
318
485
|
|
|
319
486
|
// Stop any running slider
|
|
320
487
|
if (this.#sliderIntervalId !== null) {
|
|
321
|
-
|
|
488
|
+
clearTimeout(this.#sliderIntervalId);
|
|
322
489
|
this.#sliderIntervalId = null;
|
|
323
490
|
}
|
|
324
491
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
492
|
+
if (this.#options.type === 'single') {
|
|
493
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
494
|
+
return this.#cycleSliderSlide(singleSrc, this.#transitionId);
|
|
495
|
+
} else if (this.#options.type === 'slider') {
|
|
496
|
+
this.#startSlider(this.#transitionId);
|
|
497
|
+
return Promise.resolve();
|
|
328
498
|
}
|
|
329
|
-
|
|
330
|
-
return new Promise((resolve) => {
|
|
331
|
-
setTimeout(() => {
|
|
332
|
-
if (this.#destroyed) return resolve();
|
|
333
|
-
|
|
334
|
-
if (this.#options.type === 'single') {
|
|
335
|
-
const singleSrc = typeof this.#options.src === 'string' ? this.#options.src : this.#options.src[0];
|
|
336
|
-
this.#loadBackground(singleSrc, false).then(resolve);
|
|
337
|
-
} else if (this.#options.type === 'slider') {
|
|
338
|
-
this.#startSlider();
|
|
339
|
-
resolve();
|
|
340
|
-
}
|
|
341
|
-
}, this.#options.transitionDuration);
|
|
342
|
-
});
|
|
343
499
|
}
|
|
344
500
|
|
|
345
501
|
/**
|
|
@@ -352,14 +508,9 @@ class ChameleonBackgrounds {
|
|
|
352
508
|
const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
|
|
353
509
|
this.#options = { ...this.#options, ...normalized };
|
|
354
510
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.#styleElement = null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Inject updated styles
|
|
362
|
-
this.#injectStyles();
|
|
511
|
+
this.#updateCSSVariables();
|
|
512
|
+
// In v1/v2, reloadOptions did not restart the slider; the user called reloadBackground.
|
|
513
|
+
// We intentionally do not auto-restart here to prevent concurrent race conditions.
|
|
363
514
|
}
|
|
364
515
|
|
|
365
516
|
// ---------------------------------------------------------------------------
|
|
@@ -368,8 +519,9 @@ class ChameleonBackgrounds {
|
|
|
368
519
|
|
|
369
520
|
/**
|
|
370
521
|
* Start the background slideshow.
|
|
522
|
+
* @param {number} tid - The transition ID
|
|
371
523
|
*/
|
|
372
|
-
#startSlider() {
|
|
524
|
+
#startSlider(tid = this.#transitionId) {
|
|
373
525
|
if (this.#destroyed) return;
|
|
374
526
|
|
|
375
527
|
const sources = this.#options.src;
|
|
@@ -378,53 +530,86 @@ class ChameleonBackgrounds {
|
|
|
378
530
|
return;
|
|
379
531
|
}
|
|
380
532
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.#
|
|
384
|
-
|
|
533
|
+
this.#currentSlideIndex = 0;
|
|
534
|
+
|
|
535
|
+
if (this.#hasLoadedFirstBackground) {
|
|
536
|
+
// Instance already has a background, crossfade smoothly
|
|
537
|
+
this.#cycleSliderSlide(sources[this.#currentSlideIndex], tid).then(() => {
|
|
538
|
+
if (this.#destroyed || this.#transitionId !== tid) return;
|
|
539
|
+
this.#currentSlideIndex = 1;
|
|
540
|
+
if (sources.length > 1) this.#startSliderLoop();
|
|
541
|
+
});
|
|
542
|
+
} else {
|
|
543
|
+
// First load, load immediately without solid color flash
|
|
544
|
+
this.#loadBackground(sources[this.#currentSlideIndex], true, this.#options.transitionMode === 'crossfade').then(() => {
|
|
545
|
+
if (this.#destroyed || this.#transitionId !== tid) return;
|
|
546
|
+
this.#hasLoadedFirstBackground = true;
|
|
547
|
+
this.#currentSlideIndex = 1;
|
|
548
|
+
if (sources.length > 1) this.#startSliderLoop();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#startSliderLoop() {
|
|
554
|
+
if (this.#destroyed || this.#isPaused) return;
|
|
385
555
|
|
|
386
|
-
|
|
387
|
-
|
|
556
|
+
// Parse as integers to prevent string concatenation bugs if users passed strings
|
|
557
|
+
const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
|
|
388
558
|
|
|
389
|
-
|
|
559
|
+
this.#sliderIntervalId = setTimeout(() => {
|
|
560
|
+
if (this.#destroyed || this.#isPaused) {
|
|
561
|
+
this.#sliderIntervalId = null;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
390
564
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
565
|
+
const tid = this.#transitionId;
|
|
566
|
+
this.#cycleSliderSlide(this.#options.src[this.#currentSlideIndex], tid).then(() => {
|
|
567
|
+
if (this.#destroyed || this.#isPaused || this.#transitionId !== tid) {
|
|
568
|
+
this.#sliderIntervalId = null;
|
|
394
569
|
return;
|
|
395
570
|
}
|
|
396
571
|
|
|
397
|
-
this.#
|
|
398
|
-
index++;
|
|
572
|
+
this.#currentSlideIndex++;
|
|
399
573
|
|
|
400
|
-
if (
|
|
574
|
+
if (this.#currentSlideIndex >= this.#options.src.length) {
|
|
401
575
|
if (this.#options.sliderLoop) {
|
|
402
|
-
|
|
576
|
+
this.#currentSlideIndex = 0;
|
|
403
577
|
} else {
|
|
404
|
-
clearInterval(this.#sliderIntervalId);
|
|
405
578
|
this.#sliderIntervalId = null;
|
|
579
|
+
return;
|
|
406
580
|
}
|
|
407
581
|
}
|
|
408
|
-
|
|
409
|
-
|
|
582
|
+
|
|
583
|
+
this.#startSliderLoop();
|
|
584
|
+
});
|
|
585
|
+
}, sDuration);
|
|
410
586
|
}
|
|
411
587
|
|
|
412
588
|
/**
|
|
413
589
|
* Cycle to a specific slide without resetting the slider instance.
|
|
414
|
-
* @param {string} src - The image URL to load.
|
|
590
|
+
* @param {string|ChameleonImageConfig} src - The image URL/config to load.
|
|
591
|
+
* @param {number} tid - The transition ID
|
|
592
|
+
* @returns {Promise<void>}
|
|
415
593
|
*/
|
|
416
|
-
#cycleSliderSlide(src) {
|
|
417
|
-
if (this.#destroyed) return;
|
|
594
|
+
#cycleSliderSlide(src, tid = this.#transitionId) {
|
|
595
|
+
if (this.#destroyed) return Promise.resolve();
|
|
418
596
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
597
|
+
return new Promise(resolve => {
|
|
598
|
+
if (this.#options.transitionMode === 'solid') {
|
|
599
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
600
|
+
if (loader) {
|
|
601
|
+
loader.style.opacity = '1';
|
|
602
|
+
}
|
|
423
603
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
604
|
+
setTimeout(() => {
|
|
605
|
+
if (this.#destroyed || this.#transitionId !== tid) return resolve();
|
|
606
|
+
this.#loadBackground(src, false, false).then(resolve);
|
|
607
|
+
}, this.#options.transitionDuration);
|
|
608
|
+
} else {
|
|
609
|
+
// Crossfade mode triggers immediately
|
|
610
|
+
this.#loadBackground(src, false, true).then(resolve);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
428
613
|
}
|
|
429
614
|
|
|
430
615
|
// ---------------------------------------------------------------------------
|