@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.
Files changed (55) hide show
  1. package/README.md +252 -5
  2. package/css/lightbox.scss +272 -0
  3. package/css/style.css +256 -0
  4. package/css/style.css.map +1 -1
  5. package/css/style.scss +1 -0
  6. package/js/bottom-sheet.js +4 -3
  7. package/js/bottom-sheet.ts +5 -3
  8. package/js/calendar.js +9 -5
  9. package/js/calendar.ts +7 -2
  10. package/js/carousel.js +15 -11
  11. package/js/carousel.ts +16 -11
  12. package/js/chart.js +4 -4
  13. package/js/chart.ts +5 -3
  14. package/js/datepicker.js +11 -3
  15. package/js/datepicker.ts +13 -3
  16. package/js/docs-nav.js +1 -0
  17. package/js/editor.js +28 -20
  18. package/js/editor.ts +28 -20
  19. package/js/file-uploader.js +6 -10
  20. package/js/file-uploader.ts +7 -11
  21. package/js/flyout-menu.js +8 -2
  22. package/js/flyout-menu.ts +7 -2
  23. package/js/gallery.js +6 -13
  24. package/js/gallery.ts +8 -16
  25. package/js/group-picker.js +10 -7
  26. package/js/group-picker.ts +11 -7
  27. package/js/lightbox.js +277 -0
  28. package/js/lightbox.ts +331 -0
  29. package/js/modal.js +5 -4
  30. package/js/modal.ts +6 -4
  31. package/js/popover.js +4 -2
  32. package/js/popover.ts +4 -2
  33. package/js/push-menu.js +3 -2
  34. package/js/push-menu.ts +4 -2
  35. package/js/scrollbar.js +31 -23
  36. package/js/scrollbar.ts +36 -26
  37. package/js/select.js +23 -9
  38. package/js/select.ts +29 -11
  39. package/js/stepper.js +5 -1
  40. package/js/stepper.ts +6 -1
  41. package/js/table.js +8 -3
  42. package/js/table.ts +9 -3
  43. package/js/timepicker.js +20 -21
  44. package/js/timepicker.ts +23 -21
  45. package/js/toast.js +3 -7
  46. package/js/toast.ts +4 -8
  47. package/js/tooltip.js +13 -4
  48. package/js/tooltip.ts +16 -4
  49. package/js/tree.js +4 -0
  50. package/js/tree.ts +5 -0
  51. package/js/utils.js +29 -1
  52. package/js/utils.ts +36 -1
  53. package/js/virtual-dropdown.js +4 -8
  54. package/js/virtual-dropdown.ts +5 -9
  55. 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
- toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle));
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
- toggle.removeEventListener('click', (e) => this.handleSubmenu(e, toggle));
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
- toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle));
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
- toggle.removeEventListener('click', (e) => this.handleSubmenu(e, toggle));
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 ?? this.fetchMockImages,
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="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
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">${this.escapeHtml(data.title)}</h3>
156
- <p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
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 ?? this.fetchMockImages,
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="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
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">${this.escapeHtml(data.title)}</h3>
202
- <p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
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
 
@@ -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" placeholder="${this.options.searchPlaceholder}" />
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 text;
295
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
- const regex = new RegExp(`(${escaped})`, 'gi');
297
- return text.replace(regex, '<mark>$1</mark>');
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() {
@@ -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" placeholder="${this.options.searchPlaceholder}" />
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
- if (!query) return text;
391
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
392
- const regex = new RegExp(`(${escaped})`, 'gi');
393
- return text.replace(regex, '<mark>$1</mark>');
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 };