chameleon-backgrounds 3.0.0 → 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.
|
@@ -1,348 +1,339 @@
|
|
|
1
1
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
10
|
})(this, () => {
|
|
11
|
-
|
|
11
|
+
return /******/ (() => { // webpackBootstrap
|
|
12
12
|
/******/ "use strict";
|
|
13
13
|
/******/ // The require scope
|
|
14
14
|
/******/ const __webpack_require__ = {};
|
|
15
|
-
/******/
|
|
15
|
+
/******/
|
|
16
16
|
/************************************************************************/
|
|
17
17
|
/******/ /* webpack/runtime/define property getters */
|
|
18
18
|
/******/ (() => {
|
|
19
19
|
/******/ // define getter/value functions for harmony exports
|
|
20
20
|
/******/ __webpack_require__.d = (exports, definition) => {
|
|
21
|
-
/******/ if
|
|
21
|
+
/******/ if(Array.isArray(definition)) {
|
|
22
22
|
/******/ var i = 0;
|
|
23
|
-
/******/ while
|
|
23
|
+
/******/ while(i < definition.length) {
|
|
24
24
|
/******/ var key = definition[i++];
|
|
25
25
|
/******/ var binding = definition[i++];
|
|
26
|
-
/******/ if
|
|
27
|
-
/******/ if
|
|
26
|
+
/******/ if(!__webpack_require__.o(exports, key)) {
|
|
27
|
+
/******/ if(binding === 0) {
|
|
28
28
|
/******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
|
|
29
|
-
|
|
30
|
-
} else {
|
|
29
|
+
/******/ } else {
|
|
31
30
|
/******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/******/
|
|
39
|
-
} else {
|
|
40
|
-
/******/ for (var key in definition) {
|
|
41
|
-
/******/ if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
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)) {
|
|
42
37
|
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/******/
|
|
50
|
-
};
|
|
51
|
-
/******/
|
|
52
|
-
})();
|
|
53
|
-
/******/
|
|
38
|
+
/******/ }
|
|
39
|
+
/******/ }
|
|
40
|
+
/******/ }
|
|
41
|
+
/******/ };
|
|
42
|
+
/******/ })();
|
|
43
|
+
/******/
|
|
54
44
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
55
45
|
/******/ (() => {
|
|
56
46
|
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
/******/
|
|
47
|
+
/******/ })();
|
|
48
|
+
/******/
|
|
60
49
|
/******/ /* webpack/runtime/make namespace object */
|
|
61
50
|
/******/ (() => {
|
|
62
51
|
/******/ // define __esModule on exports
|
|
63
52
|
/******/ __webpack_require__.r = (exports) => {
|
|
64
|
-
/******/ if
|
|
53
|
+
/******/ if(Symbol.toStringTag) {
|
|
65
54
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
66
|
-
|
|
67
|
-
}
|
|
55
|
+
/******/ }
|
|
68
56
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
\**************************************/
|
|
79
|
-
__webpack_require__.r(__webpack_exports__);
|
|
57
|
+
/******/ };
|
|
58
|
+
/******/ })();
|
|
59
|
+
/******/
|
|
60
|
+
/************************************************************************/
|
|
61
|
+
let __webpack_exports__ = {};
|
|
62
|
+
/*!**************************************!*\
|
|
63
|
+
!*** ./src/chameleon-backgrounds.js ***!
|
|
64
|
+
\**************************************/
|
|
65
|
+
__webpack_require__.r(__webpack_exports__);
|
|
80
66
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
81
67
|
/* harmony export */ ChameleonBackgrounds: () => (/* binding */ ChameleonBackgrounds),
|
|
82
68
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
69
|
+
/* harmony export */ });
|
|
70
|
+
/**
|
|
71
|
+
* _____ _ _
|
|
72
|
+
* / ____| | | |
|
|
73
|
+
* | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __
|
|
74
|
+
* | | | '_ \ / _` | '_ ` _ \ / _ \ |/ _ \/ _ \| '_ \
|
|
75
|
+
* | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |
|
|
76
|
+
* \_____|_| |_|\__,_|_| |_| |_|\___|_|\___|\___|_| |_|_
|
|
77
|
+
* | _ \ | | | |
|
|
78
|
+
* | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___
|
|
79
|
+
* | _ < / _` |/ __| |/ / _` | '__/ _ \| | | | '_ \ / _` / __|
|
|
80
|
+
* | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \__ \
|
|
81
|
+
* |____/ \__,_|\___|_|\_\__, |_| \___/ \__,_|_| |_|\__,_|___/
|
|
82
|
+
* __/ |
|
|
83
|
+
* |___/
|
|
84
|
+
*
|
|
85
|
+
* @module ChameleonBackgrounds
|
|
86
|
+
* @version 3.0.1
|
|
87
|
+
* @author Lennart van Ballegoij (https://weblenn.com/)
|
|
88
|
+
* @license MIT
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
|
|
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
|
+
*
|
|
101
|
+
* @typedef {Object} ChameleonOptions
|
|
102
|
+
* @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
|
|
103
|
+
* @property {'single'|'slider'} type - Background mode.
|
|
104
|
+
* @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
|
|
105
|
+
* @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
|
|
106
|
+
* @property {string} [overlayImage] - Optional overlay pattern image URL.
|
|
107
|
+
* @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
|
|
108
|
+
* @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
|
|
109
|
+
* @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
|
|
110
|
+
* @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
|
|
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).
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
class ChameleonBackgrounds {
|
|
117
|
+
/** @type {ChameleonOptions} */
|
|
118
|
+
static #DEFAULTS = Object.freeze({
|
|
119
|
+
element: 'body',
|
|
120
|
+
type: 'single',
|
|
121
|
+
src: '',
|
|
122
|
+
overlayColor: '#0f1e25',
|
|
123
|
+
overlayImage: null,
|
|
124
|
+
minOverlay: 0,
|
|
125
|
+
transitionDuration: 2000,
|
|
126
|
+
sliderDuration: 8000,
|
|
127
|
+
sliderLoop: false,
|
|
128
|
+
fetchPriority: 'auto',
|
|
129
|
+
lazyLoad: true,
|
|
130
|
+
transitionMode: 'solid',
|
|
131
|
+
respectReducedMotion: false,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Legacy snake_case → camelCase alias map
|
|
135
|
+
static #ALIASES = Object.freeze({
|
|
136
|
+
transition_duration: 'transitionDuration',
|
|
137
|
+
slider_duration: 'sliderDuration',
|
|
138
|
+
slider_loop: 'sliderLoop',
|
|
139
|
+
min_overlay: 'minOverlay',
|
|
140
|
+
overlay_color: 'overlayColor',
|
|
141
|
+
overlay_image: 'overlayImage',
|
|
142
|
+
fetch_priority: 'fetchPriority',
|
|
143
|
+
lazy_load: 'lazyLoad',
|
|
144
|
+
transition_mode: 'transitionMode',
|
|
145
|
+
respect_reduced_motion: 'respectReducedMotion',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** @type {boolean} */
|
|
149
|
+
static #globalStylesInjected = false;
|
|
150
|
+
|
|
151
|
+
/** @type {ChameleonOptions} */
|
|
152
|
+
#options;
|
|
153
|
+
|
|
154
|
+
/** @type {HTMLElement} */
|
|
155
|
+
#element;
|
|
156
|
+
|
|
157
|
+
/** @type {string} */
|
|
158
|
+
#uid;
|
|
159
|
+
|
|
160
|
+
/** @type {string} */
|
|
161
|
+
#originalHTML;
|
|
162
|
+
|
|
163
|
+
/** @type {number|null} */
|
|
164
|
+
#sliderIntervalId = null;
|
|
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
|
+
|
|
181
|
+
/** @type {boolean} */
|
|
182
|
+
#destroyed = false;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a new ChameleonBackgrounds instance.
|
|
186
|
+
* @param {Partial<ChameleonOptions>} [options]
|
|
187
|
+
*/
|
|
188
|
+
constructor(options = {}) {
|
|
189
|
+
const normalized = ChameleonBackgrounds.#normalizeOptions(options);
|
|
190
|
+
this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
|
|
191
|
+
this.#uid = ChameleonBackgrounds.#generateUID();
|
|
192
|
+
this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
|
|
193
|
+
this.#originalHTML = this.#element.innerHTML;
|
|
194
|
+
|
|
195
|
+
this.#init();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Public API
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clean up all injected DOM, styles, and intervals.
|
|
204
|
+
* Restores the target element to its original state.
|
|
205
|
+
*/
|
|
206
|
+
destroy() {
|
|
207
|
+
if (this.#destroyed) return;
|
|
208
|
+
this.#destroyed = true;
|
|
209
|
+
|
|
210
|
+
// Stop any running slider
|
|
211
|
+
if (this.#sliderIntervalId !== null) {
|
|
212
|
+
clearTimeout(this.#sliderIntervalId);
|
|
213
|
+
this.#sliderIntervalId = null;
|
|
214
|
+
}
|
|
189
215
|
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
if (this.#observer) {
|
|
217
|
+
this.#observer.disconnect();
|
|
218
|
+
this.#observer = null;
|
|
219
|
+
}
|
|
192
220
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* @param {Partial<ChameleonOptions>} [options]
|
|
196
|
-
*/
|
|
197
|
-
constructor(options = {}) {
|
|
198
|
-
const normalized = ChameleonBackgrounds.#normalizeOptions(options);
|
|
199
|
-
this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
|
|
200
|
-
this.#uid = ChameleonBackgrounds.#generateUID();
|
|
201
|
-
this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
|
|
202
|
-
this.#originalHTML = this.#element.innerHTML;
|
|
221
|
+
// Clean up dynamic classes on host
|
|
222
|
+
this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
|
|
203
223
|
|
|
204
|
-
|
|
224
|
+
// Unwrap the original content instead of resetting innerHTML
|
|
225
|
+
const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
|
|
226
|
+
if (wrapper && wrapper.parentNode === this.#element) {
|
|
227
|
+
while (wrapper.firstChild) {
|
|
228
|
+
this.#element.insertBefore(wrapper.firstChild, wrapper);
|
|
205
229
|
}
|
|
230
|
+
this.#element.removeChild(wrapper);
|
|
231
|
+
}
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
* Clean up all injected DOM, styles, and intervals.
|
|
213
|
-
* Restores the target element to its original state.
|
|
214
|
-
*/
|
|
215
|
-
destroy() {
|
|
216
|
-
if (this.#destroyed) return;
|
|
217
|
-
this.#destroyed = true;
|
|
218
|
-
|
|
219
|
-
// Stop any running slider
|
|
220
|
-
if (this.#sliderIntervalId !== null) {
|
|
221
|
-
clearInterval(this.#sliderIntervalId);
|
|
222
|
-
this.#sliderIntervalId = null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (this.#observer) {
|
|
226
|
-
this.#observer.disconnect();
|
|
227
|
-
this.#observer = null;
|
|
228
|
-
}
|
|
233
|
+
// Remove loader
|
|
234
|
+
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
235
|
+
if (loader) {
|
|
236
|
+
loader.remove();
|
|
237
|
+
}
|
|
229
238
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The resolved options (read-only copy).
|
|
253
|
+
* @returns {ChameleonOptions}
|
|
254
|
+
*/
|
|
255
|
+
get options() {
|
|
256
|
+
return { ...this.#options };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Initialization
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
#init() {
|
|
264
|
+
this.#injectGlobalStyles();
|
|
265
|
+
this.#updateCSSVariables();
|
|
266
|
+
this.#buildDOM();
|
|
267
|
+
|
|
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
|
+
}
|
|
232
272
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.#
|
|
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();
|
|
238
280
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Remove temporary crossfade elements
|
|
249
|
-
const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
|
|
250
|
-
crossfadeElements.forEach(el => el.remove());
|
|
251
|
-
|
|
252
|
-
// Remove inline CSS Variables and background-image
|
|
253
|
-
this.#element.style.backgroundImage = '';
|
|
254
|
-
this.#element.style.removeProperty('--cbg-duration');
|
|
255
|
-
this.#element.style.removeProperty('--cbg-overlay-color');
|
|
256
|
-
this.#element.style.removeProperty('--cbg-overlay-image');
|
|
257
|
-
this.#element.style.removeProperty('--cbg-min-overlay');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* The resolved options (read-only copy).
|
|
262
|
-
* @returns {ChameleonOptions}
|
|
263
|
-
*/
|
|
264
|
-
get options() {
|
|
265
|
-
return { ...this.#options };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
// Initialization
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
#init() {
|
|
273
|
-
this.#injectGlobalStyles();
|
|
274
|
-
this.#updateCSSVariables();
|
|
275
|
-
this.#buildDOM();
|
|
276
|
-
|
|
277
|
-
// Check for prefers-reduced-motion to auto-pause slider if enabled
|
|
278
|
-
if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
279
|
-
this.#isPaused = true;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
|
|
283
|
-
this.#observer = new IntersectionObserver((entries) => {
|
|
284
|
-
entries.forEach((entry) => {
|
|
285
|
-
if (entry.isIntersecting) {
|
|
286
|
-
this.#observer.disconnect();
|
|
287
|
-
this.#observer = null;
|
|
288
|
-
this.#retrieveBackground();
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
this.#observer.observe(this.#element);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
this.#observer.observe(this.#element);
|
|
284
|
+
} else {
|
|
285
|
+
if (this.#element.matches('body')) {
|
|
286
|
+
queueMicrotask(() => this.#retrieveBackground());
|
|
287
|
+
} else {
|
|
288
|
+
if (document.readyState === 'complete') {
|
|
289
|
+
this.#retrieveBackground();
|
|
293
290
|
} else {
|
|
294
|
-
|
|
295
|
-
queueMicrotask(() => this.#retrieveBackground());
|
|
296
|
-
} else {
|
|
297
|
-
if (document.readyState === 'complete') {
|
|
298
|
-
this.#retrieveBackground();
|
|
299
|
-
} else {
|
|
300
|
-
window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
|
-
// Play / Pause API
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Pause the slideshow.
|
|
312
|
-
*/
|
|
313
|
-
pause() {
|
|
314
|
-
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
315
|
-
this.#isPaused = true;
|
|
316
|
-
if (this.#sliderIntervalId !== null) {
|
|
317
|
-
clearInterval(this.#sliderIntervalId);
|
|
318
|
-
this.#sliderIntervalId = null;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Resume the slideshow.
|
|
324
|
-
*/
|
|
325
|
-
play() {
|
|
326
|
-
if (this.#destroyed || this.#options.type !== 'slider') return;
|
|
327
|
-
this.#isPaused = false;
|
|
328
|
-
// Only restart if not currently running
|
|
329
|
-
if (this.#sliderIntervalId === null) {
|
|
330
|
-
this.#startSliderLoop();
|
|
291
|
+
window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
|
|
331
292
|
}
|
|
332
293
|
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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
|
+
}
|
|
333
324
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// DOM Construction
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
337
328
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Inject global styles once.
|
|
331
|
+
*/
|
|
332
|
+
#injectGlobalStyles() {
|
|
333
|
+
if (ChameleonBackgrounds.#globalStylesInjected) return;
|
|
334
|
+
ChameleonBackgrounds.#globalStylesInjected = true;
|
|
344
335
|
|
|
345
|
-
|
|
336
|
+
const css = `
|
|
346
337
|
.cbg-host {
|
|
347
338
|
position: relative;
|
|
348
339
|
}
|
|
@@ -388,356 +379,364 @@
|
|
|
388
379
|
}
|
|
389
380
|
`;
|
|
390
381
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
|
|
412
|
-
}
|
|
382
|
+
const style = document.createElement('style');
|
|
383
|
+
style.id = 'chameleon-global-styles';
|
|
384
|
+
style.textContent = css;
|
|
385
|
+
document.head.appendChild(style);
|
|
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
|
+
}
|
|
413
401
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*/
|
|
417
|
-
#buildDOM() {
|
|
418
|
-
const uid = this.#uid;
|
|
402
|
+
this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
|
|
403
|
+
}
|
|
419
404
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Wrap existing content and insert the overlay loader.
|
|
407
|
+
*/
|
|
408
|
+
#buildDOM() {
|
|
409
|
+
const uid = this.#uid;
|
|
423
410
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
411
|
+
const wrapper = document.createElement('div');
|
|
412
|
+
wrapper.id = `cbg-inner-${uid}`;
|
|
413
|
+
wrapper.classList.add('cbg-inner');
|
|
428
414
|
|
|
429
|
-
|
|
430
|
-
|
|
415
|
+
// Move all child nodes from the element into the wrapper
|
|
416
|
+
while (this.#element.firstChild) {
|
|
417
|
+
wrapper.appendChild(this.#element.firstChild);
|
|
418
|
+
}
|
|
431
419
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
420
|
+
const loader = document.createElement('div');
|
|
421
|
+
loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
|
|
422
|
+
|
|
423
|
+
// Append wrapper and loader
|
|
424
|
+
this.#element.appendChild(wrapper);
|
|
425
|
+
this.#element.appendChild(loader);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Background Loading
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Route to the correct loader based on type.
|
|
434
|
+
*/
|
|
435
|
+
#retrieveBackground() {
|
|
436
|
+
if (this.#destroyed) return;
|
|
437
|
+
|
|
438
|
+
if (this.#options.type === 'single') {
|
|
439
|
+
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
440
|
+
this.#loadBackground(singleSrc);
|
|
441
|
+
} else if (this.#options.type === 'slider') {
|
|
442
|
+
this.#startSlider();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Preload an image and apply it as the element's background.
|
|
448
|
+
* Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
|
|
449
|
+
* @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
|
|
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.
|
|
452
|
+
* @returns {Promise<void>}
|
|
453
|
+
*/
|
|
454
|
+
#loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
|
|
455
|
+
if (this.#destroyed) return Promise.resolve();
|
|
456
|
+
|
|
457
|
+
return new Promise((resolve) => {
|
|
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;
|
|
435
465
|
}
|
|
436
466
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// ---------------------------------------------------------------------------
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Route to the correct loader based on type.
|
|
443
|
-
*/
|
|
444
|
-
#retrieveBackground() {
|
|
445
|
-
if (this.#destroyed) return;
|
|
446
|
-
|
|
447
|
-
if (this.#options.type === 'single') {
|
|
448
|
-
const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
|
|
449
|
-
this.#loadBackground(singleSrc);
|
|
450
|
-
} else if (this.#options.type === 'slider') {
|
|
451
|
-
this.#startSlider();
|
|
452
|
-
}
|
|
467
|
+
if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
|
|
468
|
+
img.fetchPriority = this.#options.fetchPriority;
|
|
453
469
|
}
|
|
454
470
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
* @returns {Promise<void>}
|
|
462
|
-
*/
|
|
463
|
-
#loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
|
|
464
|
-
if (this.#destroyed) return Promise.resolve();
|
|
465
|
-
|
|
466
|
-
return new Promise((resolve) => {
|
|
467
|
-
const img = new Image();
|
|
468
|
-
|
|
469
|
-
let urlStr = srcConfig;
|
|
470
|
-
if (typeof srcConfig === 'object' && srcConfig.url) {
|
|
471
|
-
urlStr = srcConfig.url;
|
|
472
|
-
if (srcConfig.srcset) img.srcset = srcConfig.srcset;
|
|
473
|
-
if (srcConfig.sizes) img.sizes = srcConfig.sizes;
|
|
474
|
-
}
|
|
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);
|
|
475
477
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
img.onload = () => {
|
|
479
|
+
img.remove();
|
|
480
|
+
if (this.#destroyed) return resolve();
|
|
479
481
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
img.style.visibility = 'hidden';
|
|
483
|
-
img.style.width = '0';
|
|
484
|
-
img.style.height = '0';
|
|
485
|
-
this.#element.appendChild(img);
|
|
482
|
+
// `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
|
|
483
|
+
const finalUrl = img.currentSrc || urlStr;
|
|
486
484
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
490
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (isCrossfade && !isFirst) {
|
|
495
|
-
// --- Crossfade Transition Mode ---
|
|
496
|
-
const crossfadeDiv = document.createElement('div');
|
|
497
|
-
crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
|
|
498
|
-
crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
|
|
499
|
-
|
|
500
|
-
// Insert right behind the loader (or directly at start of host)
|
|
501
|
-
this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
|
|
502
|
-
|
|
503
|
-
// Trigger layout so the transition works
|
|
504
|
-
void crossfadeDiv.offsetWidth;
|
|
505
|
-
|
|
506
|
-
crossfadeDiv.style.opacity = '1';
|
|
507
|
-
|
|
508
|
-
setTimeout(() => {
|
|
509
|
-
if (this.#destroyed) return resolve();
|
|
510
|
-
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
511
|
-
this.#element.style.backgroundSize = 'cover';
|
|
512
|
-
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
513
|
-
crossfadeDiv.remove();
|
|
514
|
-
resolve();
|
|
515
|
-
}, this.#options.transitionDuration);
|
|
516
|
-
|
|
517
|
-
} else {
|
|
518
|
-
// --- Solid Transition Mode or First Load ---
|
|
519
|
-
this.#element.style.backgroundImage = `url("${finalUrl}")`;
|
|
520
|
-
this.#element.style.backgroundSize = 'cover';
|
|
521
|
-
this.#element.style.backgroundRepeat = 'no-repeat';
|
|
522
|
-
|
|
523
|
-
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
524
|
-
if (loader) {
|
|
525
|
-
loader.style.opacity = String(this.#options.minOverlay);
|
|
526
|
-
}
|
|
527
|
-
resolve();
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
img.onerror = () => {
|
|
532
|
-
img.remove();
|
|
533
|
-
console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
|
|
534
|
-
resolve(); // resolve anyway to keep logic flowing
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
img.src = urlStr;
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
/**
|
|
543
|
-
* Reload background with a fade-out → swap → fade-in cycle.
|
|
544
|
-
* @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
|
|
545
|
-
* @returns {Promise<void>}
|
|
546
|
-
*/
|
|
547
|
-
reloadBackground(src) {
|
|
548
|
-
if (this.#destroyed) return Promise.resolve();
|
|
549
|
-
|
|
550
|
-
if (src !== undefined) {
|
|
551
|
-
if (this.#options.type === 'slider' && typeof src === 'string') {
|
|
552
|
-
// If the user passes a comma-separated string, split it.
|
|
553
|
-
this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
|
|
554
|
-
} else {
|
|
555
|
-
this.#options.src = src;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
491
|
+
// Insert right behind the loader (or directly at start of host)
|
|
492
|
+
this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
|
|
558
493
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
clearInterval(this.#sliderIntervalId);
|
|
562
|
-
this.#sliderIntervalId = null;
|
|
563
|
-
}
|
|
494
|
+
// Trigger layout so the transition works
|
|
495
|
+
void crossfadeDiv.offsetWidth;
|
|
564
496
|
|
|
565
|
-
|
|
566
|
-
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
567
|
-
if (loader && this.#options.transitionMode === 'solid') {
|
|
568
|
-
loader.style.opacity = '1';
|
|
569
|
-
}
|
|
497
|
+
crossfadeDiv.style.opacity = '1';
|
|
570
498
|
|
|
571
|
-
return new Promise((resolve) => {
|
|
572
|
-
const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;
|
|
573
499
|
setTimeout(() => {
|
|
574
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);
|
|
575
507
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
resolve();
|
|
582
|
-
}
|
|
583
|
-
}, waitTime);
|
|
584
|
-
});
|
|
585
|
-
}
|
|
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';
|
|
586
513
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
593
524
|
|
|
594
|
-
|
|
595
|
-
|
|
525
|
+
img.onerror = () => {
|
|
526
|
+
img.remove();
|
|
527
|
+
console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
|
|
528
|
+
resolve(); // resolve anyway to keep logic flowing
|
|
529
|
+
};
|
|
596
530
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
531
|
+
img.src = urlStr;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
/**
|
|
537
|
+
* Reload background with a fade-out → swap → fade-in cycle.
|
|
538
|
+
* @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
|
|
539
|
+
* @returns {Promise<void>}
|
|
540
|
+
*/
|
|
541
|
+
reloadBackground(src) {
|
|
542
|
+
if (this.#destroyed) return Promise.resolve();
|
|
543
|
+
|
|
544
|
+
this.#transitionId++;
|
|
545
|
+
|
|
546
|
+
if (src !== undefined) {
|
|
547
|
+
if (this.#options.type === 'slider' && typeof src === 'string') {
|
|
548
|
+
// If the user passes a comma-separated string, split it.
|
|
549
|
+
this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
|
|
550
|
+
} else {
|
|
551
|
+
this.#options.src = src;
|
|
602
552
|
}
|
|
553
|
+
}
|
|
603
554
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
555
|
+
// Stop any running slider
|
|
556
|
+
if (this.#sliderIntervalId !== null) {
|
|
557
|
+
clearTimeout(this.#sliderIntervalId);
|
|
558
|
+
this.#sliderIntervalId = null;
|
|
559
|
+
}
|
|
607
560
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Update options on the fly without destroying the instance.
|
|
572
|
+
* @param {Object} newOptions - The new options to merge.
|
|
573
|
+
*/
|
|
574
|
+
reloadOptions(newOptions) {
|
|
575
|
+
if (this.#destroyed) return;
|
|
576
|
+
|
|
577
|
+
const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
|
|
578
|
+
this.#options = { ...this.#options, ...normalized };
|
|
579
|
+
|
|
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.
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Slider
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Start the background slideshow.
|
|
591
|
+
* @param {number} tid - The transition ID
|
|
592
|
+
*/
|
|
593
|
+
#startSlider(tid = this.#transitionId) {
|
|
594
|
+
if (this.#destroyed) return;
|
|
595
|
+
|
|
596
|
+
const sources = this.#options.src;
|
|
597
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
598
|
+
console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
613
601
|
|
|
614
|
-
|
|
615
|
-
if (!Array.isArray(sources) || sources.length === 0) {
|
|
616
|
-
console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
602
|
+
this.#currentSlideIndex = 0;
|
|
619
603
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
}
|
|
624
621
|
|
|
625
|
-
|
|
626
|
-
|
|
622
|
+
#startSliderLoop() {
|
|
623
|
+
if (this.#destroyed || this.#isPaused) return;
|
|
627
624
|
|
|
628
|
-
|
|
629
|
-
|
|
625
|
+
// Parse as integers to prevent string concatenation bugs if users passed strings
|
|
626
|
+
const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
|
|
627
|
+
|
|
628
|
+
this.#sliderIntervalId = setTimeout(() => {
|
|
629
|
+
if (this.#destroyed || this.#isPaused) {
|
|
630
|
+
this.#sliderIntervalId = null;
|
|
631
|
+
return;
|
|
630
632
|
}
|
|
631
633
|
|
|
632
|
-
|
|
633
|
-
|
|
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;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
634
640
|
|
|
635
|
-
|
|
636
|
-
const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
|
|
637
|
-
const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;
|
|
638
|
-
const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);
|
|
641
|
+
this.#currentSlideIndex++;
|
|
639
642
|
|
|
640
|
-
this.#
|
|
641
|
-
if (this.#
|
|
642
|
-
|
|
643
|
+
if (this.#currentSlideIndex >= this.#options.src.length) {
|
|
644
|
+
if (this.#options.sliderLoop) {
|
|
645
|
+
this.#currentSlideIndex = 0;
|
|
646
|
+
} else {
|
|
643
647
|
this.#sliderIntervalId = null;
|
|
644
648
|
return;
|
|
645
649
|
}
|
|
646
|
-
|
|
647
|
-
this.#cycleSliderSlide(sources[this.#currentSlideIndex]);
|
|
648
|
-
this.#currentSlideIndex++;
|
|
649
|
-
|
|
650
|
-
if (this.#currentSlideIndex >= sources.length) {
|
|
651
|
-
if (this.#options.sliderLoop) {
|
|
652
|
-
this.#currentSlideIndex = 0;
|
|
653
|
-
} else {
|
|
654
|
-
clearInterval(this.#sliderIntervalId);
|
|
655
|
-
this.#sliderIntervalId = null;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}, interval);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Cycle to a specific slide without resetting the slider instance.
|
|
663
|
-
* @param {string|ChameleonImageConfig} src - The image URL/config to load.
|
|
664
|
-
*/
|
|
665
|
-
#cycleSliderSlide(src) {
|
|
666
|
-
if (this.#destroyed) return;
|
|
667
|
-
|
|
668
|
-
if (this.#options.transitionMode === 'solid') {
|
|
669
|
-
const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
|
|
670
|
-
if (loader) {
|
|
671
|
-
loader.style.opacity = '1';
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
setTimeout(() => {
|
|
675
|
-
if (this.#destroyed) return;
|
|
676
|
-
this.#loadBackground(src, false, false);
|
|
677
|
-
}, this.#options.transitionDuration);
|
|
678
|
-
} else {
|
|
679
|
-
// Crossfade mode triggers immediately
|
|
680
|
-
this.#loadBackground(src, false, true);
|
|
681
650
|
}
|
|
682
|
-
}
|
|
683
651
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
* Resolve a CSS selector string or HTMLElement to an HTMLElement.
|
|
704
|
-
* @param {string|HTMLElement} el
|
|
705
|
-
* @returns {HTMLElement}
|
|
706
|
-
*/
|
|
707
|
-
static #resolveElement(el) {
|
|
708
|
-
if (el instanceof HTMLElement) return el;
|
|
709
|
-
if (typeof el === 'string') {
|
|
710
|
-
if (el === 'body') return document.body;
|
|
711
|
-
const found = document.querySelector(el);
|
|
712
|
-
if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
|
|
713
|
-
return /** @type {HTMLElement} */ (found);
|
|
652
|
+
this.#startSliderLoop();
|
|
653
|
+
});
|
|
654
|
+
}, sDuration);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Cycle to a specific slide without resetting the slider instance.
|
|
659
|
+
* @param {string|ChameleonImageConfig} src - The image URL/config to load.
|
|
660
|
+
* @param {number} tid - The transition ID
|
|
661
|
+
* @returns {Promise<void>}
|
|
662
|
+
*/
|
|
663
|
+
#cycleSliderSlide(src, tid = this.#transitionId) {
|
|
664
|
+
if (this.#destroyed) return Promise.resolve();
|
|
665
|
+
|
|
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';
|
|
714
671
|
}
|
|
715
|
-
throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
|
|
716
|
-
}
|
|
717
672
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
return crypto.randomUUID().slice(0, 8);
|
|
726
|
-
}
|
|
727
|
-
// Fallback: random alphanumeric string
|
|
728
|
-
return Array.from({ length: 8 }, () =>
|
|
729
|
-
'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
|
|
730
|
-
).join('');
|
|
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);
|
|
731
680
|
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Static Helpers
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Convert legacy snake_case option keys to camelCase.
|
|
690
|
+
* @param {Object} opts
|
|
691
|
+
* @returns {Object}
|
|
692
|
+
*/
|
|
693
|
+
static #normalizeOptions(opts) {
|
|
694
|
+
const result = {};
|
|
695
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
696
|
+
const alias = ChameleonBackgrounds.#ALIASES[key];
|
|
697
|
+
result[alias ?? key] = value;
|
|
698
|
+
}
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Resolve a CSS selector string or HTMLElement to an HTMLElement.
|
|
704
|
+
* @param {string|HTMLElement} el
|
|
705
|
+
* @returns {HTMLElement}
|
|
706
|
+
*/
|
|
707
|
+
static #resolveElement(el) {
|
|
708
|
+
if (el instanceof HTMLElement) return el;
|
|
709
|
+
if (typeof el === 'string') {
|
|
710
|
+
if (el === 'body') return document.body;
|
|
711
|
+
const found = document.querySelector(el);
|
|
712
|
+
if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
|
|
713
|
+
return /** @type {HTMLElement} */ (found);
|
|
714
|
+
}
|
|
715
|
+
throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Generate a short unique ID for scoping styles.
|
|
720
|
+
* @returns {string}
|
|
721
|
+
*/
|
|
722
|
+
static #generateUID() {
|
|
723
|
+
// Prefer crypto.randomUUID where available, otherwise fall back
|
|
724
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
725
|
+
return crypto.randomUUID().slice(0, 8);
|
|
732
726
|
}
|
|
727
|
+
// Fallback: random alphanumeric string
|
|
728
|
+
return Array.from({ length: 8 }, () =>
|
|
729
|
+
'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
|
|
730
|
+
).join('');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
733
|
|
|
734
734
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ChameleonBackgrounds);
|
|
735
735
|
|
|
736
736
|
|
|
737
|
-
|
|
737
|
+
__webpack_exports__ = __webpack_exports__["default"];
|
|
738
738
|
/******/ return __webpack_exports__;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
;
|
|
739
|
+
/******/ })()
|
|
740
|
+
;
|
|
742
741
|
});
|
|
743
742
|
//# sourceMappingURL=chameleon-backgrounds.js.map
|