@dodlhuat/basix 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -5
- package/css/lightbox.scss +272 -0
- package/css/style.css +256 -0
- package/css/style.css.map +1 -1
- package/css/style.scss +1 -0
- package/js/bottom-sheet.js +4 -3
- package/js/bottom-sheet.ts +5 -3
- package/js/calendar.js +9 -5
- package/js/calendar.ts +7 -2
- package/js/carousel.js +15 -11
- package/js/carousel.ts +16 -11
- package/js/chart.js +4 -4
- package/js/chart.ts +5 -3
- package/js/datepicker.js +11 -3
- package/js/datepicker.ts +13 -3
- package/js/docs-nav.js +1 -0
- package/js/editor.js +28 -20
- package/js/editor.ts +28 -20
- package/js/file-uploader.js +6 -10
- package/js/file-uploader.ts +7 -11
- package/js/flyout-menu.js +8 -2
- package/js/flyout-menu.ts +7 -2
- package/js/gallery.js +6 -13
- package/js/gallery.ts +8 -16
- package/js/group-picker.js +10 -7
- package/js/group-picker.ts +11 -7
- package/js/lightbox.js +277 -0
- package/js/lightbox.ts +331 -0
- package/js/modal.js +5 -4
- package/js/modal.ts +6 -4
- package/js/popover.js +4 -2
- package/js/popover.ts +4 -2
- package/js/push-menu.js +3 -2
- package/js/push-menu.ts +4 -2
- package/js/scrollbar.js +31 -23
- package/js/scrollbar.ts +36 -26
- package/js/select.js +23 -9
- package/js/select.ts +29 -11
- package/js/stepper.js +5 -1
- package/js/stepper.ts +6 -1
- package/js/table.js +8 -3
- package/js/table.ts +9 -3
- package/js/timepicker.js +20 -21
- package/js/timepicker.ts +23 -21
- package/js/toast.js +3 -7
- package/js/toast.ts +4 -8
- package/js/tooltip.js +13 -4
- package/js/tooltip.ts +16 -4
- package/js/tree.js +4 -0
- package/js/tree.ts +5 -0
- package/js/utils.js +29 -1
- package/js/utils.ts +36 -1
- package/js/virtual-dropdown.js +4 -8
- package/js/virtual-dropdown.ts +5 -9
- package/package.json +1 -3
package/js/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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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)} · ${((d.value / total) * 100).toFixed(1)}%`);
|
|
297
|
+
this.showTooltip(e, `<strong>${escapeHtml(d.label)}</strong>${this.fmt(d.value)} · ${((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)} · ${((d.value / total) * 100).toFixed(1)}%`
|
|
385
|
+
`<strong>${escapeHtml(d.label)}</strong>${this.fmt(d.value)} · ${((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.
|
|
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.
|
|
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
|
-
|
|
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();
|