@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.
- package/CHANGELOG.md +53 -0
- package/README.md +207 -42
- package/docs/COMPONENTS_REFERENCE.md +142 -35
- package/docs/governance/VERSION_MANAGEMENT.md +2 -2
- package/docs/governance/VERSION_REFERENCES.md +30 -32
- package/docs/platforms/NPM_PACKAGE.md +8 -7
- package/examples/basic/multi-gallery.html +155 -0
- package/examples/button.html +5 -2
- package/examples/card.html +5 -2
- package/examples/extensions-showcase.html +5 -2
- package/examples/form.html +5 -2
- package/examples/index.html +8 -5
- package/examples/interactive-components.html +223 -0
- package/examples/layout.html +5 -2
- package/examples/nikke-team-builder.html +6 -3
- package/examples/showcase-complete.html +14 -13
- package/examples/showcase.html +6 -3
- package/package.json +6 -5
- package/src/core/corrupted-text.js +25 -5
- package/src/core/event-tracker.js +46 -0
- package/src/core/timer-registry.js +94 -0
- package/src/core/typing-animation.js +36 -17
- package/src/css/components.css +108 -0
- package/src/lib/carousel.js +308 -0
- package/src/lib/celeste-widget.js +178 -47
- package/src/lib/character-corruption.js +33 -8
- package/src/lib/components.js +357 -25
- package/src/lib/corrupted-text.js +21 -5
- package/src/lib/corruption-loading.js +40 -10
- package/src/lib/countdown-widget.js +25 -6
- package/src/lib/gallery.js +420 -354
package/src/lib/gallery.js
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
-
//
|
|
83
|
+
// GALLERY CLASS
|
|
75
84
|
// ============================================================================
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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">×</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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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.
|
|
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
|
-
}
|
|
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
|
-
|
|
272
|
-
// ============================================================================
|
|
313
|
+
this.currentFilter = filter;
|
|
314
|
+
this._updateGalleryImages();
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
//
|
|
335
|
-
//
|
|
384
|
+
// --------------------------------------------------------------------------
|
|
385
|
+
// INITIALIZATION HELPERS
|
|
386
|
+
// --------------------------------------------------------------------------
|
|
336
387
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
369
|
-
* @param {string|HTMLElement} selector - Gallery container selector or element
|
|
370
|
-
* @param {Object} options - Configuration options
|
|
371
|
-
* @returns {
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
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
|
|
444
|
-
if (
|
|
445
|
-
|
|
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
|
-
|