@dodlhuat/basix 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +252 -5
  2. package/css/lightbox.scss +272 -0
  3. package/css/style.css +256 -0
  4. package/css/style.css.map +1 -1
  5. package/css/style.scss +1 -0
  6. package/js/bottom-sheet.js +4 -3
  7. package/js/bottom-sheet.ts +5 -3
  8. package/js/calendar.js +9 -5
  9. package/js/calendar.ts +7 -2
  10. package/js/carousel.js +15 -11
  11. package/js/carousel.ts +16 -11
  12. package/js/chart.js +4 -4
  13. package/js/chart.ts +5 -3
  14. package/js/datepicker.js +11 -3
  15. package/js/datepicker.ts +13 -3
  16. package/js/docs-nav.js +1 -0
  17. package/js/editor.js +28 -20
  18. package/js/editor.ts +28 -20
  19. package/js/file-uploader.js +6 -10
  20. package/js/file-uploader.ts +7 -11
  21. package/js/flyout-menu.js +8 -2
  22. package/js/flyout-menu.ts +7 -2
  23. package/js/gallery.js +6 -13
  24. package/js/gallery.ts +8 -16
  25. package/js/group-picker.js +10 -7
  26. package/js/group-picker.ts +11 -7
  27. package/js/lightbox.js +277 -0
  28. package/js/lightbox.ts +331 -0
  29. package/js/modal.js +5 -4
  30. package/js/modal.ts +6 -4
  31. package/js/popover.js +4 -2
  32. package/js/popover.ts +4 -2
  33. package/js/push-menu.js +3 -2
  34. package/js/push-menu.ts +4 -2
  35. package/js/scrollbar.js +31 -23
  36. package/js/scrollbar.ts +36 -26
  37. package/js/select.js +23 -9
  38. package/js/select.ts +29 -11
  39. package/js/stepper.js +5 -1
  40. package/js/stepper.ts +6 -1
  41. package/js/table.js +8 -3
  42. package/js/table.ts +9 -3
  43. package/js/timepicker.js +20 -21
  44. package/js/timepicker.ts +23 -21
  45. package/js/toast.js +3 -7
  46. package/js/toast.ts +4 -8
  47. package/js/tooltip.js +13 -4
  48. package/js/tooltip.ts +16 -4
  49. package/js/tree.js +4 -0
  50. package/js/tree.ts +5 -0
  51. package/js/utils.js +29 -1
  52. package/js/utils.ts +36 -1
  53. package/js/virtual-dropdown.js +4 -8
  54. package/js/virtual-dropdown.ts +5 -9
  55. package/package.json +1 -3
package/js/carousel.ts CHANGED
@@ -16,6 +16,7 @@ class Carousel {
16
16
  private dotsNav!: HTMLDivElement;
17
17
  private dots!: HTMLButtonElement[];
18
18
  private autoPlayTimer: number | null = null;
19
+ private abortController = new AbortController();
19
20
 
20
21
  constructor(elementOrSelector: string | HTMLElement, options: CarouselOptions = {}) {
21
22
  const element = typeof elementOrSelector === 'string'
@@ -96,33 +97,35 @@ class Carousel {
96
97
  }
97
98
 
98
99
  private bindEvents(): void {
99
- this.nextButton.addEventListener('click', () => this.moveToNextSlide());
100
- this.prevButton.addEventListener('click', () => this.moveToPrevSlide());
100
+ const sig = { signal: this.abortController.signal };
101
+
102
+ this.nextButton.addEventListener('click', () => this.moveToNextSlide(), sig);
103
+ this.prevButton.addEventListener('click', () => this.moveToPrevSlide(), sig);
101
104
 
102
105
  this.dotsNav.addEventListener('click', (e: MouseEvent) => {
103
106
  const targetDot = (e.target as HTMLElement).closest('button');
104
107
  if (!targetDot) return;
105
108
  const targetIndex = this.dots.findIndex(dot => dot === targetDot);
106
109
  this.moveToSlide(targetIndex);
107
- });
110
+ }, sig);
108
111
 
109
112
  window.addEventListener('resize', () => {
110
113
  this.slideWidth = this.slides[0].getBoundingClientRect().width;
111
114
  this.moveToSlide(this.currentIndex, false);
112
- });
115
+ }, sig);
113
116
 
114
117
  // Keyboard navigation
115
118
  this.root.addEventListener('keydown', (e: KeyboardEvent) => {
116
119
  if (e.key === 'ArrowLeft') this.moveToPrevSlide();
117
120
  if (e.key === 'ArrowRight') this.moveToNextSlide();
118
- });
121
+ }, sig);
119
122
 
120
123
  // Pause autoplay on hover / focus
121
124
  if (this.options.autoPlay) {
122
- this.root.addEventListener('mouseenter', () => this.pauseAutoPlay());
123
- this.root.addEventListener('mouseleave', () => this.resumeAutoPlay());
124
- this.root.addEventListener('focusin', () => this.pauseAutoPlay());
125
- this.root.addEventListener('focusout', () => this.resumeAutoPlay());
125
+ this.root.addEventListener('mouseenter', () => this.pauseAutoPlay(), sig);
126
+ this.root.addEventListener('mouseleave', () => this.resumeAutoPlay(), sig);
127
+ this.root.addEventListener('focusin', () => this.pauseAutoPlay(), sig);
128
+ this.root.addEventListener('focusout', () => this.resumeAutoPlay(), sig);
126
129
  }
127
130
 
128
131
  this.addTouchSupport();
@@ -171,11 +174,12 @@ class Carousel {
171
174
  private addTouchSupport(): void {
172
175
  let startX = 0;
173
176
  let isDragging = false;
177
+ const sig = { signal: this.abortController.signal };
174
178
 
175
179
  this.track.addEventListener('touchstart', (e: TouchEvent) => {
176
180
  startX = e.touches[0].clientX;
177
181
  isDragging = true;
178
- }, { passive: true });
182
+ }, { passive: true, signal: this.abortController.signal });
179
183
 
180
184
  this.track.addEventListener('touchend', (e: TouchEvent) => {
181
185
  if (!isDragging) return;
@@ -186,7 +190,7 @@ class Carousel {
186
190
  else this.moveToPrevSlide();
187
191
  }
188
192
  isDragging = false;
189
- });
193
+ }, sig);
190
194
  }
191
195
 
192
196
  private startAutoPlay(): void {
@@ -210,6 +214,7 @@ class Carousel {
210
214
 
211
215
  public destroy(): void {
212
216
  this.pauseAutoPlay();
217
+ this.abortController.abort();
213
218
  }
214
219
  }
215
220
 
package/js/chart.js CHANGED
@@ -1,4 +1,4 @@
1
- // ─── Types ──────────────────────────────────────────────────────────────────
1
+ import { escapeHtml } from './utils.js';
2
2
  const MARGIN_XY = { top: 16, right: 24, bottom: 44, left: 52 };
3
3
  const MARGIN_BAR = { top: 8, right: 52, bottom: 24, left: 120 };
4
4
  const MARGIN_PIE = { top: 8, right: 8, bottom: 8, left: 8 };
@@ -294,7 +294,7 @@ class Chart {
294
294
  const { x: dx, y: dy } = this.polar(0, 0, 8, midAngle);
295
295
  path.addEventListener('mouseenter', (e) => {
296
296
  path.style.transform = `translate(${dx}px, ${dy}px)`;
297
- this.showTooltip(e, `<strong>${d.label}</strong>${this.fmt(d.value)} &nbsp;·&nbsp; ${((d.value / total) * 100).toFixed(1)}%`);
297
+ this.showTooltip(e, `<strong>${escapeHtml(d.label)}</strong>${this.fmt(d.value)} &nbsp;·&nbsp; ${((d.value / total) * 100).toFixed(1)}%`);
298
298
  }, { signal: this.abortController.signal });
299
299
  path.addEventListener('mouseleave', () => {
300
300
  path.style.transform = '';
@@ -483,7 +483,7 @@ class Chart {
483
483
  onPoint(g, s, d, i) {
484
484
  const sig = { signal: this.abortController.signal };
485
485
  g.addEventListener('mouseenter', (e) => {
486
- this.showTooltip(e, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
486
+ this.showTooltip(e, `<strong>${escapeHtml(d.label)}</strong>${escapeHtml(s.name)}: ${this.fmt(d.value)}`);
487
487
  }, sig);
488
488
  g.addEventListener('mousemove', (e) => this.moveTooltip(e), sig);
489
489
  g.addEventListener('mouseleave', () => this.hideTooltip(), sig);
@@ -493,7 +493,7 @@ class Chart {
493
493
  const sig = { signal: this.abortController.signal };
494
494
  rect.style.cursor = 'pointer';
495
495
  rect.addEventListener('mouseenter', (e) => {
496
- this.showTooltip(e, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
496
+ this.showTooltip(e, `<strong>${escapeHtml(d.label)}</strong>${escapeHtml(s.name)}: ${this.fmt(d.value)}`);
497
497
  }, sig);
498
498
  rect.addEventListener('mousemove', (e) => this.moveTooltip(e), sig);
499
499
  rect.addEventListener('mouseleave', () => this.hideTooltip(), sig);
package/js/chart.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { escapeHtml } from './utils.js';
2
+
1
3
  // ─── Types ──────────────────────────────────────────────────────────────────
2
4
 
3
5
  export type ChartType = 'line' | 'area' | 'column' | 'bar' | 'pie';
@@ -380,7 +382,7 @@ class Chart {
380
382
  path.addEventListener('mouseenter', (e) => {
381
383
  path.style.transform = `translate(${dx}px, ${dy}px)`;
382
384
  this.showTooltip(e as MouseEvent,
383
- `<strong>${d.label}</strong>${this.fmt(d.value)} &nbsp;·&nbsp; ${((d.value / total) * 100).toFixed(1)}%`
385
+ `<strong>${escapeHtml(d.label)}</strong>${this.fmt(d.value)} &nbsp;·&nbsp; ${((d.value / total) * 100).toFixed(1)}%`
384
386
  );
385
387
  }, { signal: this.abortController.signal });
386
388
 
@@ -589,7 +591,7 @@ class Chart {
589
591
  private onPoint(g: SVGElement, s: ChartSeries, d: ChartDataPoint, i: number): void {
590
592
  const sig = { signal: this.abortController.signal };
591
593
  g.addEventListener('mouseenter', (e) => {
592
- this.showTooltip(e as MouseEvent, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
594
+ this.showTooltip(e as MouseEvent, `<strong>${escapeHtml(d.label)}</strong>${escapeHtml(s.name)}: ${this.fmt(d.value)}`);
593
595
  }, sig);
594
596
  g.addEventListener('mousemove', (e) => this.moveTooltip(e as MouseEvent), sig);
595
597
  g.addEventListener('mouseleave', () => this.hideTooltip(), sig);
@@ -600,7 +602,7 @@ class Chart {
600
602
  const sig = { signal: this.abortController.signal };
601
603
  rect.style.cursor = 'pointer';
602
604
  rect.addEventListener('mouseenter', (e) => {
603
- this.showTooltip(e as MouseEvent, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
605
+ this.showTooltip(e as MouseEvent, `<strong>${escapeHtml(d.label)}</strong>${escapeHtml(s.name)}: ${this.fmt(d.value)}`);
604
606
  }, sig);
605
607
  rect.addEventListener('mousemove', (e) => this.moveTooltip(e as MouseEvent), sig);
606
608
  rect.addEventListener('mouseleave', () => this.hideTooltip(), sig);
package/js/datepicker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  class DatePicker {
2
2
  constructor(elementOrSelector, options = {}) {
3
+ this.abortController = new AbortController();
3
4
  this.input = typeof elementOrSelector === 'string'
4
5
  ? document.querySelector(elementOrSelector)
5
6
  : elementOrSelector;
@@ -52,7 +53,7 @@ class DatePicker {
52
53
  this.backdrop = document.createElement('div');
53
54
  this.backdrop.className = 'datepicker-backdrop';
54
55
  document.body.appendChild(this.backdrop);
55
- this.backdrop.addEventListener('click', () => this.hide());
56
+ this.backdrop.addEventListener('click', () => this.hide(), { signal: this.abortController.signal });
56
57
  }
57
58
  attachEvents() {
58
59
  const toggle = (e) => {
@@ -65,12 +66,13 @@ class DatePicker {
65
66
  this.show();
66
67
  }
67
68
  };
68
- this.input?.addEventListener('click', toggle);
69
+ const sig = { signal: this.abortController.signal };
70
+ this.input?.addEventListener('click', toggle, sig);
69
71
  this.backdrop.addEventListener('click', (e) => {
70
72
  e.preventDefault();
71
73
  e.stopPropagation();
72
74
  this.hide();
73
- });
75
+ }, sig);
74
76
  this.handleDocumentClick = (e) => {
75
77
  if (this.calendar.classList.contains('mobile'))
76
78
  return;
@@ -501,5 +503,11 @@ class DatePicker {
501
503
  this.input.value = value;
502
504
  }
503
505
  }
506
+ destroy() {
507
+ this.hide();
508
+ this.abortController.abort();
509
+ this.calendar.remove();
510
+ this.backdrop.remove();
511
+ }
504
512
  }
505
513
  export { DatePicker };
package/js/datepicker.ts CHANGED
@@ -35,6 +35,7 @@ class DatePicker {
35
35
  private calendar!: HTMLDivElement;
36
36
  private backdrop!: HTMLDivElement;
37
37
  private handleDocumentClick!: (e: Event) => void;
38
+ private abortController = new AbortController();
38
39
 
39
40
 
40
41
  constructor(elementOrSelector: string | HTMLInputElement, options: DatePickerOptions = {}) {
@@ -102,7 +103,7 @@ class DatePicker {
102
103
  this.backdrop.className = 'datepicker-backdrop';
103
104
  document.body.appendChild(this.backdrop);
104
105
 
105
- this.backdrop.addEventListener('click', () => this.hide());
106
+ this.backdrop.addEventListener('click', () => this.hide(), { signal: this.abortController.signal });
106
107
  }
107
108
 
108
109
  private attachEvents(): void {
@@ -117,13 +118,15 @@ class DatePicker {
117
118
  }
118
119
  };
119
120
 
120
- this.input?.addEventListener('click', toggle);
121
+ const sig = { signal: this.abortController.signal };
122
+
123
+ this.input?.addEventListener('click', toggle, sig);
121
124
 
122
125
  this.backdrop.addEventListener('click', (e: Event) => {
123
126
  e.preventDefault();
124
127
  e.stopPropagation();
125
128
  this.hide();
126
- });
129
+ }, sig);
127
130
 
128
131
  this.handleDocumentClick = (e: Event): void => {
129
132
  if (this.calendar.classList.contains('mobile')) return;
@@ -622,6 +625,13 @@ class DatePicker {
622
625
  this.input.value = value;
623
626
  }
624
627
  }
628
+
629
+ public destroy(): void {
630
+ this.hide();
631
+ this.abortController.abort();
632
+ this.calendar.remove();
633
+ this.backdrop.remove();
634
+ }
625
635
  }
626
636
 
627
637
  export { DatePicker };
package/js/docs-nav.js CHANGED
@@ -39,6 +39,7 @@ const NAV = [
39
39
  { label: 'Tooltip', href: 'overlays/tooltip.html' },
40
40
  { label: 'Bottom Sheet', href: 'overlays/bottom-sheet.html' },
41
41
  { label: 'Toast', href: 'overlays/toast.html' },
42
+ { label: 'Lightbox', href: 'overlays/lightbox.html' },
42
43
  ],
43
44
  },
44
45
  {
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();