@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/lightbox.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
interface LightboxImage {
|
|
2
|
+
src: string;
|
|
3
|
+
alt?: string;
|
|
4
|
+
caption?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface LightboxOptions {
|
|
8
|
+
src?: string;
|
|
9
|
+
alt?: string;
|
|
10
|
+
caption?: string;
|
|
11
|
+
closeable?: boolean;
|
|
12
|
+
images?: LightboxImage[];
|
|
13
|
+
startIndex?: number;
|
|
14
|
+
onOpen?: () => void;
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Lightbox {
|
|
19
|
+
private images: LightboxImage[];
|
|
20
|
+
private currentIndex: number;
|
|
21
|
+
private readonly closeable: boolean;
|
|
22
|
+
private readonly onOpen?: () => void;
|
|
23
|
+
private readonly onClose?: () => void;
|
|
24
|
+
private wrapper: HTMLElement | null = null;
|
|
25
|
+
private imgEl: HTMLImageElement | null = null;
|
|
26
|
+
private captionEl: HTMLElement | null = null;
|
|
27
|
+
private counterEl: HTMLElement | null = null;
|
|
28
|
+
private isZoomed = false;
|
|
29
|
+
private abortController = new AbortController();
|
|
30
|
+
|
|
31
|
+
constructor(options: LightboxOptions) {
|
|
32
|
+
if (options.images && options.images.length > 0) {
|
|
33
|
+
this.images = options.images;
|
|
34
|
+
} else {
|
|
35
|
+
this.images = [{ src: options.src ?? '', alt: options.alt, caption: options.caption }];
|
|
36
|
+
}
|
|
37
|
+
this.currentIndex = options.startIndex ?? 0;
|
|
38
|
+
this.closeable = options.closeable ?? true;
|
|
39
|
+
this.onOpen = options.onOpen;
|
|
40
|
+
this.onClose = options.onClose;
|
|
41
|
+
|
|
42
|
+
this.hide = this.hide.bind(this);
|
|
43
|
+
this.handleKeydown = this.handleKeydown.bind(this);
|
|
44
|
+
this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public show(): void {
|
|
48
|
+
this.hide();
|
|
49
|
+
|
|
50
|
+
const wrapper = document.createElement('div');
|
|
51
|
+
wrapper.className = 'lightbox-wrapper';
|
|
52
|
+
wrapper.setAttribute('role', 'dialog');
|
|
53
|
+
wrapper.setAttribute('aria-modal', 'true');
|
|
54
|
+
wrapper.setAttribute('aria-label', 'Image lightbox');
|
|
55
|
+
wrapper.setAttribute('tabindex', '-1');
|
|
56
|
+
wrapper.innerHTML = this.buildTemplate();
|
|
57
|
+
document.body.append(wrapper);
|
|
58
|
+
|
|
59
|
+
this.wrapper = wrapper;
|
|
60
|
+
this.imgEl = wrapper.querySelector('.lightbox-img');
|
|
61
|
+
this.captionEl = wrapper.querySelector('.lightbox-caption');
|
|
62
|
+
this.counterEl = wrapper.querySelector('.lightbox-counter');
|
|
63
|
+
|
|
64
|
+
const sig = { signal: this.abortController.signal };
|
|
65
|
+
|
|
66
|
+
if (this.closeable) {
|
|
67
|
+
wrapper.querySelector('.lightbox-close')?.addEventListener('click', this.hide, sig);
|
|
68
|
+
wrapper.querySelector('.lightbox-background')?.addEventListener('click', this.handleBackgroundClick, sig);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
document.addEventListener('keydown', this.handleKeydown, sig);
|
|
72
|
+
|
|
73
|
+
if (this.images.length > 1) {
|
|
74
|
+
wrapper.querySelector('.lightbox-prev')?.addEventListener('click', () => this.prev(), sig);
|
|
75
|
+
wrapper.querySelector('.lightbox-next')?.addEventListener('click', () => this.next(), sig);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
wrapper.querySelector('.lightbox-img-wrap')?.addEventListener('click', () => this.toggleZoom(), sig);
|
|
79
|
+
|
|
80
|
+
this.addTouchSupport();
|
|
81
|
+
|
|
82
|
+
document.body.style.overflow = 'hidden';
|
|
83
|
+
this.loadImage(this.currentIndex);
|
|
84
|
+
this.updateNav();
|
|
85
|
+
|
|
86
|
+
requestAnimationFrame(() => {
|
|
87
|
+
wrapper.classList.add('is-visible');
|
|
88
|
+
wrapper.focus();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.onOpen?.();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public hide(): void {
|
|
95
|
+
const wrapper = this.wrapper;
|
|
96
|
+
if (!wrapper) return;
|
|
97
|
+
|
|
98
|
+
this.abortController.abort();
|
|
99
|
+
this.abortController = new AbortController();
|
|
100
|
+
|
|
101
|
+
document.body.style.overflow = '';
|
|
102
|
+
wrapper.classList.remove('is-visible');
|
|
103
|
+
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
wrapper.remove();
|
|
106
|
+
this.wrapper = null;
|
|
107
|
+
this.imgEl = null;
|
|
108
|
+
this.captionEl = null;
|
|
109
|
+
this.counterEl = null;
|
|
110
|
+
this.isZoomed = false;
|
|
111
|
+
this.onClose?.();
|
|
112
|
+
}, 300);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public next(): void {
|
|
116
|
+
if (this.images.length <= 1) return;
|
|
117
|
+
this.currentIndex = (this.currentIndex + 1) % this.images.length;
|
|
118
|
+
this.loadImage(this.currentIndex);
|
|
119
|
+
this.updateNav();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public prev(): void {
|
|
123
|
+
if (this.images.length <= 1) return;
|
|
124
|
+
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
|
|
125
|
+
this.loadImage(this.currentIndex);
|
|
126
|
+
this.updateNav();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public isVisible(): boolean {
|
|
130
|
+
return this.wrapper !== null && document.body.contains(this.wrapper);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public destroy(): void {
|
|
134
|
+
this.hide();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private loadImage(index: number): void {
|
|
138
|
+
const wrap = this.wrapper?.querySelector('.lightbox-img-wrap');
|
|
139
|
+
if (!wrap || !this.imgEl) return;
|
|
140
|
+
|
|
141
|
+
this.isZoomed = false;
|
|
142
|
+
this.imgEl.classList.remove('is-zoomed');
|
|
143
|
+
wrap.classList.remove('is-zoomed', 'is-error');
|
|
144
|
+
wrap.classList.add('is-loading');
|
|
145
|
+
this.imgEl.classList.remove('is-loaded');
|
|
146
|
+
|
|
147
|
+
const { src, alt, caption } = this.images[index];
|
|
148
|
+
|
|
149
|
+
const tempImg = new Image();
|
|
150
|
+
tempImg.onload = () => {
|
|
151
|
+
if (!this.imgEl) return;
|
|
152
|
+
this.imgEl.src = src;
|
|
153
|
+
this.imgEl.alt = alt ?? '';
|
|
154
|
+
wrap.classList.remove('is-loading');
|
|
155
|
+
this.imgEl.classList.add('is-loaded');
|
|
156
|
+
};
|
|
157
|
+
tempImg.onerror = () => {
|
|
158
|
+
wrap.classList.remove('is-loading');
|
|
159
|
+
wrap.classList.add('is-error');
|
|
160
|
+
};
|
|
161
|
+
tempImg.src = src;
|
|
162
|
+
|
|
163
|
+
if (this.captionEl) {
|
|
164
|
+
if (caption) {
|
|
165
|
+
this.captionEl.textContent = caption;
|
|
166
|
+
this.captionEl.hidden = false;
|
|
167
|
+
} else {
|
|
168
|
+
this.captionEl.hidden = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.preloadAdjacent(index);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private preloadAdjacent(index: number): void {
|
|
176
|
+
if (this.images.length <= 1) return;
|
|
177
|
+
const prevIdx = (index - 1 + this.images.length) % this.images.length;
|
|
178
|
+
const nextIdx = (index + 1) % this.images.length;
|
|
179
|
+
[prevIdx, nextIdx].forEach(i => {
|
|
180
|
+
const img = new Image();
|
|
181
|
+
img.src = this.images[i].src;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private updateNav(): void {
|
|
186
|
+
if (!this.wrapper) return;
|
|
187
|
+
const isGallery = this.images.length > 1;
|
|
188
|
+
|
|
189
|
+
if (this.counterEl) {
|
|
190
|
+
this.counterEl.textContent = isGallery ? `${this.currentIndex + 1} / ${this.images.length}` : '';
|
|
191
|
+
this.counterEl.hidden = !isGallery;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const prevBtn = this.wrapper.querySelector<HTMLButtonElement>('.lightbox-prev');
|
|
195
|
+
const nextBtn = this.wrapper.querySelector<HTMLButtonElement>('.lightbox-next');
|
|
196
|
+
if (prevBtn) prevBtn.hidden = !isGallery;
|
|
197
|
+
if (nextBtn) nextBtn.hidden = !isGallery;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private toggleZoom(): void {
|
|
201
|
+
if (!this.imgEl) return;
|
|
202
|
+
this.isZoomed = !this.isZoomed;
|
|
203
|
+
this.imgEl.classList.toggle('is-zoomed', this.isZoomed);
|
|
204
|
+
this.wrapper?.querySelector('.lightbox-img-wrap')?.classList.toggle('is-zoomed', this.isZoomed);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleKeydown(e: KeyboardEvent): void {
|
|
208
|
+
switch (e.key) {
|
|
209
|
+
case 'Escape':
|
|
210
|
+
if (this.closeable) this.hide();
|
|
211
|
+
break;
|
|
212
|
+
case 'ArrowRight':
|
|
213
|
+
this.next();
|
|
214
|
+
break;
|
|
215
|
+
case 'ArrowLeft':
|
|
216
|
+
this.prev();
|
|
217
|
+
break;
|
|
218
|
+
case 'Tab':
|
|
219
|
+
this.trapFocus(e);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private trapFocus(e: KeyboardEvent): void {
|
|
225
|
+
if (!this.wrapper) return;
|
|
226
|
+
const focusable = Array.from(
|
|
227
|
+
this.wrapper.querySelectorAll<HTMLElement>(
|
|
228
|
+
'button:not([hidden]), [tabindex]:not([tabindex="-1"]):not([hidden])'
|
|
229
|
+
)
|
|
230
|
+
).filter(el => el.offsetParent !== null);
|
|
231
|
+
|
|
232
|
+
if (focusable.length === 0) return;
|
|
233
|
+
const first = focusable[0];
|
|
234
|
+
const last = focusable[focusable.length - 1];
|
|
235
|
+
|
|
236
|
+
if (e.shiftKey) {
|
|
237
|
+
if (document.activeElement === first) {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
last.focus();
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
if (document.activeElement === last) {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
first.focus();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private handleBackgroundClick(e: Event): void {
|
|
250
|
+
if ((e.target as HTMLElement)?.classList.contains('lightbox-background')) {
|
|
251
|
+
this.hide();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private addTouchSupport(): void {
|
|
256
|
+
const wrap = this.wrapper?.querySelector('.lightbox-img-wrap');
|
|
257
|
+
if (!wrap) return;
|
|
258
|
+
let startX = 0;
|
|
259
|
+
let isDragging = false;
|
|
260
|
+
|
|
261
|
+
wrap.addEventListener('touchstart', (e: Event) => {
|
|
262
|
+
startX = (e as TouchEvent).touches[0].clientX;
|
|
263
|
+
isDragging = true;
|
|
264
|
+
}, { passive: true, signal: this.abortController.signal } as AddEventListenerOptions);
|
|
265
|
+
|
|
266
|
+
wrap.addEventListener('touchend', (e: Event) => {
|
|
267
|
+
if (!isDragging) return;
|
|
268
|
+
const deltaX = (e as TouchEvent).changedTouches[0].clientX - startX;
|
|
269
|
+
if (Math.abs(deltaX) > 50) {
|
|
270
|
+
deltaX < 0 ? this.next() : this.prev();
|
|
271
|
+
}
|
|
272
|
+
isDragging = false;
|
|
273
|
+
}, { signal: this.abortController.signal } as AddEventListenerOptions);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private buildTemplate(): string {
|
|
277
|
+
return `
|
|
278
|
+
${this.closeable ? '<button class="lightbox-close" aria-label="Close lightbox"><span class="icon icon-close"></span></button>' : ''}
|
|
279
|
+
<div class="lightbox" role="document">
|
|
280
|
+
<div class="lightbox-img-wrap">
|
|
281
|
+
<div class="lightbox-spinner"><div class="spinner"></div></div>
|
|
282
|
+
<img class="lightbox-img" src="" alt="" draggable="false" />
|
|
283
|
+
</div>
|
|
284
|
+
<p class="lightbox-caption" hidden></p>
|
|
285
|
+
<div class="lightbox-counter" hidden></div>
|
|
286
|
+
</div>
|
|
287
|
+
<button class="lightbox-prev" aria-label="Previous image" hidden>
|
|
288
|
+
<span class="icon icon-navigate_before"></span>
|
|
289
|
+
</button>
|
|
290
|
+
<button class="lightbox-next" aria-label="Next image" hidden>
|
|
291
|
+
<span class="icon icon-navigate_next"></span>
|
|
292
|
+
</button>
|
|
293
|
+
<div class="lightbox-background"></div>
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static bind(selector: string = '[data-lightbox]'): void {
|
|
298
|
+
const elements = document.querySelectorAll<HTMLElement>(selector);
|
|
299
|
+
const groups = new Map<string, { el: HTMLElement; image: LightboxImage }[]>();
|
|
300
|
+
|
|
301
|
+
elements.forEach(el => {
|
|
302
|
+
const groupKey = el.dataset.lightbox || `__solo__${el.dataset.lightboxId ?? Math.random()}`;
|
|
303
|
+
const src = el instanceof HTMLAnchorElement
|
|
304
|
+
? el.href
|
|
305
|
+
: el instanceof HTMLImageElement
|
|
306
|
+
? el.src
|
|
307
|
+
: (el.dataset.src ?? '');
|
|
308
|
+
const imgChild = el.querySelector<HTMLImageElement>('img');
|
|
309
|
+
const alt = el instanceof HTMLImageElement ? el.alt : (imgChild?.alt ?? '');
|
|
310
|
+
const caption = el.dataset.lightboxCaption;
|
|
311
|
+
|
|
312
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
313
|
+
groups.get(groupKey)!.push({ el, image: { src, alt, caption } });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
groups.forEach(items => {
|
|
317
|
+
items.forEach(({ el }, idx) => {
|
|
318
|
+
(el as HTMLElement).style.cursor = 'zoom-in';
|
|
319
|
+
el.addEventListener('click', e => {
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
new Lightbox({
|
|
322
|
+
images: items.map(i => i.image),
|
|
323
|
+
startIndex: idx,
|
|
324
|
+
}).show();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export { Lightbox, type LightboxOptions, type LightboxImage };
|
package/js/modal.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeHtml } from './utils.js';
|
|
1
2
|
const CLOSE_ICON = '<div class="icon icon-close close"></div>';
|
|
2
3
|
class Modal {
|
|
3
4
|
constructor(contentOrOptions, header, footer, closeable = true, type = 'default') {
|
|
@@ -45,7 +46,7 @@ class Modal {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
hide() {
|
|
48
|
-
const wrapper =
|
|
49
|
+
const wrapper = this.modalWrapper;
|
|
49
50
|
if (!wrapper)
|
|
50
51
|
return;
|
|
51
52
|
// Remove event listeners
|
|
@@ -78,11 +79,11 @@ class Modal {
|
|
|
78
79
|
}
|
|
79
80
|
if (this.header !== undefined) {
|
|
80
81
|
const headerClass = `header ${this.type}-bg`;
|
|
81
|
-
parts.push(`<div class="${headerClass}">${this.header}</div>`);
|
|
82
|
+
parts.push(`<div class="${headerClass}">${sanitizeHtml(this.header)}</div>`);
|
|
82
83
|
}
|
|
83
|
-
parts.push(this.content);
|
|
84
|
+
parts.push(sanitizeHtml(this.content));
|
|
84
85
|
if (this.footer !== undefined) {
|
|
85
|
-
parts.push(`<div class="footer">${this.footer}</div>`);
|
|
86
|
+
parts.push(`<div class="footer">${sanitizeHtml(this.footer)}</div>`);
|
|
86
87
|
}
|
|
87
88
|
parts.push('</div>');
|
|
88
89
|
parts.push('<div class="modal-background"></div>');
|
package/js/modal.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sanitizeHtml } from './utils.js';
|
|
2
|
+
|
|
1
3
|
const CLOSE_ICON = '<div class="icon icon-close close"></div>';
|
|
2
4
|
|
|
3
5
|
type ModalType = 'default' | 'success' | 'error' | 'warning' | 'info';
|
|
@@ -81,7 +83,7 @@ class Modal {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
public hide(): void {
|
|
84
|
-
const wrapper =
|
|
86
|
+
const wrapper = this.modalWrapper;
|
|
85
87
|
if (!wrapper) return;
|
|
86
88
|
|
|
87
89
|
// Remove event listeners
|
|
@@ -123,13 +125,13 @@ class Modal {
|
|
|
123
125
|
|
|
124
126
|
if (this.header !== undefined) {
|
|
125
127
|
const headerClass = `header ${this.type}-bg`;
|
|
126
|
-
parts.push(`<div class="${headerClass}">${this.header}</div>`);
|
|
128
|
+
parts.push(`<div class="${headerClass}">${sanitizeHtml(this.header)}</div>`);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
parts.push(this.content);
|
|
131
|
+
parts.push(sanitizeHtml(this.content));
|
|
130
132
|
|
|
131
133
|
if (this.footer !== undefined) {
|
|
132
|
-
parts.push(`<div class="footer">${this.footer}</div>`);
|
|
134
|
+
parts.push(`<div class="footer">${sanitizeHtml(this.footer)}</div>`);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
parts.push('</div>');
|
package/js/popover.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computePosition } from './position.js';
|
|
2
|
+
import { sanitizeHtml } from './utils.js';
|
|
2
3
|
// Must match $arrow in popover.scss
|
|
3
4
|
const ARROW_SIZE = 6;
|
|
4
5
|
class Popover {
|
|
@@ -119,9 +120,10 @@ class Popover {
|
|
|
119
120
|
// Wrap plain content in .popover-body so it gets proper padding.
|
|
120
121
|
// Skip wrapping when content already uses structured popover elements.
|
|
121
122
|
const hasStructure = /class="popover-(header|body|footer|menu)/.test(this.opts.content);
|
|
123
|
+
const safeContent = sanitizeHtml(this.opts.content);
|
|
122
124
|
el.innerHTML = hasStructure
|
|
123
|
-
?
|
|
124
|
-
: `<div class="popover-body">${
|
|
125
|
+
? safeContent
|
|
126
|
+
: `<div class="popover-body">${safeContent}</div>`;
|
|
125
127
|
this.trigger.setAttribute('aria-expanded', 'true');
|
|
126
128
|
this.trigger.setAttribute('aria-controls', id);
|
|
127
129
|
return el;
|
package/js/popover.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computePosition } from './position.js';
|
|
2
2
|
import type { Placement } from './position.js';
|
|
3
|
+
import { sanitizeHtml } from './utils.js';
|
|
3
4
|
|
|
4
5
|
type PopoverPlacement = Placement | 'auto';
|
|
5
6
|
type PopoverAlign = 'start' | 'center' | 'end';
|
|
@@ -142,9 +143,10 @@ class Popover {
|
|
|
142
143
|
// Wrap plain content in .popover-body so it gets proper padding.
|
|
143
144
|
// Skip wrapping when content already uses structured popover elements.
|
|
144
145
|
const hasStructure = /class="popover-(header|body|footer|menu)/.test(this.opts.content);
|
|
146
|
+
const safeContent = sanitizeHtml(this.opts.content);
|
|
145
147
|
el.innerHTML = hasStructure
|
|
146
|
-
?
|
|
147
|
-
: `<div class="popover-body">${
|
|
148
|
+
? safeContent
|
|
149
|
+
: `<div class="popover-body">${safeContent}</div>`;
|
|
148
150
|
|
|
149
151
|
this.trigger.setAttribute('aria-expanded', 'true');
|
|
150
152
|
this.trigger.setAttribute('aria-controls', id);
|
package/js/push-menu.js
CHANGED
|
@@ -9,7 +9,8 @@ class PushMenu {
|
|
|
9
9
|
throw new Error('PushMenu: Required elements not found (.navigation, .push-content)');
|
|
10
10
|
}
|
|
11
11
|
this.buildPanels();
|
|
12
|
-
this.
|
|
12
|
+
this.boundHandleNavigationChange = this.handleNavigationChange.bind(this);
|
|
13
|
+
this.elements.navigation.addEventListener('change', this.boundHandleNavigationChange);
|
|
13
14
|
this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
|
|
14
15
|
this.initialized = true;
|
|
15
16
|
}
|
|
@@ -179,7 +180,7 @@ class PushMenu {
|
|
|
179
180
|
static destroy() {
|
|
180
181
|
if (!this.initialized)
|
|
181
182
|
return;
|
|
182
|
-
this.elements.navigation?.removeEventListener('change', this.
|
|
183
|
+
this.elements.navigation?.removeEventListener('change', this.boundHandleNavigationChange);
|
|
183
184
|
this.elements.content?.removeEventListener('click', this.clickNav);
|
|
184
185
|
this.elements.backdrop?.removeEventListener('click', this.handleBackdropClick);
|
|
185
186
|
this.close();
|
package/js/push-menu.ts
CHANGED
|
@@ -19,6 +19,7 @@ class PushMenu {
|
|
|
19
19
|
|
|
20
20
|
private static initialized = false;
|
|
21
21
|
private static panelStack: HTMLElement[] = [];
|
|
22
|
+
private static boundHandleNavigationChange: () => void;
|
|
22
23
|
|
|
23
24
|
public static init(): void {
|
|
24
25
|
if (this.initialized) {
|
|
@@ -34,7 +35,8 @@ class PushMenu {
|
|
|
34
35
|
|
|
35
36
|
this.buildPanels();
|
|
36
37
|
|
|
37
|
-
this.
|
|
38
|
+
this.boundHandleNavigationChange = this.handleNavigationChange.bind(this);
|
|
39
|
+
this.elements.navigation.addEventListener('change', this.boundHandleNavigationChange);
|
|
38
40
|
this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
|
|
39
41
|
|
|
40
42
|
this.initialized = true;
|
|
@@ -252,7 +254,7 @@ class PushMenu {
|
|
|
252
254
|
public static destroy(): void {
|
|
253
255
|
if (!this.initialized) return;
|
|
254
256
|
|
|
255
|
-
this.elements.navigation?.removeEventListener('change', this.
|
|
257
|
+
this.elements.navigation?.removeEventListener('change', this.boundHandleNavigationChange);
|
|
256
258
|
this.elements.content?.removeEventListener('click', this.clickNav);
|
|
257
259
|
this.elements.backdrop?.removeEventListener('click', this.handleBackdropClick);
|
|
258
260
|
|
package/js/scrollbar.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
class Scrollbar {
|
|
2
|
-
constructor(
|
|
2
|
+
constructor(container) {
|
|
3
3
|
this.dragging = false;
|
|
4
4
|
this.activePointerId = null;
|
|
5
5
|
this.startPointerY = 0;
|
|
6
6
|
this.startThumbTop = 0;
|
|
7
|
-
const container = typeof elementOrSelector === 'string'
|
|
8
|
-
? document.querySelector(elementOrSelector)
|
|
9
|
-
: elementOrSelector;
|
|
10
|
-
if (!container) {
|
|
11
|
-
throw new Error(`Scrollbar: Element not found for selector "${elementOrSelector}"`);
|
|
12
|
-
}
|
|
13
|
-
// Return existing instance if already initialized
|
|
14
|
-
const existingInstance = Scrollbar.instances.get(container);
|
|
15
|
-
if (existingInstance) {
|
|
16
|
-
return existingInstance;
|
|
17
|
-
}
|
|
18
7
|
this.container = container;
|
|
19
8
|
// Query and validate required elements
|
|
20
9
|
const elements = this.getRequiredElements(container);
|
|
@@ -37,7 +26,8 @@ class Scrollbar {
|
|
|
37
26
|
// Initialize
|
|
38
27
|
this.attachEventListeners();
|
|
39
28
|
Scrollbar.instances.set(container, this);
|
|
40
|
-
//
|
|
29
|
+
// Track instances and install global listeners once for all
|
|
30
|
+
Scrollbar.instanceCount++;
|
|
41
31
|
if (!Scrollbar.globalListenersInstalled) {
|
|
42
32
|
Scrollbar.installGlobalListeners();
|
|
43
33
|
}
|
|
@@ -64,16 +54,17 @@ class Scrollbar {
|
|
|
64
54
|
return Math.max(absoluteMin, parsed || defaultMin);
|
|
65
55
|
}
|
|
66
56
|
static installGlobalListeners() {
|
|
67
|
-
|
|
57
|
+
const ac = new AbortController();
|
|
58
|
+
Scrollbar.globalListenerAbortController = ac;
|
|
68
59
|
document.addEventListener('pointermove', (e) => {
|
|
69
60
|
Scrollbar.activeInstance?.boundPointerMove(e);
|
|
70
|
-
}, { passive: false });
|
|
61
|
+
}, { passive: false, signal: ac.signal });
|
|
71
62
|
document.addEventListener('pointerup', (e) => {
|
|
72
63
|
Scrollbar.activeInstance?.boundPointerUp(e);
|
|
73
|
-
});
|
|
64
|
+
}, { signal: ac.signal });
|
|
74
65
|
document.addEventListener('pointercancel', (e) => {
|
|
75
66
|
Scrollbar.activeInstance?.boundPointerUp(e);
|
|
76
|
-
});
|
|
67
|
+
}, { signal: ac.signal });
|
|
77
68
|
Scrollbar.globalListenersInstalled = true;
|
|
78
69
|
}
|
|
79
70
|
attachEventListeners() {
|
|
@@ -209,24 +200,41 @@ class Scrollbar {
|
|
|
209
200
|
if (Scrollbar.activeInstance === this) {
|
|
210
201
|
Scrollbar.activeInstance = null;
|
|
211
202
|
}
|
|
203
|
+
// Uninstall global listeners when last instance is destroyed
|
|
204
|
+
Scrollbar.instanceCount--;
|
|
205
|
+
if (Scrollbar.instanceCount === 0) {
|
|
206
|
+
Scrollbar.globalListenerAbortController?.abort();
|
|
207
|
+
Scrollbar.globalListenerAbortController = null;
|
|
208
|
+
Scrollbar.globalListenersInstalled = false;
|
|
209
|
+
}
|
|
212
210
|
}
|
|
213
211
|
// Static factory methods
|
|
212
|
+
static create(elementOrSelector) {
|
|
213
|
+
const container = typeof elementOrSelector === 'string'
|
|
214
|
+
? document.querySelector(elementOrSelector)
|
|
215
|
+
: elementOrSelector;
|
|
216
|
+
if (!container) {
|
|
217
|
+
throw new Error(`Scrollbar: Element not found for selector "${elementOrSelector}"`);
|
|
218
|
+
}
|
|
219
|
+
const existing = Scrollbar.instances.get(container);
|
|
220
|
+
if (existing)
|
|
221
|
+
return existing;
|
|
222
|
+
return new Scrollbar(container);
|
|
223
|
+
}
|
|
214
224
|
static initAll(selector) {
|
|
215
225
|
const containers = document.querySelectorAll(selector);
|
|
216
|
-
return Array.from(containers).map(container =>
|
|
226
|
+
return Array.from(containers).map(container => Scrollbar.create(container));
|
|
217
227
|
}
|
|
218
228
|
static initOne(elementOrSelector) {
|
|
219
|
-
return
|
|
229
|
+
return Scrollbar.create(elementOrSelector);
|
|
220
230
|
}
|
|
221
231
|
static getInstance(container) {
|
|
222
232
|
return Scrollbar.instances.get(container);
|
|
223
233
|
}
|
|
224
|
-
static destroyAll() {
|
|
225
|
-
// Note: WeakMap doesn't support iteration, so this is a no-op
|
|
226
|
-
// Individual instances should be destroyed by calling destroy()
|
|
227
|
-
}
|
|
228
234
|
}
|
|
229
235
|
Scrollbar.instances = new WeakMap();
|
|
230
236
|
Scrollbar.activeInstance = null;
|
|
231
237
|
Scrollbar.globalListenersInstalled = false;
|
|
238
|
+
Scrollbar.instanceCount = 0;
|
|
239
|
+
Scrollbar.globalListenerAbortController = null;
|
|
232
240
|
export { Scrollbar };
|