@dodlhuat/basix 1.2.2 → 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 +42 -0
- 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 +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 -1
package/js/editor.js
CHANGED
|
@@ -2,6 +2,7 @@ class Editor {
|
|
|
2
2
|
constructor(options = {}) {
|
|
3
3
|
this.undoStack = [];
|
|
4
4
|
this.redoStack = [];
|
|
5
|
+
this.abortController = new AbortController();
|
|
5
6
|
const editable = document.getElementById('editable');
|
|
6
7
|
if (!editable) {
|
|
7
8
|
throw new Error('Editor: #editable element not found');
|
|
@@ -35,23 +36,25 @@ class Editor {
|
|
|
35
36
|
this.saveState();
|
|
36
37
|
}
|
|
37
38
|
bindToolbar() {
|
|
39
|
+
const sig = { signal: this.abortController.signal };
|
|
38
40
|
document.querySelectorAll('[data-cmd]').forEach(btn => {
|
|
39
41
|
btn.addEventListener('click', () => {
|
|
40
42
|
const cmd = btn.dataset.cmd;
|
|
41
43
|
const val = btn.dataset.value ?? null;
|
|
42
44
|
this.exec(cmd, val);
|
|
43
45
|
this.editable.focus();
|
|
44
|
-
});
|
|
46
|
+
}, sig);
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
bindActions() {
|
|
50
|
+
const sig = { signal: this.abortController.signal };
|
|
48
51
|
document.getElementById('linkBtn')?.addEventListener('click', () => {
|
|
49
52
|
const url = prompt('Enter URL:', 'https://');
|
|
50
53
|
if (url)
|
|
51
54
|
this.exec('createLink', url);
|
|
52
|
-
});
|
|
55
|
+
}, sig);
|
|
53
56
|
const imageFile = document.getElementById('imageFile');
|
|
54
|
-
document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
|
|
57
|
+
document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click(), sig);
|
|
55
58
|
imageFile?.addEventListener('change', () => {
|
|
56
59
|
const file = imageFile.files?.[0];
|
|
57
60
|
if (!file)
|
|
@@ -64,7 +67,7 @@ class Editor {
|
|
|
64
67
|
};
|
|
65
68
|
reader.readAsDataURL(file);
|
|
66
69
|
imageFile.value = '';
|
|
67
|
-
});
|
|
70
|
+
}, sig);
|
|
68
71
|
document.getElementById('cleanBtn')?.addEventListener('click', () => {
|
|
69
72
|
const sel = window.getSelection();
|
|
70
73
|
if (!sel || sel.rangeCount === 0)
|
|
@@ -74,13 +77,13 @@ class Editor {
|
|
|
74
77
|
range.deleteContents();
|
|
75
78
|
range.insertNode(document.createTextNode(text));
|
|
76
79
|
this.onContentChange();
|
|
77
|
-
});
|
|
78
|
-
document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
|
|
79
|
-
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
|
|
80
|
+
}, sig);
|
|
81
|
+
document.getElementById('undoBtn')?.addEventListener('click', () => this.undo(), sig);
|
|
82
|
+
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo(), sig);
|
|
80
83
|
document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
|
|
81
84
|
this.sidePanel?.classList.toggle('hidden');
|
|
82
85
|
this.syncViews();
|
|
83
|
-
});
|
|
86
|
+
}, sig);
|
|
84
87
|
// Code action buttons — matched by position within .code-actions
|
|
85
88
|
if (this.code) {
|
|
86
89
|
const code = this.code;
|
|
@@ -88,27 +91,27 @@ class Editor {
|
|
|
88
91
|
codeActions[0]?.addEventListener('click', () => {
|
|
89
92
|
this.editable.innerHTML = this.sanitizeHTML(code.value);
|
|
90
93
|
this.onContentChange();
|
|
91
|
-
});
|
|
94
|
+
}, sig);
|
|
92
95
|
codeActions[1]?.addEventListener('click', () => {
|
|
93
96
|
code.value = this.sanitizeHTML(code.value);
|
|
94
97
|
this.editable.innerHTML = code.value;
|
|
95
98
|
this.onContentChange();
|
|
96
|
-
});
|
|
99
|
+
}, sig);
|
|
97
100
|
codeActions[2]?.addEventListener('click', () => {
|
|
98
101
|
code.value = code.value
|
|
99
102
|
.replace(/\n/g, '')
|
|
100
103
|
.replace(/>\s+</g, '><')
|
|
101
104
|
.trim();
|
|
102
|
-
});
|
|
105
|
+
}, sig);
|
|
103
106
|
}
|
|
104
107
|
const saveBtn = document.getElementById('saveBtn');
|
|
105
|
-
saveBtn?.addEventListener('click', () => this.downloadHTML());
|
|
108
|
+
saveBtn?.addEventListener('click', () => this.downloadHTML(), sig);
|
|
106
109
|
document.getElementById('clearBtn')?.addEventListener('click', () => {
|
|
107
110
|
if (confirm('Clear all content?')) {
|
|
108
111
|
this.editable.innerHTML = '';
|
|
109
112
|
this.onContentChange();
|
|
110
113
|
}
|
|
111
|
-
});
|
|
114
|
+
}, sig);
|
|
112
115
|
}
|
|
113
116
|
bindKeyboard() {
|
|
114
117
|
const saveBtn = document.getElementById('saveBtn');
|
|
@@ -147,19 +150,21 @@ class Editor {
|
|
|
147
150
|
e.preventDefault();
|
|
148
151
|
this.redo();
|
|
149
152
|
}
|
|
150
|
-
});
|
|
153
|
+
}, { signal: this.abortController.signal });
|
|
151
154
|
}
|
|
152
155
|
bindEditable() {
|
|
153
|
-
|
|
156
|
+
const sig = { signal: this.abortController.signal };
|
|
157
|
+
this.editable.addEventListener('input', () => this.onContentChange(), sig);
|
|
154
158
|
this.editable.addEventListener('paste', (e) => {
|
|
155
159
|
e.preventDefault();
|
|
156
160
|
const text = e.clipboardData?.getData('text/plain') ?? '';
|
|
157
161
|
this.insertText(text);
|
|
158
|
-
});
|
|
159
|
-
this.editable.addEventListener('keyup', () => this.refreshActiveState());
|
|
160
|
-
this.editable.addEventListener('mouseup', () => this.refreshActiveState());
|
|
162
|
+
}, sig);
|
|
163
|
+
this.editable.addEventListener('keyup', () => this.refreshActiveState(), sig);
|
|
164
|
+
this.editable.addEventListener('mouseup', () => this.refreshActiveState(), sig);
|
|
161
165
|
}
|
|
162
166
|
bindTabs() {
|
|
167
|
+
const sig = { signal: this.abortController.signal };
|
|
163
168
|
document.querySelectorAll('.side-tab[data-tab]').forEach(tab => {
|
|
164
169
|
tab.addEventListener('click', () => {
|
|
165
170
|
const targetId = tab.dataset.tab;
|
|
@@ -167,7 +172,7 @@ class Editor {
|
|
|
167
172
|
document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
|
|
168
173
|
tab.classList.add('active');
|
|
169
174
|
document.getElementById(targetId)?.classList.add('active');
|
|
170
|
-
});
|
|
175
|
+
}, sig);
|
|
171
176
|
});
|
|
172
177
|
}
|
|
173
178
|
onContentChange() {
|
|
@@ -178,7 +183,7 @@ class Editor {
|
|
|
178
183
|
if (this.code)
|
|
179
184
|
this.code.value = this.editable.innerHTML.trim();
|
|
180
185
|
if (this.preview)
|
|
181
|
-
this.preview.innerHTML = this.editable.innerHTML;
|
|
186
|
+
this.preview.innerHTML = this.sanitizeHTML(this.editable.innerHTML);
|
|
182
187
|
this.updateWordCount();
|
|
183
188
|
}
|
|
184
189
|
updateWordCount() {
|
|
@@ -458,5 +463,8 @@ ${content}
|
|
|
458
463
|
btn.classList.toggle('active', active);
|
|
459
464
|
});
|
|
460
465
|
}
|
|
466
|
+
destroy() {
|
|
467
|
+
this.abortController.abort();
|
|
468
|
+
}
|
|
461
469
|
}
|
|
462
470
|
export { Editor };
|
package/js/editor.ts
CHANGED
|
@@ -12,6 +12,7 @@ class Editor {
|
|
|
12
12
|
private readonly wordCount: HTMLElement | null;
|
|
13
13
|
private undoStack: string[] = [];
|
|
14
14
|
private redoStack: string[] = [];
|
|
15
|
+
private abortController = new AbortController();
|
|
15
16
|
|
|
16
17
|
constructor(options: EditorOptions = {}) {
|
|
17
18
|
const editable = document.getElementById('editable');
|
|
@@ -53,24 +54,26 @@ class Editor {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
private bindToolbar(): void {
|
|
57
|
+
const sig = { signal: this.abortController.signal };
|
|
56
58
|
document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
|
|
57
59
|
btn.addEventListener('click', () => {
|
|
58
60
|
const cmd = btn.dataset.cmd!;
|
|
59
61
|
const val = btn.dataset.value ?? null;
|
|
60
62
|
this.exec(cmd, val);
|
|
61
63
|
this.editable.focus();
|
|
62
|
-
});
|
|
64
|
+
}, sig);
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
private bindActions(): void {
|
|
69
|
+
const sig = { signal: this.abortController.signal };
|
|
67
70
|
document.getElementById('linkBtn')?.addEventListener('click', () => {
|
|
68
71
|
const url = prompt('Enter URL:', 'https://');
|
|
69
72
|
if (url) this.exec('createLink', url);
|
|
70
|
-
});
|
|
73
|
+
}, sig);
|
|
71
74
|
|
|
72
75
|
const imageFile = document.getElementById('imageFile') as HTMLInputElement;
|
|
73
|
-
document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
|
|
76
|
+
document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click(), sig);
|
|
74
77
|
imageFile?.addEventListener('change', () => {
|
|
75
78
|
const file = imageFile.files?.[0];
|
|
76
79
|
if (!file) return;
|
|
@@ -82,7 +85,7 @@ class Editor {
|
|
|
82
85
|
};
|
|
83
86
|
reader.readAsDataURL(file);
|
|
84
87
|
imageFile.value = '';
|
|
85
|
-
});
|
|
88
|
+
}, sig);
|
|
86
89
|
|
|
87
90
|
document.getElementById('cleanBtn')?.addEventListener('click', () => {
|
|
88
91
|
const sel = window.getSelection();
|
|
@@ -92,15 +95,15 @@ class Editor {
|
|
|
92
95
|
range.deleteContents();
|
|
93
96
|
range.insertNode(document.createTextNode(text));
|
|
94
97
|
this.onContentChange();
|
|
95
|
-
});
|
|
98
|
+
}, sig);
|
|
96
99
|
|
|
97
|
-
document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
|
|
98
|
-
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
|
|
100
|
+
document.getElementById('undoBtn')?.addEventListener('click', () => this.undo(), sig);
|
|
101
|
+
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo(), sig);
|
|
99
102
|
|
|
100
103
|
document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
|
|
101
104
|
this.sidePanel?.classList.toggle('hidden');
|
|
102
105
|
this.syncViews();
|
|
103
|
-
});
|
|
106
|
+
}, sig);
|
|
104
107
|
|
|
105
108
|
// Code action buttons — matched by position within .code-actions
|
|
106
109
|
if (this.code) {
|
|
@@ -109,29 +112,29 @@ class Editor {
|
|
|
109
112
|
codeActions[0]?.addEventListener('click', () => {
|
|
110
113
|
this.editable.innerHTML = this.sanitizeHTML(code.value);
|
|
111
114
|
this.onContentChange();
|
|
112
|
-
});
|
|
115
|
+
}, sig);
|
|
113
116
|
codeActions[1]?.addEventListener('click', () => {
|
|
114
117
|
code.value = this.sanitizeHTML(code.value);
|
|
115
118
|
this.editable.innerHTML = code.value;
|
|
116
119
|
this.onContentChange();
|
|
117
|
-
});
|
|
120
|
+
}, sig);
|
|
118
121
|
codeActions[2]?.addEventListener('click', () => {
|
|
119
122
|
code.value = code.value
|
|
120
123
|
.replace(/\n/g, '')
|
|
121
124
|
.replace(/>\s+</g, '><')
|
|
122
125
|
.trim();
|
|
123
|
-
});
|
|
126
|
+
}, sig);
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
const saveBtn = document.getElementById('saveBtn');
|
|
127
|
-
saveBtn?.addEventListener('click', () => this.downloadHTML());
|
|
130
|
+
saveBtn?.addEventListener('click', () => this.downloadHTML(), sig);
|
|
128
131
|
|
|
129
132
|
document.getElementById('clearBtn')?.addEventListener('click', () => {
|
|
130
133
|
if (confirm('Clear all content?')) {
|
|
131
134
|
this.editable.innerHTML = '';
|
|
132
135
|
this.onContentChange();
|
|
133
136
|
}
|
|
134
|
-
});
|
|
137
|
+
}, sig);
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
private bindKeyboard(): void {
|
|
@@ -154,23 +157,25 @@ class Editor {
|
|
|
154
157
|
else if (key === 's') { e.preventDefault(); saveBtn?.click(); }
|
|
155
158
|
else if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); }
|
|
156
159
|
else if (key === 'y' || (key === 'z' && e.shiftKey)) { e.preventDefault(); this.redo(); }
|
|
157
|
-
});
|
|
160
|
+
}, { signal: this.abortController.signal });
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
private bindEditable(): void {
|
|
161
|
-
|
|
164
|
+
const sig = { signal: this.abortController.signal };
|
|
165
|
+
this.editable.addEventListener('input', () => this.onContentChange(), sig);
|
|
162
166
|
|
|
163
167
|
this.editable.addEventListener('paste', (e: ClipboardEvent) => {
|
|
164
168
|
e.preventDefault();
|
|
165
169
|
const text = e.clipboardData?.getData('text/plain') ?? '';
|
|
166
170
|
this.insertText(text);
|
|
167
|
-
});
|
|
171
|
+
}, sig);
|
|
168
172
|
|
|
169
|
-
this.editable.addEventListener('keyup', () => this.refreshActiveState());
|
|
170
|
-
this.editable.addEventListener('mouseup', () => this.refreshActiveState());
|
|
173
|
+
this.editable.addEventListener('keyup', () => this.refreshActiveState(), sig);
|
|
174
|
+
this.editable.addEventListener('mouseup', () => this.refreshActiveState(), sig);
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
private bindTabs(): void {
|
|
178
|
+
const sig = { signal: this.abortController.signal };
|
|
174
179
|
document.querySelectorAll<HTMLElement>('.side-tab[data-tab]').forEach(tab => {
|
|
175
180
|
tab.addEventListener('click', () => {
|
|
176
181
|
const targetId = tab.dataset.tab!;
|
|
@@ -180,7 +185,7 @@ class Editor {
|
|
|
180
185
|
|
|
181
186
|
tab.classList.add('active');
|
|
182
187
|
document.getElementById(targetId)?.classList.add('active');
|
|
183
|
-
});
|
|
188
|
+
}, sig);
|
|
184
189
|
});
|
|
185
190
|
}
|
|
186
191
|
|
|
@@ -191,7 +196,7 @@ class Editor {
|
|
|
191
196
|
|
|
192
197
|
private syncViews(): void {
|
|
193
198
|
if (this.code) this.code.value = this.editable.innerHTML.trim();
|
|
194
|
-
if (this.preview) this.preview.innerHTML = this.editable.innerHTML;
|
|
199
|
+
if (this.preview) this.preview.innerHTML = this.sanitizeHTML(this.editable.innerHTML);
|
|
195
200
|
this.updateWordCount();
|
|
196
201
|
}
|
|
197
202
|
|
|
@@ -477,7 +482,10 @@ ${content}
|
|
|
477
482
|
|
|
478
483
|
btn.classList.toggle('active', active);
|
|
479
484
|
});
|
|
485
|
+
}
|
|
480
486
|
|
|
487
|
+
public destroy(): void {
|
|
488
|
+
this.abortController.abort();
|
|
481
489
|
}
|
|
482
490
|
}
|
|
483
491
|
|
package/js/file-uploader.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
1
2
|
class FileUploader {
|
|
2
3
|
constructor(elementOrSelector, config = {}) {
|
|
3
4
|
this.files = new Map();
|
|
@@ -51,10 +52,10 @@ class FileUploader {
|
|
|
51
52
|
}
|
|
52
53
|
this.container = container;
|
|
53
54
|
this.container.classList.add('file-uploader');
|
|
54
|
-
const dropZone = container.querySelector('
|
|
55
|
-
const fileInput = container.querySelector('
|
|
56
|
-
const fileList = container.querySelector('
|
|
57
|
-
const uploadBtn = container.querySelector('
|
|
55
|
+
const dropZone = container.querySelector('.drop-zone');
|
|
56
|
+
const fileInput = container.querySelector('.file-input');
|
|
57
|
+
const fileList = container.querySelector('.file-list');
|
|
58
|
+
const uploadBtn = container.querySelector('.upload-btn');
|
|
58
59
|
if (!dropZone || !fileInput || !fileList || !uploadBtn) {
|
|
59
60
|
throw new Error('Required elements not found in container');
|
|
60
61
|
}
|
|
@@ -119,7 +120,7 @@ class FileUploader {
|
|
|
119
120
|
const key = this.fileKey(file);
|
|
120
121
|
const item = document.createElement('div');
|
|
121
122
|
item.className = 'file-item';
|
|
122
|
-
const escapedFileName =
|
|
123
|
+
const escapedFileName = escapeHtml(file.name);
|
|
123
124
|
item.innerHTML = `
|
|
124
125
|
<div class="file-item-header">
|
|
125
126
|
<div class="file-info">
|
|
@@ -255,11 +256,6 @@ class FileUploader {
|
|
|
255
256
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
256
257
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
257
258
|
}
|
|
258
|
-
escapeHtml(text) {
|
|
259
|
-
const div = document.createElement('div');
|
|
260
|
-
div.textContent = text;
|
|
261
|
-
return div.innerHTML;
|
|
262
|
-
}
|
|
263
259
|
destroy() {
|
|
264
260
|
this.abortControllers.forEach(abort => abort());
|
|
265
261
|
this.abortControllers.clear();
|
package/js/file-uploader.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
|
|
1
3
|
interface FileData {
|
|
2
4
|
file: File;
|
|
3
5
|
element: HTMLDivElement;
|
|
@@ -44,10 +46,10 @@ class FileUploader {
|
|
|
44
46
|
this.container = container;
|
|
45
47
|
this.container.classList.add('file-uploader');
|
|
46
48
|
|
|
47
|
-
const dropZone = container.querySelector<HTMLElement>('
|
|
48
|
-
const fileInput = container.querySelector<HTMLInputElement>('
|
|
49
|
-
const fileList = container.querySelector<HTMLElement>('
|
|
50
|
-
const uploadBtn = container.querySelector<HTMLButtonElement>('
|
|
49
|
+
const dropZone = container.querySelector<HTMLElement>('.drop-zone');
|
|
50
|
+
const fileInput = container.querySelector<HTMLInputElement>('.file-input');
|
|
51
|
+
const fileList = container.querySelector<HTMLElement>('.file-list');
|
|
52
|
+
const uploadBtn = container.querySelector<HTMLButtonElement>('.upload-btn');
|
|
51
53
|
|
|
52
54
|
if (!dropZone || !fileInput || !fileList || !uploadBtn) {
|
|
53
55
|
throw new Error('Required elements not found in container');
|
|
@@ -182,7 +184,7 @@ class FileUploader {
|
|
|
182
184
|
const item = document.createElement('div');
|
|
183
185
|
item.className = 'file-item';
|
|
184
186
|
|
|
185
|
-
const escapedFileName =
|
|
187
|
+
const escapedFileName = escapeHtml(file.name);
|
|
186
188
|
|
|
187
189
|
item.innerHTML = `
|
|
188
190
|
<div class="file-item-header">
|
|
@@ -332,12 +334,6 @@ class FileUploader {
|
|
|
332
334
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
private escapeHtml(text: string): string {
|
|
336
|
-
const div = document.createElement('div');
|
|
337
|
-
div.textContent = text;
|
|
338
|
-
return div.innerHTML;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
337
|
public destroy(): void {
|
|
342
338
|
this.abortControllers.forEach(abort => abort());
|
|
343
339
|
this.abortControllers.clear();
|
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() {
|