@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.
Files changed (55) hide show
  1. package/README.md +42 -0
  2. package/css/lightbox.scss +272 -0
  3. package/css/style.css +263 -3
  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 -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
- this.editable.addEventListener('input', () => this.onContentChange());
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
- this.editable.addEventListener('input', () => this.onContentChange());
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
 
@@ -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('#drop-zone');
55
- const fileInput = container.querySelector('#file-input');
56
- const fileList = container.querySelector('#file-list');
57
- const uploadBtn = container.querySelector('#upload-btn');
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 = this.escapeHtml(file.name);
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();
@@ -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>('#drop-zone');
48
- const fileInput = container.querySelector<HTMLInputElement>('#file-input');
49
- const fileList = container.querySelector<HTMLElement>('#file-list');
50
- const uploadBtn = container.querySelector<HTMLButtonElement>('#upload-btn');
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 = this.escapeHtml(file.name);
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
- 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() {