@whykusanagi/corrupted-theme 0.1.5 → 0.1.7

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,13 +1,14 @@
1
1
  /**
2
2
  * gallery.js — Gallery System with Lightbox and NSFW Support
3
- *
3
+ *
4
4
  * A complete gallery system with filtering, lightbox viewer, and NSFW content handling.
5
5
  * Integrates with the Corrupted Theme design system.
6
- *
6
+ * Supports multiple independent gallery instances on a single page.
7
+ *
7
8
  * @module gallery
8
- * @version 1.0.0
9
+ * @version 2.0.0
9
10
  * @license MIT
10
- *
11
+ *
11
12
  * Features:
12
13
  * - Responsive gallery grid
13
14
  * - Category filtering with animated transitions
@@ -15,7 +16,8 @@
15
16
  * - NSFW content blur with click-to-reveal
16
17
  * - Lazy loading support
17
18
  * - Touch gesture support for mobile
18
- *
19
+ * - Multiple galleries per page
20
+ *
19
21
  * Usage:
20
22
  * ```html
21
23
  * <div class="filter-bar">
@@ -28,14 +30,18 @@
28
30
  * <div class="gallery-caption">Caption</div>
29
31
  * </div>
30
32
  * </div>
31
- *
33
+ *
32
34
  * <script type="module">
33
- * import { initGallery } from '@whykusanagi/corrupted-theme/src/lib/gallery.js';
34
- * initGallery('#gallery');
35
+ * import { initGallery } from '@whykusanagi/corrupted-theme/gallery';
36
+ * const gallery = initGallery('#gallery');
37
+ * // gallery.destroy() when done
35
38
  * </script>
36
39
  * ```
37
40
  */
38
41
 
42
+ import { TimerRegistry } from '../core/timer-registry.js';
43
+ import { EventTracker } from '../core/event-tracker.js';
44
+
39
45
  // ============================================================================
40
46
  // CONFIGURATION
41
47
  // ============================================================================
@@ -46,23 +52,23 @@ const DEFAULT_CONFIG = {
46
52
  itemSelector: '.gallery-item',
47
53
  filterBarSelector: '.filter-bar',
48
54
  filterBtnSelector: '.filter-btn',
49
-
55
+
50
56
  // Lightbox
51
57
  enableLightbox: true,
52
58
  lightboxId: 'corrupted-lightbox',
53
-
59
+
54
60
  // NSFW
55
61
  enableNsfw: true,
56
62
  nsfwSelector: '.nsfw-content',
57
63
  nsfwWarning: '18+ Click to View',
58
-
64
+
59
65
  // Animation
60
66
  filterAnimation: true,
61
67
  animationDuration: 300,
62
-
68
+
63
69
  // Keyboard
64
70
  enableKeyboard: true,
65
-
71
+
66
72
  // Callbacks
67
73
  onFilter: null,
68
74
  onLightboxOpen: null,
@@ -70,390 +76,451 @@ const DEFAULT_CONFIG = {
70
76
  onNsfwReveal: null
71
77
  };
72
78
 
79
+ // Instance counter for unique lightbox IDs
80
+ let instanceCounter = 0;
81
+
73
82
  // ============================================================================
74
- // STATE
83
+ // GALLERY CLASS
75
84
  // ============================================================================
76
85
 
77
- let state = {
78
- currentFilter: 'all',
79
- lightboxOpen: false,
80
- currentImageIndex: 0,
81
- galleryImages: [],
82
- config: { ...DEFAULT_CONFIG }
83
- };
86
+ class Gallery {
87
+ /**
88
+ * @param {string|HTMLElement} selector - Gallery container selector or element
89
+ * @param {Object} options - Configuration options
90
+ */
91
+ constructor(selector, options = {}) {
92
+ this._id = ++instanceCounter;
93
+ this._events = new EventTracker();
94
+ this._timers = new TimerRegistry();
84
95
 
85
- // ============================================================================
86
- // LIGHTBOX
87
- // ============================================================================
96
+ this.config = { ...DEFAULT_CONFIG, ...options };
88
97
 
89
- /**
90
- * Creates and injects the lightbox HTML into the DOM
91
- * @private
92
- */
93
- function createLightbox() {
94
- if (document.getElementById(state.config.lightboxId)) return;
95
-
96
- const lightbox = document.createElement('div');
97
- lightbox.id = state.config.lightboxId;
98
- lightbox.className = 'lightbox';
99
- lightbox.innerHTML = `
100
- <button class="lightbox-close" aria-label="Close lightbox">&times;</button>
101
- <button class="lightbox-prev" aria-label="Previous image">
102
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
103
- <path d="M15 18l-6-6 6-6"/>
104
- </svg>
105
- </button>
106
- <img class="lightbox-image" src="" alt="">
107
- <button class="lightbox-next" aria-label="Next image">
108
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
109
- <path d="M9 18l6-6-6-6"/>
110
- </svg>
111
- </button>
112
- <div class="lightbox-caption"></div>
113
- <div class="lightbox-counter"></div>
114
- `;
115
-
116
- document.body.appendChild(lightbox);
117
-
118
- // Event listeners
119
- lightbox.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
120
- lightbox.querySelector('.lightbox-prev').addEventListener('click', () => navigateLightbox(-1));
121
- lightbox.querySelector('.lightbox-next').addEventListener('click', () => navigateLightbox(1));
122
- lightbox.addEventListener('click', (e) => {
123
- if (e.target === lightbox) closeLightbox();
124
- });
125
-
126
- // Touch gestures
127
- let touchStartX = 0;
128
- lightbox.addEventListener('touchstart', (e) => {
129
- touchStartX = e.touches[0].clientX;
130
- }, { passive: true });
131
-
132
- lightbox.addEventListener('touchend', (e) => {
133
- const touchEndX = e.changedTouches[0].clientX;
134
- const diff = touchStartX - touchEndX;
135
-
136
- if (Math.abs(diff) > 50) {
137
- navigateLightbox(diff > 0 ? 1 : -1);
98
+ if (typeof selector === 'string') {
99
+ this.config.gallerySelector = selector;
100
+ } else if (selector instanceof HTMLElement) {
101
+ // When passed an element directly, derive a selector from its ID
102
+ // or fall back to a unique attribute for querySelectorAll compatibility
103
+ if (selector.id) {
104
+ this.config.gallerySelector = `#${selector.id}`;
105
+ } else {
106
+ const attrKey = `data-gallery-${this._id}`;
107
+ selector.setAttribute(attrKey, '');
108
+ this.config.gallerySelector = `[${attrKey}]`;
109
+ }
138
110
  }
139
- }, { passive: true });
140
- }
141
111
 
142
- /**
143
- * Opens the lightbox with specified image
144
- * @param {number} index - Index of image in gallery
145
- */
146
- function openLightbox(index) {
147
- const lightbox = document.getElementById(state.config.lightboxId);
148
- if (!lightbox || !state.galleryImages[index]) return;
149
-
150
- state.currentImageIndex = index;
151
- state.lightboxOpen = true;
152
-
153
- const imageData = state.galleryImages[index];
154
- const img = lightbox.querySelector('.lightbox-image');
155
- const caption = lightbox.querySelector('.lightbox-caption');
156
- const counter = lightbox.querySelector('.lightbox-counter');
157
-
158
- img.src = imageData.src;
159
- img.alt = imageData.alt || '';
160
-
161
- // Handle NSFW class
162
- if (imageData.isNsfw) {
163
- img.classList.add('nsfw-revealed');
164
- } else {
165
- img.classList.remove('nsfw-revealed');
112
+ // Per-instance lightbox ID
113
+ this.config.lightboxId = `corrupted-lightbox-${this._id}`;
114
+
115
+ this.currentFilter = 'all';
116
+ this.lightboxOpen = false;
117
+ this.currentImageIndex = 0;
118
+ this.galleryImages = [];
119
+
120
+ this._init();
166
121
  }
167
-
168
- caption.textContent = imageData.caption || '';
169
- caption.style.display = imageData.caption ? 'block' : 'none';
170
-
171
- counter.textContent = `${index + 1} / ${state.galleryImages.length}`;
172
-
173
- // Update navigation buttons
174
- lightbox.querySelector('.lightbox-prev').disabled = index === 0;
175
- lightbox.querySelector('.lightbox-next').disabled = index === state.galleryImages.length - 1;
176
-
177
- lightbox.classList.add('active');
178
- document.body.style.overflow = 'hidden';
179
-
180
- if (state.config.onLightboxOpen) {
181
- state.config.onLightboxOpen(imageData, index);
122
+
123
+ // --------------------------------------------------------------------------
124
+ // INITIALIZATION
125
+ // --------------------------------------------------------------------------
126
+
127
+ /** @private */
128
+ _init() {
129
+ if (this.config.enableLightbox) {
130
+ this._createLightbox();
131
+ }
132
+
133
+ this._initFilterButtons();
134
+ this._initGalleryItems();
135
+
136
+ if (this.config.enableNsfw) {
137
+ this._initNsfwContent();
138
+ }
139
+
140
+ if (this.config.enableKeyboard) {
141
+ this._events.add(document, 'keydown', (e) => this._handleKeyboard(e));
142
+ }
143
+
144
+ this._updateGalleryImages();
182
145
  }
183
- }
184
146
 
185
- /**
186
- * Closes the lightbox
187
- */
188
- function closeLightbox() {
189
- const lightbox = document.getElementById(state.config.lightboxId);
190
- if (!lightbox) return;
191
-
192
- lightbox.classList.remove('active');
193
- document.body.style.overflow = '';
194
- state.lightboxOpen = false;
195
-
196
- if (state.config.onLightboxClose) {
197
- state.config.onLightboxClose();
147
+ // --------------------------------------------------------------------------
148
+ // LIGHTBOX
149
+ // --------------------------------------------------------------------------
150
+
151
+ /** @private */
152
+ _createLightbox() {
153
+ if (document.getElementById(this.config.lightboxId)) return;
154
+
155
+ const lightbox = document.createElement('div');
156
+ lightbox.id = this.config.lightboxId;
157
+ lightbox.className = 'lightbox';
158
+ // Static HTML only — no interpolated variables, safe from XSS
159
+ lightbox.innerHTML = `
160
+ <button class="lightbox-close" aria-label="Close lightbox">&times;</button>
161
+ <button class="lightbox-prev" aria-label="Previous image">
162
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
163
+ <path d="M15 18l-6-6 6-6"/>
164
+ </svg>
165
+ </button>
166
+ <img class="lightbox-image" src="" alt="">
167
+ <button class="lightbox-next" aria-label="Next image">
168
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
169
+ <path d="M9 18l6-6-6-6"/>
170
+ </svg>
171
+ </button>
172
+ <div class="lightbox-caption"></div>
173
+ <div class="lightbox-counter"></div>
174
+ `;
175
+
176
+ document.body.appendChild(lightbox);
177
+
178
+ // All listeners tracked for cleanup
179
+ this._events.add(lightbox.querySelector('.lightbox-close'), 'click', () => this.closeLightbox());
180
+ this._events.add(lightbox.querySelector('.lightbox-prev'), 'click', () => this.navigateLightbox(-1));
181
+ this._events.add(lightbox.querySelector('.lightbox-next'), 'click', () => this.navigateLightbox(1));
182
+ this._events.add(lightbox, 'click', (e) => {
183
+ if (e.target === lightbox) this.closeLightbox();
184
+ });
185
+
186
+ // Touch gestures
187
+ let touchStartX = 0;
188
+ this._events.add(lightbox, 'touchstart', (e) => {
189
+ touchStartX = e.touches[0].clientX;
190
+ }, { passive: true });
191
+
192
+ this._events.add(lightbox, 'touchend', (e) => {
193
+ const touchEndX = e.changedTouches[0].clientX;
194
+ const diff = touchStartX - touchEndX;
195
+ if (Math.abs(diff) > 50) {
196
+ this.navigateLightbox(diff > 0 ? 1 : -1);
197
+ }
198
+ }, { passive: true });
198
199
  }
199
- }
200
200
 
201
- /**
202
- * Navigates to next/previous image in lightbox
203
- * @param {number} direction - 1 for next, -1 for previous
204
- */
205
- function navigateLightbox(direction) {
206
- const newIndex = state.currentImageIndex + direction;
207
-
208
- if (newIndex >= 0 && newIndex < state.galleryImages.length) {
209
- openLightbox(newIndex);
201
+ /**
202
+ * Opens the lightbox with specified image
203
+ * @param {number} index - Index of image in gallery
204
+ */
205
+ openLightbox(index) {
206
+ const lightbox = document.getElementById(this.config.lightboxId);
207
+ if (!lightbox || !this.galleryImages[index]) return;
208
+
209
+ this.currentImageIndex = index;
210
+ this.lightboxOpen = true;
211
+
212
+ const imageData = this.galleryImages[index];
213
+ const img = lightbox.querySelector('.lightbox-image');
214
+ const caption = lightbox.querySelector('.lightbox-caption');
215
+ const counter = lightbox.querySelector('.lightbox-counter');
216
+
217
+ img.src = imageData.src;
218
+ img.alt = imageData.alt || '';
219
+
220
+ if (imageData.isNsfw) {
221
+ img.classList.add('nsfw-revealed');
222
+ } else {
223
+ img.classList.remove('nsfw-revealed');
224
+ }
225
+
226
+ caption.textContent = imageData.caption || '';
227
+ caption.style.display = imageData.caption ? 'block' : 'none';
228
+
229
+ counter.textContent = `${index + 1} / ${this.galleryImages.length}`;
230
+
231
+ lightbox.querySelector('.lightbox-prev').disabled = index === 0;
232
+ lightbox.querySelector('.lightbox-next').disabled = index === this.galleryImages.length - 1;
233
+
234
+ lightbox.classList.add('active');
235
+ document.body.style.overflow = 'hidden';
236
+
237
+ if (this.config.onLightboxOpen) {
238
+ this.config.onLightboxOpen(imageData, index);
239
+ }
210
240
  }
211
- }
212
241
 
213
- // ============================================================================
214
- // FILTERING
215
- // ============================================================================
242
+ /**
243
+ * Closes the lightbox
244
+ */
245
+ closeLightbox() {
246
+ const lightbox = document.getElementById(this.config.lightboxId);
247
+ if (!lightbox) return;
216
248
 
217
- /**
218
- * Filters gallery items by category
219
- * @param {string} filter - Filter value (tag name or 'all')
220
- */
221
- function filterGallery(filter) {
222
- const galleries = document.querySelectorAll(state.config.gallerySelector);
223
-
224
- galleries.forEach(gallery => {
225
- const items = gallery.querySelectorAll(state.config.itemSelector);
226
-
227
- items.forEach(item => {
228
- const tags = (item.dataset.tags || '').split(',').map(t => t.trim());
229
- const shouldShow = filter === 'all' || tags.includes(filter);
230
-
231
- if (state.config.filterAnimation) {
232
- item.style.transition = `opacity ${state.config.animationDuration}ms ease, transform ${state.config.animationDuration}ms ease`;
233
-
234
- if (shouldShow) {
235
- item.style.opacity = '0';
236
- item.style.transform = 'scale(0.9)';
237
- item.style.display = '';
238
-
239
- requestAnimationFrame(() => {
240
- item.style.opacity = '1';
241
- item.style.transform = 'scale(1)';
242
- });
249
+ lightbox.classList.remove('active');
250
+ document.body.style.overflow = '';
251
+ this.lightboxOpen = false;
252
+
253
+ if (this.config.onLightboxClose) {
254
+ this.config.onLightboxClose();
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Navigates to next/previous image in lightbox
260
+ * @param {number} direction - 1 for next, -1 for previous
261
+ */
262
+ navigateLightbox(direction) {
263
+ const newIndex = this.currentImageIndex + direction;
264
+ if (newIndex >= 0 && newIndex < this.galleryImages.length) {
265
+ this.openLightbox(newIndex);
266
+ }
267
+ }
268
+
269
+ // --------------------------------------------------------------------------
270
+ // FILTERING
271
+ // --------------------------------------------------------------------------
272
+
273
+ /**
274
+ * Filters gallery items by category
275
+ * @param {string} filter - Filter value (tag name or 'all')
276
+ */
277
+ filter(filter) {
278
+ const galleries = document.querySelectorAll(this.config.gallerySelector);
279
+
280
+ galleries.forEach(gallery => {
281
+ const items = gallery.querySelectorAll(this.config.itemSelector);
282
+
283
+ items.forEach(item => {
284
+ const tags = (item.dataset.tags || '').split(',').map(t => t.trim());
285
+ const shouldShow = filter === 'all' || tags.includes(filter);
286
+
287
+ if (this.config.filterAnimation) {
288
+ item.style.transition = `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease`;
289
+
290
+ if (shouldShow) {
291
+ item.style.opacity = '0';
292
+ item.style.transform = 'scale(0.9)';
293
+ item.style.display = '';
294
+
295
+ requestAnimationFrame(() => {
296
+ item.style.opacity = '1';
297
+ item.style.transform = 'scale(1)';
298
+ });
299
+ } else {
300
+ item.style.opacity = '0';
301
+ item.style.transform = 'scale(0.9)';
302
+
303
+ this._timers.setTimeout(() => {
304
+ item.style.display = 'none';
305
+ }, this.config.animationDuration);
306
+ }
243
307
  } else {
244
- item.style.opacity = '0';
245
- item.style.transform = 'scale(0.9)';
246
-
247
- setTimeout(() => {
248
- item.style.display = 'none';
249
- }, state.config.animationDuration);
308
+ item.style.display = shouldShow ? '' : 'none';
250
309
  }
251
- } else {
252
- item.style.display = shouldShow ? '' : 'none';
253
- }
310
+ });
254
311
  });
255
- });
256
-
257
- state.currentFilter = filter;
258
- updateGalleryImages();
259
-
260
- // Update filter button active state
261
- document.querySelectorAll(state.config.filterBtnSelector).forEach(btn => {
262
- btn.classList.toggle('active', btn.dataset.filter === filter);
263
- });
264
-
265
- if (state.config.onFilter) {
266
- state.config.onFilter(filter);
267
- }
268
- }
269
312
 
270
- // ============================================================================
271
- // NSFW HANDLING
272
- // ============================================================================
313
+ this.currentFilter = filter;
314
+ this._updateGalleryImages();
273
315
 
274
- /**
275
- * Reveals NSFW content on click
276
- * @param {HTMLElement} element - NSFW content element
277
- */
278
- function revealNsfwContent(element) {
279
- if (element.classList.contains('revealed')) return;
280
-
281
- element.classList.add('revealed');
282
-
283
- if (state.config.onNsfwReveal) {
284
- state.config.onNsfwReveal(element);
316
+ // Update filter button active state
317
+ document.querySelectorAll(this.config.filterBtnSelector).forEach(btn => {
318
+ btn.classList.toggle('active', btn.dataset.filter === filter);
319
+ });
320
+
321
+ if (this.config.onFilter) {
322
+ this.config.onFilter(filter);
323
+ }
285
324
  }
286
- }
287
325
 
288
- /**
289
- * Initializes NSFW content handling
290
- * @private
291
- */
292
- function initNsfwContent() {
293
- document.querySelectorAll(state.config.nsfwSelector).forEach(item => {
294
- if (!item.dataset.warning) {
295
- item.dataset.warning = state.config.nsfwWarning;
326
+ // --------------------------------------------------------------------------
327
+ // NSFW HANDLING
328
+ // --------------------------------------------------------------------------
329
+
330
+ /**
331
+ * Reveals NSFW content on click
332
+ * @param {HTMLElement} element - NSFW content element
333
+ */
334
+ revealNsfw(element) {
335
+ if (element.classList.contains('revealed')) return;
336
+ element.classList.add('revealed');
337
+
338
+ if (this.config.onNsfwReveal) {
339
+ this.config.onNsfwReveal(element);
296
340
  }
297
-
298
- item.addEventListener('click', (e) => {
299
- if (!item.classList.contains('revealed')) {
300
- e.preventDefault();
301
- e.stopPropagation();
302
- revealNsfwContent(item);
341
+ }
342
+
343
+ /** @private */
344
+ _initNsfwContent() {
345
+ const container = document.querySelector(this.config.gallerySelector);
346
+ if (!container) return;
347
+
348
+ container.querySelectorAll(this.config.nsfwSelector).forEach(item => {
349
+ if (!item.dataset.warning) {
350
+ item.dataset.warning = this.config.nsfwWarning;
303
351
  }
352
+
353
+ this._events.add(item, 'click', (e) => {
354
+ if (!item.classList.contains('revealed')) {
355
+ e.preventDefault();
356
+ e.stopPropagation();
357
+ this.revealNsfw(item);
358
+ }
359
+ });
304
360
  });
305
- });
306
- }
361
+ }
307
362
 
308
- // ============================================================================
309
- // KEYBOARD NAVIGATION
310
- // ============================================================================
363
+ // --------------------------------------------------------------------------
364
+ // KEYBOARD NAVIGATION
365
+ // --------------------------------------------------------------------------
311
366
 
312
- /**
313
- * Handles keyboard events for lightbox navigation
314
- * @private
315
- * @param {KeyboardEvent} e - Keyboard event
316
- */
317
- function handleKeyboard(e) {
318
- if (!state.lightboxOpen) return;
319
-
320
- switch (e.key) {
321
- case 'Escape':
322
- closeLightbox();
323
- break;
324
- case 'ArrowLeft':
325
- navigateLightbox(-1);
326
- break;
327
- case 'ArrowRight':
328
- navigateLightbox(1);
329
- break;
367
+ /** @private */
368
+ _handleKeyboard(e) {
369
+ if (!this.lightboxOpen) return;
370
+
371
+ switch (e.key) {
372
+ case 'Escape':
373
+ this.closeLightbox();
374
+ break;
375
+ case 'ArrowLeft':
376
+ this.navigateLightbox(-1);
377
+ break;
378
+ case 'ArrowRight':
379
+ this.navigateLightbox(1);
380
+ break;
381
+ }
330
382
  }
331
- }
332
383
 
333
- // ============================================================================
334
- // UTILITY
335
- // ============================================================================
384
+ // --------------------------------------------------------------------------
385
+ // INITIALIZATION HELPERS
386
+ // --------------------------------------------------------------------------
336
387
 
337
- /**
338
- * Updates the list of gallery images for lightbox navigation
339
- * @private
340
- */
341
- function updateGalleryImages() {
342
- state.galleryImages = [];
343
-
344
- document.querySelectorAll(`${state.config.gallerySelector} ${state.config.itemSelector}`).forEach((item, index) => {
345
- if (item.style.display === 'none') return;
346
-
347
- const img = item.querySelector('img');
348
- const caption = item.querySelector('.gallery-caption');
349
-
350
- if (img) {
351
- state.galleryImages.push({
352
- src: img.dataset.fullSrc || img.src,
353
- alt: img.alt,
354
- caption: caption ? caption.textContent : '',
355
- isNsfw: item.classList.contains('nsfw-content'),
356
- element: item,
357
- originalIndex: index
388
+ /** @private */
389
+ _initFilterButtons() {
390
+ document.querySelectorAll(this.config.filterBtnSelector).forEach(btn => {
391
+ this._events.add(btn, 'click', () => {
392
+ this.filter(btn.dataset.filter || 'all');
358
393
  });
394
+ });
395
+ }
396
+
397
+ /** @private */
398
+ _initGalleryItems() {
399
+ document.querySelectorAll(`${this.config.gallerySelector} ${this.config.itemSelector}`).forEach((item) => {
400
+ const img = item.querySelector('img');
401
+
402
+ if (img && this.config.enableLightbox) {
403
+ img.style.cursor = 'pointer';
404
+ this._events.add(img, 'click', (e) => {
405
+ if (item.classList.contains('nsfw-content') && !item.classList.contains('revealed')) {
406
+ return;
407
+ }
408
+
409
+ e.stopPropagation();
410
+ const visibleIndex = this.galleryImages.findIndex(i => i.element === item);
411
+ if (visibleIndex !== -1) {
412
+ this.openLightbox(visibleIndex);
413
+ }
414
+ });
415
+ }
416
+ });
417
+ }
418
+
419
+ // --------------------------------------------------------------------------
420
+ // UTILITY
421
+ // --------------------------------------------------------------------------
422
+
423
+ /** @private */
424
+ _updateGalleryImages() {
425
+ this.galleryImages = [];
426
+
427
+ document.querySelectorAll(`${this.config.gallerySelector} ${this.config.itemSelector}`).forEach((item, index) => {
428
+ if (item.style.display === 'none') return;
429
+
430
+ const img = item.querySelector('img');
431
+ const caption = item.querySelector('.gallery-caption');
432
+
433
+ if (img) {
434
+ this.galleryImages.push({
435
+ src: img.dataset.fullSrc || img.src,
436
+ alt: img.alt,
437
+ caption: caption ? caption.textContent : '',
438
+ isNsfw: item.classList.contains('nsfw-content'),
439
+ element: item,
440
+ originalIndex: index
441
+ });
442
+ }
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Refresh the gallery image list (call after dynamic content changes)
448
+ */
449
+ refresh() {
450
+ this._updateGalleryImages();
451
+ }
452
+
453
+ /**
454
+ * @returns {Array} Copy of current gallery images
455
+ */
456
+ getImages() {
457
+ return [...this.galleryImages];
458
+ }
459
+
460
+ /**
461
+ * @returns {string} Current filter value
462
+ */
463
+ getCurrentFilter() {
464
+ return this.currentFilter;
465
+ }
466
+
467
+ // --------------------------------------------------------------------------
468
+ // DESTROY
469
+ // --------------------------------------------------------------------------
470
+
471
+ /**
472
+ * Tear down this gallery instance: remove event listeners, lightbox, and state.
473
+ */
474
+ destroy() {
475
+ this._events.removeAll();
476
+ this._timers.clearAll();
477
+
478
+ const lightbox = document.getElementById(this.config.lightboxId);
479
+ if (lightbox) {
480
+ lightbox.remove();
359
481
  }
360
- });
482
+
483
+ this.currentFilter = 'all';
484
+ this.lightboxOpen = false;
485
+ this.currentImageIndex = 0;
486
+ this.galleryImages = [];
487
+ }
361
488
  }
362
489
 
363
490
  // ============================================================================
364
- // PUBLIC API
491
+ // PUBLIC API — backward compatible
365
492
  // ============================================================================
366
493
 
494
+ /** @type {Gallery|null} Default auto-initialized instance */
495
+ let _defaultInstance = null;
496
+
367
497
  /**
368
- * Initializes the gallery system
369
- * @param {string|HTMLElement} selector - Gallery container selector or element
370
- * @param {Object} options - Configuration options
371
- * @returns {Object} Gallery API
498
+ * Initializes a gallery instance
499
+ * @param {string|HTMLElement} [selector='.gallery-container'] - Gallery container selector or element
500
+ * @param {Object} [options={}] - Configuration options
501
+ * @returns {Gallery} Gallery instance with filter, openLightbox, closeLightbox, destroy, etc.
372
502
  */
373
503
  export function initGallery(selector = '.gallery-container', options = {}) {
374
- // Merge config
375
- state.config = { ...DEFAULT_CONFIG, ...options };
376
-
377
- if (typeof selector === 'string') {
378
- state.config.gallerySelector = selector;
379
- }
380
-
381
- // Create lightbox
382
- if (state.config.enableLightbox) {
383
- createLightbox();
384
- }
385
-
386
- // Initialize filter buttons
387
- document.querySelectorAll(state.config.filterBtnSelector).forEach(btn => {
388
- btn.addEventListener('click', () => {
389
- filterGallery(btn.dataset.filter || 'all');
390
- });
391
- });
392
-
393
- // Initialize gallery items
394
- document.querySelectorAll(`${state.config.gallerySelector} ${state.config.itemSelector}`).forEach((item, index) => {
395
- const img = item.querySelector('img');
396
-
397
- if (img && state.config.enableLightbox) {
398
- img.style.cursor = 'pointer';
399
- img.addEventListener('click', (e) => {
400
- // Don't open lightbox for unrevealed NSFW content
401
- if (item.classList.contains('nsfw-content') && !item.classList.contains('revealed')) {
402
- return;
403
- }
404
-
405
- e.stopPropagation();
406
- const visibleIndex = state.galleryImages.findIndex(i => i.element === item);
407
- if (visibleIndex !== -1) {
408
- openLightbox(visibleIndex);
409
- }
410
- });
411
- }
412
- });
413
-
414
- // Initialize NSFW handling
415
- if (state.config.enableNsfw) {
416
- initNsfwContent();
417
- }
418
-
419
- // Initialize keyboard navigation
420
- if (state.config.enableKeyboard) {
421
- document.addEventListener('keydown', handleKeyboard);
504
+ const instance = new Gallery(selector, options);
505
+ // Track as default if this is the first / auto-init call
506
+ if (!_defaultInstance) {
507
+ _defaultInstance = instance;
422
508
  }
423
-
424
- // Build initial image list
425
- updateGalleryImages();
426
-
427
- // Return public API
428
- return {
429
- filter: filterGallery,
430
- openLightbox,
431
- closeLightbox,
432
- revealNsfw: revealNsfwContent,
433
- refresh: updateGalleryImages,
434
- getImages: () => [...state.galleryImages],
435
- getCurrentFilter: () => state.currentFilter
436
- };
509
+ return instance;
437
510
  }
438
511
 
439
512
  /**
440
- * Destroys the gallery instance and removes event listeners
513
+ * Destroys a gallery instance. If no instance is passed, destroys the default instance.
514
+ * @param {Gallery} [instance] - Gallery instance to destroy
441
515
  */
442
- export function destroyGallery() {
443
- const lightbox = document.getElementById(state.config.lightboxId);
444
- if (lightbox) {
445
- lightbox.remove();
516
+ export function destroyGallery(instance) {
517
+ const target = instance || _defaultInstance;
518
+ if (target) {
519
+ target.destroy();
520
+ if (target === _defaultInstance) {
521
+ _defaultInstance = null;
522
+ }
446
523
  }
447
-
448
- document.removeEventListener('keydown', handleKeyboard);
449
-
450
- state = {
451
- currentFilter: 'all',
452
- lightboxOpen: false,
453
- currentImageIndex: 0,
454
- galleryImages: [],
455
- config: { ...DEFAULT_CONFIG }
456
- };
457
524
  }
458
525
 
459
526
  // Auto-initialize if DOM is ready and elements exist
@@ -461,12 +528,12 @@ if (typeof document !== 'undefined') {
461
528
  if (document.readyState === 'loading') {
462
529
  document.addEventListener('DOMContentLoaded', () => {
463
530
  if (document.querySelector('.gallery-container')) {
464
- initGallery();
531
+ _defaultInstance = initGallery();
465
532
  }
466
533
  });
467
534
  } else {
468
535
  if (document.querySelector('.gallery-container')) {
469
- initGallery();
536
+ _defaultInstance = initGallery();
470
537
  }
471
538
  }
472
539
  }
@@ -478,4 +545,3 @@ if (typeof window !== 'undefined') {
478
545
  destroy: destroyGallery
479
546
  };
480
547
  }
481
-