@dodlhuat/basix 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -5
- package/css/lightbox.scss +272 -0
- package/css/style.css +256 -0
- package/css/style.css.map +1 -1
- package/css/style.scss +1 -0
- package/js/bottom-sheet.js +4 -3
- package/js/bottom-sheet.ts +5 -3
- package/js/calendar.js +9 -5
- package/js/calendar.ts +7 -2
- package/js/carousel.js +15 -11
- package/js/carousel.ts +16 -11
- package/js/chart.js +4 -4
- package/js/chart.ts +5 -3
- package/js/datepicker.js +11 -3
- package/js/datepicker.ts +13 -3
- package/js/docs-nav.js +1 -0
- package/js/editor.js +28 -20
- package/js/editor.ts +28 -20
- package/js/file-uploader.js +6 -10
- package/js/file-uploader.ts +7 -11
- package/js/flyout-menu.js +8 -2
- package/js/flyout-menu.ts +7 -2
- package/js/gallery.js +6 -13
- package/js/gallery.ts +8 -16
- package/js/group-picker.js +10 -7
- package/js/group-picker.ts +11 -7
- package/js/lightbox.js +277 -0
- package/js/lightbox.ts +331 -0
- package/js/modal.js +5 -4
- package/js/modal.ts +6 -4
- package/js/popover.js +4 -2
- package/js/popover.ts +4 -2
- package/js/push-menu.js +3 -2
- package/js/push-menu.ts +4 -2
- package/js/scrollbar.js +31 -23
- package/js/scrollbar.ts +36 -26
- package/js/select.js +23 -9
- package/js/select.ts +29 -11
- package/js/stepper.js +5 -1
- package/js/stepper.ts +6 -1
- package/js/table.js +8 -3
- package/js/table.ts +9 -3
- package/js/timepicker.js +20 -21
- package/js/timepicker.ts +23 -21
- package/js/toast.js +3 -7
- package/js/toast.ts +4 -8
- package/js/tooltip.js +13 -4
- package/js/tooltip.ts +16 -4
- package/js/tree.js +4 -0
- package/js/tree.ts +5 -0
- package/js/utils.js +29 -1
- package/js/utils.ts +36 -1
- package/js/virtual-dropdown.js +4 -8
- package/js/virtual-dropdown.ts +5 -9
- package/package.json +1 -3
package/js/flyout-menu.js
CHANGED
|
@@ -3,6 +3,7 @@ class FlyoutMenu {
|
|
|
3
3
|
this.closeBtn = null;
|
|
4
4
|
this.submenuToggles = null;
|
|
5
5
|
this.menuLinks = null;
|
|
6
|
+
this.submenuHandlers = new Map();
|
|
6
7
|
this.options = {
|
|
7
8
|
triggerSelector: '.menu-trigger',
|
|
8
9
|
menuSelector: '#flyoutMenu',
|
|
@@ -119,7 +120,9 @@ class FlyoutMenu {
|
|
|
119
120
|
this.flyoutOverlay?.addEventListener('click', this.close);
|
|
120
121
|
// Submenus
|
|
121
122
|
this.submenuToggles?.forEach(toggle => {
|
|
122
|
-
|
|
123
|
+
const handler = (e) => this.handleSubmenu(e, toggle);
|
|
124
|
+
this.submenuHandlers.set(toggle, handler);
|
|
125
|
+
toggle.addEventListener('click', handler);
|
|
123
126
|
});
|
|
124
127
|
// Close on Link Click
|
|
125
128
|
this.menuLinks?.forEach(link => {
|
|
@@ -183,8 +186,11 @@ class FlyoutMenu {
|
|
|
183
186
|
this.closeBtn?.removeEventListener('click', this.close);
|
|
184
187
|
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
185
188
|
this.submenuToggles?.forEach(toggle => {
|
|
186
|
-
|
|
189
|
+
const handler = this.submenuHandlers.get(toggle);
|
|
190
|
+
if (handler)
|
|
191
|
+
toggle.removeEventListener('click', handler);
|
|
187
192
|
});
|
|
193
|
+
this.submenuHandlers.clear();
|
|
188
194
|
this.menuLinks?.forEach(link => {
|
|
189
195
|
link.removeEventListener('click', this.close);
|
|
190
196
|
});
|
package/js/flyout-menu.ts
CHANGED
|
@@ -20,6 +20,7 @@ class FlyoutMenu {
|
|
|
20
20
|
private closeBtn: HTMLElement | null = null;
|
|
21
21
|
private submenuToggles: NodeListOf<HTMLElement> | null = null;
|
|
22
22
|
private menuLinks: NodeListOf<HTMLAnchorElement> | null = null;
|
|
23
|
+
private submenuHandlers = new Map<HTMLElement, (e: Event) => void>();
|
|
23
24
|
|
|
24
25
|
constructor(options: FlyoutMenuOptions = {}) {
|
|
25
26
|
this.options = {
|
|
@@ -157,7 +158,9 @@ class FlyoutMenu {
|
|
|
157
158
|
|
|
158
159
|
// Submenus
|
|
159
160
|
this.submenuToggles?.forEach(toggle => {
|
|
160
|
-
|
|
161
|
+
const handler = (e: Event) => this.handleSubmenu(e, toggle);
|
|
162
|
+
this.submenuHandlers.set(toggle, handler);
|
|
163
|
+
toggle.addEventListener('click', handler);
|
|
161
164
|
});
|
|
162
165
|
|
|
163
166
|
// Close on Link Click
|
|
@@ -234,8 +237,10 @@ class FlyoutMenu {
|
|
|
234
237
|
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
235
238
|
|
|
236
239
|
this.submenuToggles?.forEach(toggle => {
|
|
237
|
-
|
|
240
|
+
const handler = this.submenuHandlers.get(toggle);
|
|
241
|
+
if (handler) toggle.removeEventListener('click', handler);
|
|
238
242
|
});
|
|
243
|
+
this.submenuHandlers.clear();
|
|
239
244
|
|
|
240
245
|
this.menuLinks?.forEach(link => {
|
|
241
246
|
link.removeEventListener('click', this.close);
|
package/js/gallery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
1
2
|
class MasonryGallery {
|
|
2
|
-
constructor(containerId, options
|
|
3
|
+
constructor(containerId, options) {
|
|
3
4
|
this.columns = [];
|
|
4
5
|
this.isFetching = false;
|
|
5
6
|
this.resizeObserver = null;
|
|
@@ -13,9 +14,6 @@ class MasonryGallery {
|
|
|
13
14
|
this.loadMoreImages();
|
|
14
15
|
}
|
|
15
16
|
};
|
|
16
|
-
this.fetchMockImages = () => {
|
|
17
|
-
throw Error("Method not implemented.");
|
|
18
|
-
};
|
|
19
17
|
const container = document.getElementById(containerId);
|
|
20
18
|
if (!container) {
|
|
21
19
|
throw new Error(`Container with id "${containerId}" not found`);
|
|
@@ -26,7 +24,7 @@ class MasonryGallery {
|
|
|
26
24
|
minColumnWidth: options.minColumnWidth ?? 250,
|
|
27
25
|
scrollThreshold: options.scrollThreshold ?? 100,
|
|
28
26
|
reload: 2,
|
|
29
|
-
fetchFunction: options.fetchFunction
|
|
27
|
+
fetchFunction: options.fetchFunction,
|
|
30
28
|
};
|
|
31
29
|
this.init();
|
|
32
30
|
}
|
|
@@ -150,19 +148,14 @@ class MasonryGallery {
|
|
|
150
148
|
const div = document.createElement("div");
|
|
151
149
|
div.className = "masonry-item";
|
|
152
150
|
div.innerHTML = `
|
|
153
|
-
<img src="${
|
|
151
|
+
<img src="${escapeHtml(data.src)}" alt="${escapeHtml(data.title)}" loading="lazy">
|
|
154
152
|
<div class="masonry-item-info">
|
|
155
|
-
<h3 class="masonry-item-title">${
|
|
156
|
-
<p class="masonry-item-desc">${
|
|
153
|
+
<h3 class="masonry-item-title">${escapeHtml(data.title)}</h3>
|
|
154
|
+
<p class="masonry-item-desc">${escapeHtml(data.desc)}</p>
|
|
157
155
|
</div>
|
|
158
156
|
`;
|
|
159
157
|
return div;
|
|
160
158
|
}
|
|
161
|
-
escapeHtml(text) {
|
|
162
|
-
const div = document.createElement("div");
|
|
163
|
-
div.textContent = text;
|
|
164
|
-
return div.innerHTML;
|
|
165
|
-
}
|
|
166
159
|
addToShortestColumn(element) {
|
|
167
160
|
if (this.columns.length === 0)
|
|
168
161
|
return;
|
package/js/gallery.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
|
|
1
3
|
interface ImageData {
|
|
2
4
|
src: string;
|
|
3
5
|
title: string;
|
|
@@ -5,11 +7,11 @@ interface ImageData {
|
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
interface MasonryGalleryOptions {
|
|
10
|
+
fetchFunction: () => Promise<ImageData[]>;
|
|
8
11
|
minColumnWidth?: number;
|
|
9
12
|
scrollThreshold?: number;
|
|
10
13
|
loaderSelector?: string;
|
|
11
14
|
reload?: number;
|
|
12
|
-
fetchFunction?: () => Promise<ImageData[]>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
class MasonryGallery {
|
|
@@ -22,7 +24,7 @@ class MasonryGallery {
|
|
|
22
24
|
private abortController: AbortController | null = null;
|
|
23
25
|
private reloaded = 0;
|
|
24
26
|
|
|
25
|
-
constructor(containerId: string, options: MasonryGalleryOptions
|
|
27
|
+
constructor(containerId: string, options: MasonryGalleryOptions) {
|
|
26
28
|
const container = document.getElementById(containerId);
|
|
27
29
|
if (!container) {
|
|
28
30
|
throw new Error(`Container with id "${containerId}" not found`);
|
|
@@ -34,7 +36,7 @@ class MasonryGallery {
|
|
|
34
36
|
minColumnWidth: options.minColumnWidth ?? 250,
|
|
35
37
|
scrollThreshold: options.scrollThreshold ?? 100,
|
|
36
38
|
reload: 2,
|
|
37
|
-
fetchFunction: options.fetchFunction
|
|
39
|
+
fetchFunction: options.fetchFunction,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
this.init();
|
|
@@ -159,10 +161,6 @@ class MasonryGallery {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
private fetchMockImages = (): Promise<ImageData[]> => {
|
|
163
|
-
throw Error("Method not implemented.");
|
|
164
|
-
};
|
|
165
|
-
|
|
166
164
|
private renderImages(imageDataList: ImageData[]): void {
|
|
167
165
|
// Sort columns by current height so we start filling from the shortest.
|
|
168
166
|
// Then round-robin across them — this avoids the problem where unloaded
|
|
@@ -196,22 +194,16 @@ class MasonryGallery {
|
|
|
196
194
|
div.className = "masonry-item";
|
|
197
195
|
|
|
198
196
|
div.innerHTML = `
|
|
199
|
-
<img src="${
|
|
197
|
+
<img src="${escapeHtml(data.src)}" alt="${escapeHtml(data.title)}" loading="lazy">
|
|
200
198
|
<div class="masonry-item-info">
|
|
201
|
-
<h3 class="masonry-item-title">${
|
|
202
|
-
<p class="masonry-item-desc">${
|
|
199
|
+
<h3 class="masonry-item-title">${escapeHtml(data.title)}</h3>
|
|
200
|
+
<p class="masonry-item-desc">${escapeHtml(data.desc)}</p>
|
|
203
201
|
</div>
|
|
204
202
|
`;
|
|
205
203
|
|
|
206
204
|
return div;
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
private escapeHtml(text: string): string {
|
|
210
|
-
const div = document.createElement("div");
|
|
211
|
-
div.textContent = text;
|
|
212
|
-
return div.innerHTML;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
207
|
private addToShortestColumn(element: HTMLElement): void {
|
|
216
208
|
if (this.columns.length === 0) return;
|
|
217
209
|
|
package/js/group-picker.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
1
2
|
class GroupPicker {
|
|
2
3
|
constructor(selector, data, options = {}) {
|
|
3
4
|
// State
|
|
@@ -39,9 +40,10 @@ class GroupPicker {
|
|
|
39
40
|
searchWrap.className = 'group-picker__search';
|
|
40
41
|
searchWrap.innerHTML = `
|
|
41
42
|
<span class="icon icon-search group-picker__search-icon" aria-hidden="true"></span>
|
|
42
|
-
<input type="text"
|
|
43
|
+
<input type="text" />
|
|
43
44
|
`;
|
|
44
45
|
this.searchInput = searchWrap.querySelector('input');
|
|
46
|
+
this.searchInput.placeholder = this.options.searchPlaceholder;
|
|
45
47
|
// List
|
|
46
48
|
this.listEl = document.createElement('div');
|
|
47
49
|
this.listEl.className = 'group-picker__list';
|
|
@@ -95,7 +97,7 @@ class GroupPicker {
|
|
|
95
97
|
label.className = 'group-picker__group-label';
|
|
96
98
|
label.innerHTML = query && groupMatches
|
|
97
99
|
? this.highlightText(group.label, query)
|
|
98
|
-
: group.label;
|
|
100
|
+
: escapeHtml(group.label);
|
|
99
101
|
if (hasChildren) {
|
|
100
102
|
// Chevron — Basix font icon
|
|
101
103
|
const chevron = document.createElement('span');
|
|
@@ -134,7 +136,7 @@ class GroupPicker {
|
|
|
134
136
|
const subEl = document.createElement('span');
|
|
135
137
|
subEl.className = 'chip clickable group-picker__subgroup';
|
|
136
138
|
subEl.dataset.subId = sub.id;
|
|
137
|
-
subEl.innerHTML = query ? this.highlightText(sub.label, query) : sub.label;
|
|
139
|
+
subEl.innerHTML = query ? this.highlightText(sub.label, query) : escapeHtml(sub.label);
|
|
138
140
|
const isSubSelected = this.selectedSubs.get(group.id)?.has(sub.id) ?? false;
|
|
139
141
|
if (isSubSelected)
|
|
140
142
|
subEl.classList.add('is-selected');
|
|
@@ -290,11 +292,12 @@ class GroupPicker {
|
|
|
290
292
|
}));
|
|
291
293
|
}
|
|
292
294
|
highlightText(text, query) {
|
|
295
|
+
const safeText = escapeHtml(text);
|
|
293
296
|
if (!query)
|
|
294
|
-
return
|
|
295
|
-
const
|
|
296
|
-
const regex = new RegExp(`(${
|
|
297
|
-
return
|
|
297
|
+
return safeText;
|
|
298
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
299
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
300
|
+
return safeText.replace(regex, '<mark>$1</mark>');
|
|
298
301
|
}
|
|
299
302
|
// Public API
|
|
300
303
|
getSelection() {
|
package/js/group-picker.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
|
|
1
3
|
interface SubgroupData {
|
|
2
4
|
id: string;
|
|
3
5
|
label: string;
|
|
@@ -86,9 +88,10 @@ class GroupPicker {
|
|
|
86
88
|
searchWrap.className = 'group-picker__search';
|
|
87
89
|
searchWrap.innerHTML = `
|
|
88
90
|
<span class="icon icon-search group-picker__search-icon" aria-hidden="true"></span>
|
|
89
|
-
<input type="text"
|
|
91
|
+
<input type="text" />
|
|
90
92
|
`;
|
|
91
93
|
this.searchInput = searchWrap.querySelector('input')!;
|
|
94
|
+
this.searchInput.placeholder = this.options.searchPlaceholder;
|
|
92
95
|
|
|
93
96
|
// List
|
|
94
97
|
this.listEl = document.createElement('div');
|
|
@@ -160,7 +163,7 @@ class GroupPicker {
|
|
|
160
163
|
label.className = 'group-picker__group-label';
|
|
161
164
|
label.innerHTML = query && groupMatches
|
|
162
165
|
? this.highlightText(group.label, query)
|
|
163
|
-
: group.label;
|
|
166
|
+
: escapeHtml(group.label);
|
|
164
167
|
|
|
165
168
|
if (hasChildren) {
|
|
166
169
|
// Chevron — Basix font icon
|
|
@@ -208,7 +211,7 @@ class GroupPicker {
|
|
|
208
211
|
const subEl = document.createElement('span');
|
|
209
212
|
subEl.className = 'chip clickable group-picker__subgroup';
|
|
210
213
|
subEl.dataset.subId = sub.id;
|
|
211
|
-
subEl.innerHTML = query ? this.highlightText(sub.label, query) : sub.label;
|
|
214
|
+
subEl.innerHTML = query ? this.highlightText(sub.label, query) : escapeHtml(sub.label);
|
|
212
215
|
|
|
213
216
|
const isSubSelected = this.selectedSubs.get(group.id)?.has(sub.id) ?? false;
|
|
214
217
|
if (isSubSelected) subEl.classList.add('is-selected');
|
|
@@ -387,10 +390,11 @@ class GroupPicker {
|
|
|
387
390
|
}
|
|
388
391
|
|
|
389
392
|
private highlightText(text: string, query: string): string {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
393
|
+
const safeText = escapeHtml(text);
|
|
394
|
+
if (!query) return safeText;
|
|
395
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
396
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
397
|
+
return safeText.replace(regex, '<mark>$1</mark>');
|
|
394
398
|
}
|
|
395
399
|
|
|
396
400
|
// Public API
|
package/js/lightbox.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
class Lightbox {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.wrapper = null;
|
|
4
|
+
this.imgEl = null;
|
|
5
|
+
this.captionEl = null;
|
|
6
|
+
this.counterEl = null;
|
|
7
|
+
this.isZoomed = false;
|
|
8
|
+
this.abortController = new AbortController();
|
|
9
|
+
if (options.images && options.images.length > 0) {
|
|
10
|
+
this.images = options.images;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
this.images = [{ src: options.src ?? '', alt: options.alt, caption: options.caption }];
|
|
14
|
+
}
|
|
15
|
+
this.currentIndex = options.startIndex ?? 0;
|
|
16
|
+
this.closeable = options.closeable ?? true;
|
|
17
|
+
this.onOpen = options.onOpen;
|
|
18
|
+
this.onClose = options.onClose;
|
|
19
|
+
this.hide = this.hide.bind(this);
|
|
20
|
+
this.handleKeydown = this.handleKeydown.bind(this);
|
|
21
|
+
this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
|
|
22
|
+
}
|
|
23
|
+
show() {
|
|
24
|
+
this.hide();
|
|
25
|
+
const wrapper = document.createElement('div');
|
|
26
|
+
wrapper.className = 'lightbox-wrapper';
|
|
27
|
+
wrapper.setAttribute('role', 'dialog');
|
|
28
|
+
wrapper.setAttribute('aria-modal', 'true');
|
|
29
|
+
wrapper.setAttribute('aria-label', 'Image lightbox');
|
|
30
|
+
wrapper.setAttribute('tabindex', '-1');
|
|
31
|
+
wrapper.innerHTML = this.buildTemplate();
|
|
32
|
+
document.body.append(wrapper);
|
|
33
|
+
this.wrapper = wrapper;
|
|
34
|
+
this.imgEl = wrapper.querySelector('.lightbox-img');
|
|
35
|
+
this.captionEl = wrapper.querySelector('.lightbox-caption');
|
|
36
|
+
this.counterEl = wrapper.querySelector('.lightbox-counter');
|
|
37
|
+
const sig = { signal: this.abortController.signal };
|
|
38
|
+
if (this.closeable) {
|
|
39
|
+
wrapper.querySelector('.lightbox-close')?.addEventListener('click', this.hide, sig);
|
|
40
|
+
wrapper.querySelector('.lightbox-background')?.addEventListener('click', this.handleBackgroundClick, sig);
|
|
41
|
+
}
|
|
42
|
+
document.addEventListener('keydown', this.handleKeydown, sig);
|
|
43
|
+
if (this.images.length > 1) {
|
|
44
|
+
wrapper.querySelector('.lightbox-prev')?.addEventListener('click', () => this.prev(), sig);
|
|
45
|
+
wrapper.querySelector('.lightbox-next')?.addEventListener('click', () => this.next(), sig);
|
|
46
|
+
}
|
|
47
|
+
wrapper.querySelector('.lightbox-img-wrap')?.addEventListener('click', () => this.toggleZoom(), sig);
|
|
48
|
+
this.addTouchSupport();
|
|
49
|
+
document.body.style.overflow = 'hidden';
|
|
50
|
+
this.loadImage(this.currentIndex);
|
|
51
|
+
this.updateNav();
|
|
52
|
+
requestAnimationFrame(() => {
|
|
53
|
+
wrapper.classList.add('is-visible');
|
|
54
|
+
wrapper.focus();
|
|
55
|
+
});
|
|
56
|
+
this.onOpen?.();
|
|
57
|
+
}
|
|
58
|
+
hide() {
|
|
59
|
+
const wrapper = this.wrapper;
|
|
60
|
+
if (!wrapper)
|
|
61
|
+
return;
|
|
62
|
+
this.abortController.abort();
|
|
63
|
+
this.abortController = new AbortController();
|
|
64
|
+
document.body.style.overflow = '';
|
|
65
|
+
wrapper.classList.remove('is-visible');
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
wrapper.remove();
|
|
68
|
+
this.wrapper = null;
|
|
69
|
+
this.imgEl = null;
|
|
70
|
+
this.captionEl = null;
|
|
71
|
+
this.counterEl = null;
|
|
72
|
+
this.isZoomed = false;
|
|
73
|
+
this.onClose?.();
|
|
74
|
+
}, 300);
|
|
75
|
+
}
|
|
76
|
+
next() {
|
|
77
|
+
if (this.images.length <= 1)
|
|
78
|
+
return;
|
|
79
|
+
this.currentIndex = (this.currentIndex + 1) % this.images.length;
|
|
80
|
+
this.loadImage(this.currentIndex);
|
|
81
|
+
this.updateNav();
|
|
82
|
+
}
|
|
83
|
+
prev() {
|
|
84
|
+
if (this.images.length <= 1)
|
|
85
|
+
return;
|
|
86
|
+
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
|
|
87
|
+
this.loadImage(this.currentIndex);
|
|
88
|
+
this.updateNav();
|
|
89
|
+
}
|
|
90
|
+
isVisible() {
|
|
91
|
+
return this.wrapper !== null && document.body.contains(this.wrapper);
|
|
92
|
+
}
|
|
93
|
+
destroy() {
|
|
94
|
+
this.hide();
|
|
95
|
+
}
|
|
96
|
+
loadImage(index) {
|
|
97
|
+
const wrap = this.wrapper?.querySelector('.lightbox-img-wrap');
|
|
98
|
+
if (!wrap || !this.imgEl)
|
|
99
|
+
return;
|
|
100
|
+
this.isZoomed = false;
|
|
101
|
+
this.imgEl.classList.remove('is-zoomed');
|
|
102
|
+
wrap.classList.remove('is-zoomed', 'is-error');
|
|
103
|
+
wrap.classList.add('is-loading');
|
|
104
|
+
this.imgEl.classList.remove('is-loaded');
|
|
105
|
+
const { src, alt, caption } = this.images[index];
|
|
106
|
+
const tempImg = new Image();
|
|
107
|
+
tempImg.onload = () => {
|
|
108
|
+
if (!this.imgEl)
|
|
109
|
+
return;
|
|
110
|
+
this.imgEl.src = src;
|
|
111
|
+
this.imgEl.alt = alt ?? '';
|
|
112
|
+
wrap.classList.remove('is-loading');
|
|
113
|
+
this.imgEl.classList.add('is-loaded');
|
|
114
|
+
};
|
|
115
|
+
tempImg.onerror = () => {
|
|
116
|
+
wrap.classList.remove('is-loading');
|
|
117
|
+
wrap.classList.add('is-error');
|
|
118
|
+
};
|
|
119
|
+
tempImg.src = src;
|
|
120
|
+
if (this.captionEl) {
|
|
121
|
+
if (caption) {
|
|
122
|
+
this.captionEl.textContent = caption;
|
|
123
|
+
this.captionEl.hidden = false;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.captionEl.hidden = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.preloadAdjacent(index);
|
|
130
|
+
}
|
|
131
|
+
preloadAdjacent(index) {
|
|
132
|
+
if (this.images.length <= 1)
|
|
133
|
+
return;
|
|
134
|
+
const prevIdx = (index - 1 + this.images.length) % this.images.length;
|
|
135
|
+
const nextIdx = (index + 1) % this.images.length;
|
|
136
|
+
[prevIdx, nextIdx].forEach(i => {
|
|
137
|
+
const img = new Image();
|
|
138
|
+
img.src = this.images[i].src;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
updateNav() {
|
|
142
|
+
if (!this.wrapper)
|
|
143
|
+
return;
|
|
144
|
+
const isGallery = this.images.length > 1;
|
|
145
|
+
if (this.counterEl) {
|
|
146
|
+
this.counterEl.textContent = isGallery ? `${this.currentIndex + 1} / ${this.images.length}` : '';
|
|
147
|
+
this.counterEl.hidden = !isGallery;
|
|
148
|
+
}
|
|
149
|
+
const prevBtn = this.wrapper.querySelector('.lightbox-prev');
|
|
150
|
+
const nextBtn = this.wrapper.querySelector('.lightbox-next');
|
|
151
|
+
if (prevBtn)
|
|
152
|
+
prevBtn.hidden = !isGallery;
|
|
153
|
+
if (nextBtn)
|
|
154
|
+
nextBtn.hidden = !isGallery;
|
|
155
|
+
}
|
|
156
|
+
toggleZoom() {
|
|
157
|
+
if (!this.imgEl)
|
|
158
|
+
return;
|
|
159
|
+
this.isZoomed = !this.isZoomed;
|
|
160
|
+
this.imgEl.classList.toggle('is-zoomed', this.isZoomed);
|
|
161
|
+
this.wrapper?.querySelector('.lightbox-img-wrap')?.classList.toggle('is-zoomed', this.isZoomed);
|
|
162
|
+
}
|
|
163
|
+
handleKeydown(e) {
|
|
164
|
+
switch (e.key) {
|
|
165
|
+
case 'Escape':
|
|
166
|
+
if (this.closeable)
|
|
167
|
+
this.hide();
|
|
168
|
+
break;
|
|
169
|
+
case 'ArrowRight':
|
|
170
|
+
this.next();
|
|
171
|
+
break;
|
|
172
|
+
case 'ArrowLeft':
|
|
173
|
+
this.prev();
|
|
174
|
+
break;
|
|
175
|
+
case 'Tab':
|
|
176
|
+
this.trapFocus(e);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
trapFocus(e) {
|
|
181
|
+
if (!this.wrapper)
|
|
182
|
+
return;
|
|
183
|
+
const focusable = Array.from(this.wrapper.querySelectorAll('button:not([hidden]), [tabindex]:not([tabindex="-1"]):not([hidden])')).filter(el => el.offsetParent !== null);
|
|
184
|
+
if (focusable.length === 0)
|
|
185
|
+
return;
|
|
186
|
+
const first = focusable[0];
|
|
187
|
+
const last = focusable[focusable.length - 1];
|
|
188
|
+
if (e.shiftKey) {
|
|
189
|
+
if (document.activeElement === first) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
last.focus();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (document.activeElement === last) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
first.focus();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
handleBackgroundClick(e) {
|
|
202
|
+
if (e.target?.classList.contains('lightbox-background')) {
|
|
203
|
+
this.hide();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
addTouchSupport() {
|
|
207
|
+
const wrap = this.wrapper?.querySelector('.lightbox-img-wrap');
|
|
208
|
+
if (!wrap)
|
|
209
|
+
return;
|
|
210
|
+
let startX = 0;
|
|
211
|
+
let isDragging = false;
|
|
212
|
+
wrap.addEventListener('touchstart', (e) => {
|
|
213
|
+
startX = e.touches[0].clientX;
|
|
214
|
+
isDragging = true;
|
|
215
|
+
}, { passive: true, signal: this.abortController.signal });
|
|
216
|
+
wrap.addEventListener('touchend', (e) => {
|
|
217
|
+
if (!isDragging)
|
|
218
|
+
return;
|
|
219
|
+
const deltaX = e.changedTouches[0].clientX - startX;
|
|
220
|
+
if (Math.abs(deltaX) > 50) {
|
|
221
|
+
deltaX < 0 ? this.next() : this.prev();
|
|
222
|
+
}
|
|
223
|
+
isDragging = false;
|
|
224
|
+
}, { signal: this.abortController.signal });
|
|
225
|
+
}
|
|
226
|
+
buildTemplate() {
|
|
227
|
+
return `
|
|
228
|
+
${this.closeable ? '<button class="lightbox-close" aria-label="Close lightbox"><span class="icon icon-close"></span></button>' : ''}
|
|
229
|
+
<div class="lightbox" role="document">
|
|
230
|
+
<div class="lightbox-img-wrap">
|
|
231
|
+
<div class="lightbox-spinner"><div class="spinner"></div></div>
|
|
232
|
+
<img class="lightbox-img" src="" alt="" draggable="false" />
|
|
233
|
+
</div>
|
|
234
|
+
<p class="lightbox-caption" hidden></p>
|
|
235
|
+
<div class="lightbox-counter" hidden></div>
|
|
236
|
+
</div>
|
|
237
|
+
<button class="lightbox-prev" aria-label="Previous image" hidden>
|
|
238
|
+
<span class="icon icon-navigate_before"></span>
|
|
239
|
+
</button>
|
|
240
|
+
<button class="lightbox-next" aria-label="Next image" hidden>
|
|
241
|
+
<span class="icon icon-navigate_next"></span>
|
|
242
|
+
</button>
|
|
243
|
+
<div class="lightbox-background"></div>
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
static bind(selector = '[data-lightbox]') {
|
|
247
|
+
const elements = document.querySelectorAll(selector);
|
|
248
|
+
const groups = new Map();
|
|
249
|
+
elements.forEach(el => {
|
|
250
|
+
const groupKey = el.dataset.lightbox || `__solo__${el.dataset.lightboxId ?? Math.random()}`;
|
|
251
|
+
const src = el instanceof HTMLAnchorElement
|
|
252
|
+
? el.href
|
|
253
|
+
: el instanceof HTMLImageElement
|
|
254
|
+
? el.src
|
|
255
|
+
: (el.dataset.src ?? '');
|
|
256
|
+
const imgChild = el.querySelector('img');
|
|
257
|
+
const alt = el instanceof HTMLImageElement ? el.alt : (imgChild?.alt ?? '');
|
|
258
|
+
const caption = el.dataset.lightboxCaption;
|
|
259
|
+
if (!groups.has(groupKey))
|
|
260
|
+
groups.set(groupKey, []);
|
|
261
|
+
groups.get(groupKey).push({ el, image: { src, alt, caption } });
|
|
262
|
+
});
|
|
263
|
+
groups.forEach(items => {
|
|
264
|
+
items.forEach(({ el }, idx) => {
|
|
265
|
+
el.style.cursor = 'zoom-in';
|
|
266
|
+
el.addEventListener('click', e => {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
new Lightbox({
|
|
269
|
+
images: items.map(i => i.image),
|
|
270
|
+
startIndex: idx,
|
|
271
|
+
}).show();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export { Lightbox };
|