chameleon-backgrounds 2.1.2 → 3.0.0

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