@dodlhuat/basix 1.2.2 → 1.2.4
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 +45 -1
- package/css/lightbox.scss +272 -0
- package/css/style.css +263 -3
- 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 +32 -21
- package/js/timepicker.ts +29 -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 -1
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 };
|
package/js/scrollbar.ts
CHANGED
|
@@ -9,6 +9,8 @@ class Scrollbar {
|
|
|
9
9
|
private static readonly instances = new WeakMap<HTMLElement, Scrollbar>();
|
|
10
10
|
private static activeInstance: Scrollbar | null = null;
|
|
11
11
|
private static globalListenersInstalled = false;
|
|
12
|
+
private static instanceCount = 0;
|
|
13
|
+
private static globalListenerAbortController: AbortController | null = null;
|
|
12
14
|
|
|
13
15
|
private readonly container!: HTMLElement;
|
|
14
16
|
private readonly viewport!: HTMLElement;
|
|
@@ -32,21 +34,7 @@ class Scrollbar {
|
|
|
32
34
|
private readonly boundUpdateThumb!: () => void;
|
|
33
35
|
private readonly boundContainerWheel!: (e: WheelEvent) => void;
|
|
34
36
|
|
|
35
|
-
constructor(
|
|
36
|
-
const container = typeof elementOrSelector === 'string'
|
|
37
|
-
? document.querySelector<HTMLElement>(elementOrSelector)
|
|
38
|
-
: elementOrSelector;
|
|
39
|
-
|
|
40
|
-
if (!container) {
|
|
41
|
-
throw new Error(`Scrollbar: Element not found for selector "${elementOrSelector}"`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Return existing instance if already initialized
|
|
45
|
-
const existingInstance = Scrollbar.instances.get(container);
|
|
46
|
-
if (existingInstance) {
|
|
47
|
-
return existingInstance as any;
|
|
48
|
-
}
|
|
49
|
-
|
|
37
|
+
private constructor(container: HTMLElement) {
|
|
50
38
|
this.container = container;
|
|
51
39
|
|
|
52
40
|
// Query and validate required elements
|
|
@@ -75,7 +63,8 @@ class Scrollbar {
|
|
|
75
63
|
this.attachEventListeners();
|
|
76
64
|
Scrollbar.instances.set(container, this);
|
|
77
65
|
|
|
78
|
-
//
|
|
66
|
+
// Track instances and install global listeners once for all
|
|
67
|
+
Scrollbar.instanceCount++;
|
|
79
68
|
if (!Scrollbar.globalListenersInstalled) {
|
|
80
69
|
Scrollbar.installGlobalListeners();
|
|
81
70
|
}
|
|
@@ -112,18 +101,20 @@ class Scrollbar {
|
|
|
112
101
|
}
|
|
113
102
|
|
|
114
103
|
private static installGlobalListeners(): void {
|
|
115
|
-
|
|
104
|
+
const ac = new AbortController();
|
|
105
|
+
Scrollbar.globalListenerAbortController = ac;
|
|
106
|
+
|
|
116
107
|
document.addEventListener('pointermove', (e: PointerEvent) => {
|
|
117
108
|
Scrollbar.activeInstance?.boundPointerMove(e);
|
|
118
|
-
}, { passive: false });
|
|
109
|
+
}, { passive: false, signal: ac.signal });
|
|
119
110
|
|
|
120
111
|
document.addEventListener('pointerup', (e: PointerEvent) => {
|
|
121
112
|
Scrollbar.activeInstance?.boundPointerUp(e);
|
|
122
|
-
});
|
|
113
|
+
}, { signal: ac.signal });
|
|
123
114
|
|
|
124
115
|
document.addEventListener('pointercancel', (e: PointerEvent) => {
|
|
125
116
|
Scrollbar.activeInstance?.boundPointerUp(e);
|
|
126
|
-
});
|
|
117
|
+
}, { signal: ac.signal });
|
|
127
118
|
|
|
128
119
|
Scrollbar.globalListenersInstalled = true;
|
|
129
120
|
}
|
|
@@ -300,26 +291,45 @@ class Scrollbar {
|
|
|
300
291
|
if (Scrollbar.activeInstance === this) {
|
|
301
292
|
Scrollbar.activeInstance = null;
|
|
302
293
|
}
|
|
294
|
+
|
|
295
|
+
// Uninstall global listeners when last instance is destroyed
|
|
296
|
+
Scrollbar.instanceCount--;
|
|
297
|
+
if (Scrollbar.instanceCount === 0) {
|
|
298
|
+
Scrollbar.globalListenerAbortController?.abort();
|
|
299
|
+
Scrollbar.globalListenerAbortController = null;
|
|
300
|
+
Scrollbar.globalListenersInstalled = false;
|
|
301
|
+
}
|
|
303
302
|
}
|
|
304
303
|
|
|
305
304
|
// Static factory methods
|
|
305
|
+
public static create(elementOrSelector: string | HTMLElement): Scrollbar {
|
|
306
|
+
const container = typeof elementOrSelector === 'string'
|
|
307
|
+
? document.querySelector<HTMLElement>(elementOrSelector)
|
|
308
|
+
: elementOrSelector;
|
|
309
|
+
|
|
310
|
+
if (!container) {
|
|
311
|
+
throw new Error(`Scrollbar: Element not found for selector "${elementOrSelector}"`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const existing = Scrollbar.instances.get(container);
|
|
315
|
+
if (existing) return existing;
|
|
316
|
+
|
|
317
|
+
return new Scrollbar(container);
|
|
318
|
+
}
|
|
319
|
+
|
|
306
320
|
public static initAll(selector: string): Scrollbar[] {
|
|
307
321
|
const containers = document.querySelectorAll<HTMLElement>(selector);
|
|
308
|
-
return Array.from(containers).map(container =>
|
|
322
|
+
return Array.from(containers).map(container => Scrollbar.create(container));
|
|
309
323
|
}
|
|
310
324
|
|
|
311
325
|
public static initOne(elementOrSelector: string | HTMLElement): Scrollbar {
|
|
312
|
-
return
|
|
326
|
+
return Scrollbar.create(elementOrSelector);
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
public static getInstance(container: HTMLElement): Scrollbar | undefined {
|
|
316
330
|
return Scrollbar.instances.get(container);
|
|
317
331
|
}
|
|
318
332
|
|
|
319
|
-
public static destroyAll(): void {
|
|
320
|
-
// Note: WeakMap doesn't support iteration, so this is a no-op
|
|
321
|
-
// Individual instances should be destroyed by calling destroy()
|
|
322
|
-
}
|
|
323
333
|
}
|
|
324
334
|
|
|
325
335
|
export { Scrollbar, ScrollbarElements };
|
package/js/select.js
CHANGED
|
@@ -11,7 +11,18 @@ class Select {
|
|
|
11
11
|
if (result === null) {
|
|
12
12
|
throw new Error(`Select: Failed to initialize select for "${elementOrSelector}"`);
|
|
13
13
|
}
|
|
14
|
-
this.isMultiselect = result;
|
|
14
|
+
this.isMultiselect = result.isMulti;
|
|
15
|
+
this.dropdown = result.dropdown;
|
|
16
|
+
this.documentClickHandler = (e) => {
|
|
17
|
+
if (this.dropdown && !this.dropdown.contains(e.target)) {
|
|
18
|
+
this.dropdown.classList.remove('open');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
document.addEventListener('click', this.documentClickHandler);
|
|
22
|
+
}
|
|
23
|
+
destroy() {
|
|
24
|
+
document.removeEventListener('click', this.documentClickHandler);
|
|
25
|
+
this.dropdown?.classList.remove('open');
|
|
15
26
|
}
|
|
16
27
|
value() {
|
|
17
28
|
if (!this.element) {
|
|
@@ -29,7 +40,16 @@ class Select {
|
|
|
29
40
|
if (!element) {
|
|
30
41
|
return null;
|
|
31
42
|
}
|
|
32
|
-
|
|
43
|
+
const result = Select.initElement(element);
|
|
44
|
+
if (!result)
|
|
45
|
+
return null;
|
|
46
|
+
// Static init path: add document listener without lifecycle management
|
|
47
|
+
document.addEventListener('click', (e) => {
|
|
48
|
+
if (!result.dropdown.contains(e.target)) {
|
|
49
|
+
result.dropdown.classList.remove('open');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return result.isMulti;
|
|
33
53
|
}
|
|
34
54
|
static initElement(element) {
|
|
35
55
|
if (!Select.transformSelect(element)) {
|
|
@@ -74,13 +94,7 @@ class Select {
|
|
|
74
94
|
dropdown.classList.remove('open');
|
|
75
95
|
});
|
|
76
96
|
}
|
|
77
|
-
|
|
78
|
-
document.addEventListener('click', (e) => {
|
|
79
|
-
if (!dropdown.contains(e.target)) {
|
|
80
|
-
dropdown.classList.remove('open');
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
return isMulti;
|
|
97
|
+
return { isMulti, dropdown };
|
|
84
98
|
}
|
|
85
99
|
static closeAllDropdowns(exceptDropdown) {
|
|
86
100
|
document.querySelectorAll('.dropdown').forEach(dropdown => {
|
package/js/select.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
class Select {
|
|
2
2
|
private readonly element: HTMLSelectElement;
|
|
3
3
|
private readonly isMultiselect: boolean;
|
|
4
|
+
private readonly dropdown: HTMLElement | null;
|
|
5
|
+
private readonly documentClickHandler: (e: Event) => void;
|
|
4
6
|
|
|
5
7
|
constructor(elementOrSelector: string | HTMLSelectElement) {
|
|
6
8
|
const element = typeof elementOrSelector === 'string'
|
|
@@ -18,7 +20,20 @@ class Select {
|
|
|
18
20
|
throw new Error(`Select: Failed to initialize select for "${elementOrSelector}"`);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
this.isMultiselect = result;
|
|
23
|
+
this.isMultiselect = result.isMulti;
|
|
24
|
+
this.dropdown = result.dropdown;
|
|
25
|
+
|
|
26
|
+
this.documentClickHandler = (e: Event) => {
|
|
27
|
+
if (this.dropdown && !this.dropdown.contains(e.target as Node)) {
|
|
28
|
+
this.dropdown.classList.remove('open');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener('click', this.documentClickHandler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public destroy(): void {
|
|
35
|
+
document.removeEventListener('click', this.documentClickHandler);
|
|
36
|
+
this.dropdown?.classList.remove('open');
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
public value(): string | string[] | undefined {
|
|
@@ -42,10 +57,20 @@ class Select {
|
|
|
42
57
|
return null;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
const result = Select.initElement(element);
|
|
61
|
+
if (!result) return null;
|
|
62
|
+
|
|
63
|
+
// Static init path: add document listener without lifecycle management
|
|
64
|
+
document.addEventListener('click', (e: Event) => {
|
|
65
|
+
if (!result.dropdown.contains(e.target as Node)) {
|
|
66
|
+
result.dropdown.classList.remove('open');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return result.isMulti;
|
|
46
71
|
}
|
|
47
72
|
|
|
48
|
-
private static initElement(element: HTMLSelectElement): boolean | null {
|
|
73
|
+
private static initElement(element: HTMLSelectElement): { isMulti: boolean; dropdown: HTMLElement } | null {
|
|
49
74
|
if (!Select.transformSelect(element)) {
|
|
50
75
|
return null;
|
|
51
76
|
}
|
|
@@ -98,14 +123,7 @@ class Select {
|
|
|
98
123
|
});
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
document.addEventListener('click', (e: Event) => {
|
|
103
|
-
if (!dropdown.contains(e.target as Node)) {
|
|
104
|
-
dropdown.classList.remove('open');
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return isMulti;
|
|
126
|
+
return { isMulti, dropdown };
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
private static closeAllDropdowns(exceptDropdown?: HTMLElement): void {
|
package/js/stepper.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
class Stepper {
|
|
2
2
|
constructor(elementOrSelector, options = {}) {
|
|
3
|
+
this.abortController = new AbortController();
|
|
3
4
|
const element = typeof elementOrSelector === 'string'
|
|
4
5
|
? document.querySelector(elementOrSelector)
|
|
5
6
|
: elementOrSelector;
|
|
@@ -17,7 +18,7 @@ class Stepper {
|
|
|
17
18
|
if (options.clickable) {
|
|
18
19
|
this.container.classList.add('stepper-clickable');
|
|
19
20
|
this.steps.forEach((step, i) => {
|
|
20
|
-
step.addEventListener('click', () => this.goTo(i));
|
|
21
|
+
step.addEventListener('click', () => this.goTo(i), { signal: this.abortController.signal });
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
this.render();
|
|
@@ -76,5 +77,8 @@ class Stepper {
|
|
|
76
77
|
isLast() {
|
|
77
78
|
return this.current === this.steps.length - 1;
|
|
78
79
|
}
|
|
80
|
+
destroy() {
|
|
81
|
+
this.abortController.abort();
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
84
|
export { Stepper };
|
package/js/stepper.ts
CHANGED
|
@@ -10,6 +10,7 @@ class Stepper {
|
|
|
10
10
|
private connectors: HTMLElement[];
|
|
11
11
|
private current: number;
|
|
12
12
|
private readonly onChange?: (current: number, previous: number) => void;
|
|
13
|
+
private abortController = new AbortController();
|
|
13
14
|
|
|
14
15
|
constructor(elementOrSelector: string | HTMLElement, options: StepperOptions = {}) {
|
|
15
16
|
const element = typeof elementOrSelector === 'string'
|
|
@@ -32,7 +33,7 @@ class Stepper {
|
|
|
32
33
|
if (options.clickable) {
|
|
33
34
|
this.container.classList.add('stepper-clickable');
|
|
34
35
|
this.steps.forEach((step, i) => {
|
|
35
|
-
step.addEventListener('click', () => this.goTo(i));
|
|
36
|
+
step.addEventListener('click', () => this.goTo(i), { signal: this.abortController.signal });
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -99,6 +100,10 @@ class Stepper {
|
|
|
99
100
|
public isLast(): boolean {
|
|
100
101
|
return this.current === this.steps.length - 1;
|
|
101
102
|
}
|
|
103
|
+
|
|
104
|
+
public destroy(): void {
|
|
105
|
+
this.abortController.abort();
|
|
106
|
+
}
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export { Stepper, type StepperOptions };
|
package/js/table.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Select } from "./select.js";
|
|
2
2
|
class Table {
|
|
3
3
|
constructor(elementOrSelector, options = {}) {
|
|
4
|
+
this.abortController = new AbortController();
|
|
4
5
|
const element = typeof elementOrSelector === 'string'
|
|
5
6
|
? document.querySelector(elementOrSelector)
|
|
6
7
|
: elementOrSelector;
|
|
@@ -74,7 +75,7 @@ class Table {
|
|
|
74
75
|
searchInput.className = 'search-input';
|
|
75
76
|
searchInput.addEventListener('input', (e) => {
|
|
76
77
|
this.handleSearch(e.target.value);
|
|
77
|
-
});
|
|
78
|
+
}, { signal: this.abortController.signal });
|
|
78
79
|
controlsDiv.appendChild(searchInput);
|
|
79
80
|
// Page size selector
|
|
80
81
|
const selectGroup = document.createElement('div');
|
|
@@ -93,7 +94,7 @@ class Table {
|
|
|
93
94
|
});
|
|
94
95
|
pageSizeSelect.addEventListener('change', (e) => {
|
|
95
96
|
this.handlePageSizeChange(parseInt(e.target.value, 10));
|
|
96
|
-
});
|
|
97
|
+
}, { signal: this.abortController.signal });
|
|
97
98
|
this.assignUniqueId(pageSizeSelect, 'page-size-select-0');
|
|
98
99
|
selectGroup.appendChild(pageSizeSelect);
|
|
99
100
|
controlsDiv.appendChild(selectGroup);
|
|
@@ -117,7 +118,7 @@ class Table {
|
|
|
117
118
|
th.dataset.key = col.key;
|
|
118
119
|
if (col.sortable !== false) {
|
|
119
120
|
th.classList.add('sortable');
|
|
120
|
-
th.addEventListener('click', () => this.handleSort(col.key));
|
|
121
|
+
th.addEventListener('click', () => this.handleSort(col.key), { signal: this.abortController.signal });
|
|
121
122
|
}
|
|
122
123
|
tr.appendChild(th);
|
|
123
124
|
});
|
|
@@ -355,5 +356,9 @@ class Table {
|
|
|
355
356
|
getData() {
|
|
356
357
|
return this.getFilteredAndSortedData();
|
|
357
358
|
}
|
|
359
|
+
destroy() {
|
|
360
|
+
this.abortController.abort();
|
|
361
|
+
this.container.innerHTML = '';
|
|
362
|
+
}
|
|
358
363
|
}
|
|
359
364
|
export { Table };
|
package/js/table.ts
CHANGED
|
@@ -30,6 +30,7 @@ class Table {
|
|
|
30
30
|
private tableBody!: HTMLTableSectionElement;
|
|
31
31
|
private tableHeader!: HTMLTableSectionElement;
|
|
32
32
|
private paginationContainer!: HTMLDivElement;
|
|
33
|
+
private abortController = new AbortController();
|
|
33
34
|
|
|
34
35
|
constructor(elementOrSelector: string | HTMLElement, options: TableOptions = {}) {
|
|
35
36
|
const element = typeof elementOrSelector === 'string'
|
|
@@ -118,7 +119,7 @@ class Table {
|
|
|
118
119
|
searchInput.className = 'search-input';
|
|
119
120
|
searchInput.addEventListener('input', (e) => {
|
|
120
121
|
this.handleSearch((e.target as HTMLInputElement).value);
|
|
121
|
-
});
|
|
122
|
+
}, { signal: this.abortController.signal });
|
|
122
123
|
controlsDiv.appendChild(searchInput);
|
|
123
124
|
|
|
124
125
|
// Page size selector
|
|
@@ -142,7 +143,7 @@ class Table {
|
|
|
142
143
|
|
|
143
144
|
pageSizeSelect.addEventListener('change', (e) => {
|
|
144
145
|
this.handlePageSizeChange(parseInt((e.target as HTMLSelectElement).value, 10));
|
|
145
|
-
});
|
|
146
|
+
}, { signal: this.abortController.signal });
|
|
146
147
|
|
|
147
148
|
this.assignUniqueId(pageSizeSelect, 'page-size-select-0');
|
|
148
149
|
selectGroup.appendChild(pageSizeSelect);
|
|
@@ -172,7 +173,7 @@ class Table {
|
|
|
172
173
|
|
|
173
174
|
if (col.sortable !== false) {
|
|
174
175
|
th.classList.add('sortable');
|
|
175
|
-
th.addEventListener('click', () => this.handleSort(col.key));
|
|
176
|
+
th.addEventListener('click', () => this.handleSort(col.key), { signal: this.abortController.signal });
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
tr.appendChild(th);
|
|
@@ -448,6 +449,11 @@ class Table {
|
|
|
448
449
|
public getData(): TableRow[] {
|
|
449
450
|
return this.getFilteredAndSortedData();
|
|
450
451
|
}
|
|
452
|
+
|
|
453
|
+
public destroy(): void {
|
|
454
|
+
this.abortController.abort();
|
|
455
|
+
this.container.innerHTML = '';
|
|
456
|
+
}
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
export { Table, TableRow, TableColumn, TableOptions };
|