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
|
@@ -83,7 +83,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
83
83
|
* |___/
|
|
84
84
|
*
|
|
85
85
|
* @module ChameleonBackgrounds
|
|
86
|
-
* @version
|
|
86
|
+
* @version 3.0.1
|
|
87
87
|
* @author Lennart van Ballegoij (https://weblenn.com/)
|
|
88
88
|
* @license MIT
|
|
89
89
|
* @see https://github.com/WebLenn/ChameleonBackgrounds
|
|
@@ -93,10 +93,15 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
+
* @typedef {Object} ChameleonImageConfig
|
|
97
|
+
* @property {string} url - The fallback URL.
|
|
98
|
+
* @property {string} [srcset] - Optional srcset.
|
|
99
|
+
* @property {string} [sizes] - Optional sizes.
|
|
100
|
+
*
|
|
96
101
|
* @typedef {Object} ChameleonOptions
|
|
97
102
|
* @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
|
|
98
103
|
* @property {'single'|'slider'} type - Background mode.
|
|
99
|
-
* @property {string|string[]}
|
|
104
|
+
* @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
|
|
100
105
|
* @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
|
|
101
106
|
* @property {string} [overlayImage] - Optional overlay pattern image URL.
|
|
102
107
|
* @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
|
|
@@ -104,6 +109,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
104
109
|
* @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
|
|
105
110
|
* @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
|
|
106
111
|
* @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
|
|
112
|
+
* @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.
|
|
113
|
+
* @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).
|
|
107
114
|
*/
|
|
108
115
|
|
|
109
116
|
class ChameleonBackgrounds {
|
|
@@ -119,6 +126,9 @@ class ChameleonBackgrounds {
|
|
|
119
126
|
sliderDuration: 8000,
|
|
120
127
|
sliderLoop: false,
|
|
121
128
|
fetchPriority: 'auto',
|
|
129
|
+
lazyLoad: true,
|
|
130
|
+
transitionMode: 'solid',
|
|
131
|
+
respectReducedMotion: false,
|
|
122
132
|
});
|
|
123
133
|
|
|
124
134
|
// Legacy snake_case → camelCase alias map
|
|
@@ -130,8 +140,14 @@ class ChameleonBackgrounds {
|
|
|
130
140
|
overlay_color: 'overlayColor',
|
|
131
141
|
overlay_image: 'overlayImage',
|
|
132
142
|
fetch_priority: 'fetchPriority',
|
|
143
|
+
lazy_load: 'lazyLoad',
|
|
144
|
+
transition_mode: 'transitionMode',
|
|
145
|
+
respect_reduced_motion: 'respectReducedMotion',
|
|
133
146
|
});
|
|
134
147
|
|
|
148
|
+
/** @type {boolean} */
|
|
149
|
+
static #globalStylesInjected = false;
|
|
150
|
+
|
|
135
151
|
/** @type {ChameleonOptions} */
|
|
136
152
|
#options;
|
|
137
153
|
|
|
@@ -144,12 +160,24 @@ class ChameleonBackgrounds {
|
|
|
144
160
|
/** @type {string} */
|
|
145
161
|
#originalHTML;
|
|
146
162
|
|
|
147
|
-
/** @type {HTMLStyleElement|null} */
|
|
148
|
-
#styleElement = null;
|
|
149
|
-
|
|
150
163
|
/** @type {number|null} */
|
|
151
164
|
#sliderIntervalId = null;
|
|
152
165
|
|
|
166
|
+
/** @type {number} */
|
|
167
|
+
#currentSlideIndex = 0;
|
|
168
|
+
|
|
169
|
+
/** @type {boolean} */
|
|
170
|
+
#hasLoadedFirstBackground = false;
|
|
171
|
+
|
|
172
|
+
/** @type {number} */
|
|
173
|
+
#transitionId = 0;
|
|
174
|
+
|
|
175
|
+
/** @type {boolean} */
|
|
176
|
+
#isPaused = false;
|
|
177
|
+
|
|
178
|
+
/** @type {IntersectionObserver|null} */
|
|
179
|
+
#observer = null;
|
|
180
|
+
|
|
153
181
|
/** @type {boolean} */
|
|
154
182
|
#destroyed = false;
|
|
155
183
|
|
|
@@ -181,16 +209,18 @@ class ChameleonBackgrounds {
|
|
|
181
209
|
|
|
182
210
|
// Stop any running slider
|
|
183
211
|
if (this.#sliderIntervalId !== null) {
|
|
184
|
-
|
|
212
|
+
clearTimeout(this.#sliderIntervalId);
|
|
185
213
|
this.#sliderIntervalId = null;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.#
|
|
191
|
-
this.#styleElement = null;
|
|
216
|
+
if (this.#observer) {
|
|
217
|
+
this.#observer.disconnect();
|
|
218
|
+
this.#observer = null;
|
|
192
219
|
}
|
|
193
220
|
|
|
221
|
+
// Clean up dynamic classes on host
|
|
222
|
+
this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
223
|
+
|
|
194
224
|
// Unwrap the original content instead of resetting innerHTML
|
|
195
225
|
const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
|
|
196
226
|
if (wrapper && wrapper.parentNode === this.#element) {
|
|
@@ -206,8 +236,16 @@ class ChameleonBackgrounds {
|
|
|
206
236
|
loader.remove();
|
|
207
237
|
}
|
|
208
238
|
|
|
209
|
-
// Remove
|
|
239
|
+
// Remove temporary crossfade elements
|
|
240
|
+
const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
|
|
241
|
+
crossfadeElements.forEach(el => el.remove());
|
|
242
|
+
|
|
243
|
+
// Remove inline CSS Variables and background-image
|
|
210
244
|
this.#element.style.backgroundImage = '';
|
|
245
|
+
this.#element.style.removeProperty('--cbg-duration');
|
|
246
|
+
this.#element.style.removeProperty('--cbg-overlay-color');
|
|
247
|
+
this.#element.style.removeProperty('--cbg-overlay-image');
|
|
248
|
+
this.#element.style.removeProperty('--cbg-min-overlay');
|
|
211
249
|
}
|
|
212
250
|
|
|
213
251
|
/**
|
|
@@ -223,71 +261,145 @@ class ChameleonBackgrounds {
|
|
|
223
261
|
// ---------------------------------------------------------------------------
|
|
224
262
|
|
|
225
263
|
#init() {
|
|
226
|
-
this.#
|
|
264
|
+
this.#injectGlobalStyles();
|
|
265
|
+
this.#updateCSSVariables();
|
|
227
266
|
this.#buildDOM();
|
|
228
267
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
268
|
+
// Check for prefers-reduced-motion to auto-pause slider if enabled
|
|
269
|
+
if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
270
|
+
this.#isPaused = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
|
|
274
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
275
|
+
entries.forEach((entry) => {
|
|
276
|
+
if (entry.isIntersecting) {
|
|
277
|
+
this.#observer.disconnect();
|
|
278
|
+
this.#observer = null;
|
|
279
|
+
this.#retrieveBackground();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
this.#observer.observe(this.#element);
|
|
235
284
|
} else {
|
|
236
|
-
if (
|
|
237
|
-
this.#retrieveBackground();
|
|
285
|
+
if (this.#element.matches('body')) {
|
|
286
|
+
queueMicrotask(() => this.#retrieveBackground());
|
|
238
287
|
} else {
|
|
239
|
-
|
|
288
|
+
if (document.readyState === 'complete') {
|
|
289
|
+
this.#retrieveBackground();
|
|
290
|
+
} else {
|
|
291
|
+
window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
|
|
292
|
+
}
|
|
240
293
|
}
|
|
241
294
|
}
|
|
242
295
|
}
|
|
243
296
|
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Play / Pause API
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Pause the slideshow.
|
|
303
|
+
*/
|
|
304
|
+
pause() {
|
|
305
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
306
|
+
this.#isPaused = true;
|
|
307
|
+
if (this.#sliderIntervalId !== null) {
|
|
308
|
+
clearTimeout(this.#sliderIntervalId);
|
|
309
|
+
this.#sliderIntervalId = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resume the slideshow.
|
|
315
|
+
*/
|
|
316
|
+
play() {
|
|
317
|
+
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
318
|
+
this.#isPaused = false;
|
|
319
|
+
// Only restart if not currently running
|
|
320
|
+
if (this.#sliderIntervalId === null) {
|
|
321
|
+
this.#startSliderLoop();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
244
325
|
// ---------------------------------------------------------------------------
|
|
245
326
|
// DOM Construction
|
|
246
327
|
// ---------------------------------------------------------------------------
|
|
247
328
|
|
|
248
329
|
/**
|
|
249
|
-
* Inject
|
|
330
|
+
* Inject global styles once.
|
|
250
331
|
*/
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? 'body'
|
|
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})`
|
|
260
|
-
: 'none';
|
|
332
|
+
#injectGlobalStyles() {
|
|
333
|
+
if (ChameleonBackgrounds.#globalStylesInjected) return;
|
|
334
|
+
ChameleonBackgrounds.#globalStylesInjected = true;
|
|
261
335
|
|
|
262
336
|
const css = `
|
|
263
|
-
|
|
337
|
+
.cbg-host {
|
|
264
338
|
position: relative;
|
|
265
339
|
}
|
|
266
|
-
|
|
267
|
-
|
|
340
|
+
body.cbg-host {
|
|
341
|
+
/* body naturally acts as relative for background sizing if not explicitly positioned differently */
|
|
342
|
+
}
|
|
343
|
+
.cbg-inner {
|
|
268
344
|
z-index: 2;
|
|
269
345
|
position: relative;
|
|
270
346
|
}
|
|
271
|
-
|
|
272
|
-
.cbg-loader-${uid} {
|
|
347
|
+
.cbg-loader {
|
|
273
348
|
height: 100%;
|
|
274
349
|
width: 100%;
|
|
275
|
-
position:
|
|
276
|
-
|
|
277
|
-
|
|
350
|
+
position: absolute;
|
|
351
|
+
top: 0;
|
|
352
|
+
left: 0;
|
|
353
|
+
background-image: var(--cbg-overlay-image, none);
|
|
354
|
+
background-color: var(--cbg-overlay-color, transparent);
|
|
278
355
|
opacity: 1;
|
|
279
356
|
z-index: 1;
|
|
280
|
-
transition: opacity
|
|
357
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
358
|
+
pointer-events: none;
|
|
359
|
+
}
|
|
360
|
+
body.cbg-host > .cbg-loader {
|
|
361
|
+
position: fixed;
|
|
362
|
+
}
|
|
363
|
+
.cbg-crossfade-el {
|
|
364
|
+
position: absolute;
|
|
281
365
|
top: 0;
|
|
282
366
|
left: 0;
|
|
367
|
+
height: 100%;
|
|
368
|
+
width: 100%;
|
|
369
|
+
background-size: cover;
|
|
370
|
+
background-position: center;
|
|
371
|
+
background-repeat: no-repeat;
|
|
372
|
+
z-index: 0;
|
|
373
|
+
opacity: 0;
|
|
374
|
+
transition: opacity var(--cbg-duration, 2s) ease;
|
|
375
|
+
pointer-events: none;
|
|
376
|
+
}
|
|
377
|
+
body.cbg-host > .cbg-crossfade-el {
|
|
378
|
+
position: fixed;
|
|
283
379
|
}
|
|
284
380
|
`;
|
|
285
381
|
|
|
286
382
|
const style = document.createElement('style');
|
|
287
|
-
style.
|
|
383
|
+
style.id = 'chameleon-global-styles';
|
|
288
384
|
style.textContent = css;
|
|
289
385
|
document.head.appendChild(style);
|
|
290
|
-
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Apply CSS custom properties to the host element.
|
|
390
|
+
*/
|
|
391
|
+
#updateCSSVariables() {
|
|
392
|
+
this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
393
|
+
this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
|
|
394
|
+
this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
|
|
395
|
+
|
|
396
|
+
if (this.#options.overlayImage) {
|
|
397
|
+
this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
|
|
398
|
+
} else {
|
|
399
|
+
this.#element.style.setProperty('--cbg-overlay-image', 'none');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
|
|
291
403
|
}
|
|
292
404
|
|
|
293
405
|
/**
|
|
@@ -298,6 +410,7 @@ class ChameleonBackgrounds {
|
|
|
298
410
|
|
|
299
411
|
const wrapper = document.createElement('div');
|
|
300
412
|
wrapper.id = `cbg-inner-${uid}`;
|
|
413
|
+
wrapper.classList.add('cbg-inner');
|
|
301
414
|
|
|
302
415
|
// Move all child nodes from the element into the wrapper
|
|
303
416
|
while (this.#element.firstChild) {
|
|
@@ -305,7 +418,7 @@ class ChameleonBackgrounds {
|
|
|
305
418
|
}
|
|
306
419
|
|
|
307
420
|
const loader = document.createElement('div');
|
|
308
|
-
loader.classList.add(`cbg-loader-${uid}`);
|
|
421
|
+
loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
|
|
309
422
|
|
|
310
423
|
// Append wrapper and loader
|
|
311
424
|
this.#element.appendChild(wrapper);
|
|
@@ -323,7 +436,8 @@ class ChameleonBackgrounds {
|
|
|
323
436
|
if (this.#destroyed) return;
|
|
324
437
|
|
|
325
438
|
if (this.#options.type === 'single') {
|
|
326
|
-
|
|
439
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
440
|
+
this.#loadBackground(singleSrc);
|
|
327
441
|
} else if (this.#options.type === 'slider') {
|
|
328
442
|
this.#startSlider();
|
|
329
443
|
}
|
|
@@ -331,39 +445,90 @@ class ChameleonBackgrounds {
|
|
|
331
445
|
|
|
332
446
|
/**
|
|
333
447
|
* Preload an image and apply it as the element's background.
|
|
334
|
-
*
|
|
448
|
+
* Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
|
|
449
|
+
* @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
|
|
335
450
|
* @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
|
|
451
|
+
* @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
|
|
336
452
|
* @returns {Promise<void>}
|
|
337
453
|
*/
|
|
338
|
-
#loadBackground(
|
|
454
|
+
#loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
|
|
339
455
|
if (this.#destroyed) return Promise.resolve();
|
|
340
456
|
|
|
341
457
|
return new Promise((resolve) => {
|
|
342
458
|
const img = new Image();
|
|
459
|
+
|
|
460
|
+
let urlStr = srcConfig;
|
|
461
|
+
if (typeof srcConfig === 'object' && srcConfig.url) {
|
|
462
|
+
urlStr = srcConfig.url;
|
|
463
|
+
if (srcConfig.srcset) img.srcset = srcConfig.srcset;
|
|
464
|
+
if (srcConfig.sizes) img.sizes = srcConfig.sizes;
|
|
465
|
+
}
|
|
466
|
+
|
|
343
467
|
if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
|
|
344
468
|
img.fetchPriority = this.#options.fetchPriority;
|
|
345
469
|
}
|
|
346
470
|
|
|
471
|
+
// Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
|
|
472
|
+
img.style.position = 'absolute';
|
|
473
|
+
img.style.visibility = 'hidden';
|
|
474
|
+
img.style.width = '0';
|
|
475
|
+
img.style.height = '0';
|
|
476
|
+
this.#element.appendChild(img);
|
|
477
|
+
|
|
347
478
|
img.onload = () => {
|
|
479
|
+
img.remove();
|
|
348
480
|
if (this.#destroyed) return resolve();
|
|
349
481
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
482
|
+
// `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
|
|
483
|
+
const finalUrl = img.currentSrc || urlStr;
|
|
484
|
+
|
|
485
|
+
if (isCrossfade && !isFirst) {
|
|
486
|
+
// --- Crossfade Transition Mode ---
|
|
487
|
+
const crossfadeDiv = document.createElement('div');
|
|
488
|
+
crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
|
|
489
|
+
crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
|
|
490
|
+
|
|
491
|
+
// Insert right behind the loader (or directly at start of host)
|
|
492
|
+
this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
|
|
493
|
+
|
|
494
|
+
// Trigger layout so the transition works
|
|
495
|
+
void crossfadeDiv.offsetWidth;
|
|
496
|
+
|
|
497
|
+
crossfadeDiv.style.opacity = '1';
|
|
498
|
+
|
|
499
|
+
setTimeout(() => {
|
|
500
|
+
if (this.#destroyed) return resolve();
|
|
501
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
502
|
+
this.#element.style.backgroundSize = 'cover';
|
|
503
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
504
|
+
crossfadeDiv.remove();
|
|
505
|
+
resolve();
|
|
506
|
+
}, this.#options.transitionDuration);
|
|
507
|
+
|
|
508
|
+
} else {
|
|
509
|
+
// --- Solid Transition Mode or First Load ---
|
|
510
|
+
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
511
|
+
this.#element.style.backgroundSize = 'cover';
|
|
512
|
+
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
513
|
+
|
|
514
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
515
|
+
if (loader) {
|
|
516
|
+
loader.style.opacity = String(this.#options.minOverlay);
|
|
517
|
+
}
|
|
518
|
+
this.#hasLoadedFirstBackground = true;
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
resolve();
|
|
521
|
+
}, this.#options.transitionDuration);
|
|
356
522
|
}
|
|
357
|
-
|
|
358
|
-
resolve();
|
|
359
523
|
};
|
|
360
524
|
|
|
361
525
|
img.onerror = () => {
|
|
362
|
-
|
|
363
|
-
|
|
526
|
+
img.remove();
|
|
527
|
+
console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
|
|
528
|
+
resolve(); // resolve anyway to keep logic flowing
|
|
364
529
|
};
|
|
365
530
|
|
|
366
|
-
img.src =
|
|
531
|
+
img.src = urlStr;
|
|
367
532
|
});
|
|
368
533
|
}
|
|
369
534
|
|
|
@@ -376,9 +541,11 @@ class ChameleonBackgrounds {
|
|
|
376
541
|
reloadBackground(src) {
|
|
377
542
|
if (this.#destroyed) return Promise.resolve();
|
|
378
543
|
|
|
544
|
+
this.#transitionId++;
|
|
545
|
+
|
|
379
546
|
if (src !== undefined) {
|
|
380
547
|
if (this.#options.type === 'slider' && typeof src === 'string') {
|
|
381
|
-
// If the user passes a comma-separated string, split it.
|
|
548
|
+
// If the user passes a comma-separated string, split it.
|
|
382
549
|
this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
|
|
383
550
|
} else {
|
|
384
551
|
this.#options.src = src;
|
|
@@ -387,28 +554,17 @@ class ChameleonBackgrounds {
|
|
|
387
554
|
|
|
388
555
|
// Stop any running slider
|
|
389
556
|
if (this.#sliderIntervalId !== null) {
|
|
390
|
-
|
|
557
|
+
clearTimeout(this.#sliderIntervalId);
|
|
391
558
|
this.#sliderIntervalId = null;
|
|
392
559
|
}
|
|
393
560
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
561
|
+
if (this.#options.type === 'single') {
|
|
562
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
563
|
+
return this.#cycleSliderSlide(singleSrc, this.#transitionId);
|
|
564
|
+
} else if (this.#options.type === 'slider') {
|
|
565
|
+
this.#startSlider(this.#transitionId);
|
|
566
|
+
return Promise.resolve();
|
|
397
567
|
}
|
|
398
|
-
|
|
399
|
-
return new Promise((resolve) => {
|
|
400
|
-
setTimeout(() => {
|
|
401
|
-
if (this.#destroyed) return resolve();
|
|
402
|
-
|
|
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
568
|
}
|
|
413
569
|
|
|
414
570
|
/**
|
|
@@ -421,14 +577,9 @@ class ChameleonBackgrounds {
|
|
|
421
577
|
const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
|
|
422
578
|
this.#options = { ...this.#options, ...normalized };
|
|
423
579
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.#styleElement = null;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Inject updated styles
|
|
431
|
-
this.#injectStyles();
|
|
580
|
+
this.#updateCSSVariables();
|
|
581
|
+
// In v1/v2, reloadOptions did not restart the slider; the user called reloadBackground.
|
|
582
|
+
// We intentionally do not auto-restart here to prevent concurrent race conditions.
|
|
432
583
|
}
|
|
433
584
|
|
|
434
585
|
// ---------------------------------------------------------------------------
|
|
@@ -437,8 +588,9 @@ class ChameleonBackgrounds {
|
|
|
437
588
|
|
|
438
589
|
/**
|
|
439
590
|
* Start the background slideshow.
|
|
591
|
+
* @param {number} tid - The transition ID
|
|
440
592
|
*/
|
|
441
|
-
#startSlider() {
|
|
593
|
+
#startSlider(tid = this.#transitionId) {
|
|
442
594
|
if (this.#destroyed) return;
|
|
443
595
|
|
|
444
596
|
const sources = this.#options.src;
|
|
@@ -447,53 +599,86 @@ class ChameleonBackgrounds {
|
|
|
447
599
|
return;
|
|
448
600
|
}
|
|
449
601
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.#
|
|
453
|
-
|
|
602
|
+
this.#currentSlideIndex = 0;
|
|
603
|
+
|
|
604
|
+
if (this.#hasLoadedFirstBackground) {
|
|
605
|
+
// Instance already has a background, crossfade smoothly
|
|
606
|
+
this.#cycleSliderSlide(sources[this.#currentSlideIndex], tid).then(() => {
|
|
607
|
+
if (this.#destroyed || this.#transitionId !== tid) return;
|
|
608
|
+
this.#currentSlideIndex = 1;
|
|
609
|
+
if (sources.length > 1) this.#startSliderLoop();
|
|
610
|
+
});
|
|
611
|
+
} else {
|
|
612
|
+
// First load, load immediately without solid color flash
|
|
613
|
+
this.#loadBackground(sources[this.#currentSlideIndex], true, this.#options.transitionMode === 'crossfade').then(() => {
|
|
614
|
+
if (this.#destroyed || this.#transitionId !== tid) return;
|
|
615
|
+
this.#hasLoadedFirstBackground = true;
|
|
616
|
+
this.#currentSlideIndex = 1;
|
|
617
|
+
if (sources.length > 1) this.#startSliderLoop();
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
#startSliderLoop() {
|
|
623
|
+
if (this.#destroyed || this.#isPaused) return;
|
|
454
624
|
|
|
455
|
-
|
|
456
|
-
|
|
625
|
+
// Parse as integers to prevent string concatenation bugs if users passed strings
|
|
626
|
+
const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
|
|
457
627
|
|
|
458
|
-
|
|
628
|
+
this.#sliderIntervalId = setTimeout(() => {
|
|
629
|
+
if (this.#destroyed || this.#isPaused) {
|
|
630
|
+
this.#sliderIntervalId = null;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
459
633
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
634
|
+
const tid = this.#transitionId;
|
|
635
|
+
this.#cycleSliderSlide(this.#options.src[this.#currentSlideIndex], tid).then(() => {
|
|
636
|
+
if (this.#destroyed || this.#isPaused || this.#transitionId !== tid) {
|
|
637
|
+
this.#sliderIntervalId = null;
|
|
463
638
|
return;
|
|
464
639
|
}
|
|
465
640
|
|
|
466
|
-
this.#
|
|
467
|
-
index++;
|
|
641
|
+
this.#currentSlideIndex++;
|
|
468
642
|
|
|
469
|
-
if (
|
|
643
|
+
if (this.#currentSlideIndex >= this.#options.src.length) {
|
|
470
644
|
if (this.#options.sliderLoop) {
|
|
471
|
-
|
|
645
|
+
this.#currentSlideIndex = 0;
|
|
472
646
|
} else {
|
|
473
|
-
clearInterval(this.#sliderIntervalId);
|
|
474
647
|
this.#sliderIntervalId = null;
|
|
648
|
+
return;
|
|
475
649
|
}
|
|
476
650
|
}
|
|
477
|
-
|
|
478
|
-
|
|
651
|
+
|
|
652
|
+
this.#startSliderLoop();
|
|
653
|
+
});
|
|
654
|
+
}, sDuration);
|
|
479
655
|
}
|
|
480
656
|
|
|
481
657
|
/**
|
|
482
658
|
* Cycle to a specific slide without resetting the slider instance.
|
|
483
|
-
* @param {string} src - The image URL to load.
|
|
659
|
+
* @param {string|ChameleonImageConfig} src - The image URL/config to load.
|
|
660
|
+
* @param {number} tid - The transition ID
|
|
661
|
+
* @returns {Promise<void>}
|
|
484
662
|
*/
|
|
485
|
-
#cycleSliderSlide(src) {
|
|
486
|
-
if (this.#destroyed) return;
|
|
663
|
+
#cycleSliderSlide(src, tid = this.#transitionId) {
|
|
664
|
+
if (this.#destroyed) return Promise.resolve();
|
|
487
665
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
666
|
+
return new Promise(resolve => {
|
|
667
|
+
if (this.#options.transitionMode === 'solid') {
|
|
668
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
669
|
+
if (loader) {
|
|
670
|
+
loader.style.opacity = '1';
|
|
671
|
+
}
|
|
492
672
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
if (this.#destroyed || this.#transitionId !== tid) return resolve();
|
|
675
|
+
this.#loadBackground(src, false, false).then(resolve);
|
|
676
|
+
}, this.#options.transitionDuration);
|
|
677
|
+
} else {
|
|
678
|
+
// Crossfade mode triggers immediately
|
|
679
|
+
this.#loadBackground(src, false, true).then(resolve);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
497
682
|
}
|
|
498
683
|
|
|
499
684
|
// ---------------------------------------------------------------------------
|