@dodlhuat/basix 1.3.1 → 1.3.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 +14 -8
- package/css/accordion.scss +0 -5
- package/css/badge.scss +1 -6
- package/css/bottom-sheet.scss +3 -8
- package/css/breadcrumb.scss +6 -15
- package/css/button.scss +2 -1
- package/css/calendar.scss +0 -67
- package/css/card.scss +0 -5
- package/css/carousel.scss +0 -3
- package/css/chart.scss +0 -25
- package/css/chat-bubbles.scss +0 -15
- package/css/checkbox.scss +3 -2
- package/css/chips.scss +3 -7
- package/css/code-viewer.scss +1 -5
- package/css/context-menu.scss +5 -21
- package/css/datepicker.scss +6 -9
- package/css/docs.scss +0 -4
- package/css/dropdown.scss +1 -1
- package/css/editor.scss +1 -23
- package/css/file-uploader.scss +2 -2
- package/css/flyout-menu.scss +65 -58
- package/css/form.scss +0 -28
- package/css/gallery.scss +2 -3
- package/css/group-picker.scss +5 -35
- package/css/icons.scss +0 -3
- package/css/lightbox.scss +2 -4
- package/css/mixins.scss +8 -0
- package/css/modal.scss +3 -3
- package/css/parameters.scss +6 -1
- package/css/popover.scss +3 -15
- package/css/progress.scss +0 -6
- package/css/push-menu.scss +3 -28
- package/css/radiobutton.scss +2 -1
- package/css/range-slider.scss +1 -7
- package/css/scrollbar.scss +9 -16
- package/css/sidebar-nav.scss +0 -12
- package/css/stepper.scss +0 -4
- package/css/style.css +108 -116
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.min.css.map +1 -1
- package/css/style.scss +1 -1
- package/css/table.scss +0 -4
- package/css/tabs.scss +0 -2
- package/css/timeline.scss +1 -13
- package/css/timepicker.scss +55 -39
- package/css/toast.scss +1 -1
- package/css/tooltip.scss +1 -5
- package/css/tree.scss +1 -1
- package/css/typography.scss +3 -3
- package/css/virtual-dropdown.scss +3 -28
- package/js/bottom-sheet.d.ts +3 -1
- package/js/bottom-sheet.js +26 -27
- package/js/calendar.d.ts +7 -0
- package/js/calendar.js +14 -33
- package/js/carousel.d.ts +2 -0
- package/js/carousel.js +13 -5
- package/js/chart.d.ts +4 -0
- package/js/chart.js +13 -31
- package/js/code-viewer.d.ts +1 -0
- package/js/code-viewer.js +4 -0
- package/js/context-menu.d.ts +9 -2
- package/js/context-menu.js +17 -14
- package/js/datepicker.d.ts +4 -0
- package/js/datepicker.js +26 -11
- package/js/dropdown.d.ts +3 -3
- package/js/dropdown.js +6 -9
- package/js/editor.d.ts +1 -1
- package/js/editor.js +14 -20
- package/js/file-uploader.d.ts +4 -0
- package/js/file-uploader.js +52 -43
- package/js/flyout-menu.d.ts +5 -3
- package/js/flyout-menu.js +23 -46
- package/js/gallery.d.ts +4 -0
- package/js/gallery.js +39 -50
- package/js/group-picker.d.ts +5 -0
- package/js/group-picker.js +12 -17
- package/js/lightbox.d.ts +3 -0
- package/js/lightbox.js +12 -6
- package/js/modal.d.ts +3 -1
- package/js/modal.js +14 -11
- package/js/popover.d.ts +2 -0
- package/js/popover.js +26 -30
- package/js/position.d.ts +2 -0
- package/js/position.js +1 -5
- package/js/push-menu.d.ts +2 -1
- package/js/push-menu.js +25 -48
- package/js/range-slider.d.ts +1 -0
- package/js/range-slider.js +5 -3
- package/js/scroll.d.ts +2 -0
- package/js/scroll.js +1 -0
- package/js/scrollbar.d.ts +2 -0
- package/js/scrollbar.js +24 -36
- package/js/select.d.ts +1 -0
- package/js/select.js +5 -10
- package/js/sidebar-nav.d.ts +2 -0
- package/js/sidebar-nav.js +8 -0
- package/js/stepper.d.ts +2 -0
- package/js/stepper.js +7 -1
- package/js/table.d.ts +4 -0
- package/js/table.js +15 -22
- package/js/tabs.d.ts +2 -0
- package/js/tabs.js +6 -14
- package/js/theme.d.ts +1 -0
- package/js/theme.js +5 -13
- package/js/timepicker.d.ts +22 -5
- package/js/timepicker.js +160 -57
- package/js/toast.d.ts +3 -1
- package/js/toast.js +25 -22
- package/js/tooltip.d.ts +2 -0
- package/js/tooltip.js +21 -19
- package/js/tree.d.ts +3 -0
- package/js/tree.js +13 -0
- package/js/utils.d.ts +1 -3
- package/js/utils.js +0 -3
- package/js/virtual-dropdown.d.ts +3 -0
- package/js/virtual-dropdown.js +25 -0
- package/package.json +2 -2
package/js/context-menu.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
/** Right-click context menu with keyboard navigation and nested submenu support. */
|
|
1
2
|
class ContextMenu {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
items;
|
|
4
|
+
targets;
|
|
5
|
+
menuEl = null;
|
|
6
|
+
currentTarget = null;
|
|
7
|
+
abortController = new AbortController();
|
|
8
|
+
spritePath;
|
|
9
|
+
constructor(selectorOrElement, items, options = {}) {
|
|
6
10
|
this.items = items;
|
|
11
|
+
this.spritePath = options.spritePath ?? null;
|
|
7
12
|
if (typeof selectorOrElement === 'string') {
|
|
8
13
|
this.targets = Array.from(document.querySelectorAll(selectorOrElement));
|
|
9
14
|
}
|
|
@@ -25,7 +30,6 @@ class ContextMenu {
|
|
|
25
30
|
}, { signal });
|
|
26
31
|
});
|
|
27
32
|
document.addEventListener('click', () => this.close(), { signal });
|
|
28
|
-
// Close on right-click outside the menu
|
|
29
33
|
document.addEventListener('contextmenu', (e) => {
|
|
30
34
|
if (this.menuEl && !this.menuEl.contains(e.target)) {
|
|
31
35
|
this.close();
|
|
@@ -50,7 +54,6 @@ class ContextMenu {
|
|
|
50
54
|
this.activateFocused();
|
|
51
55
|
}
|
|
52
56
|
}, { signal });
|
|
53
|
-
// Close on scroll outside the menu
|
|
54
57
|
window.addEventListener('scroll', (e) => {
|
|
55
58
|
if (!this.menuEl?.contains(e.target))
|
|
56
59
|
this.close();
|
|
@@ -61,14 +64,12 @@ class ContextMenu {
|
|
|
61
64
|
this.close();
|
|
62
65
|
this.menuEl = this.buildMenu(this.items);
|
|
63
66
|
document.body.appendChild(this.menuEl);
|
|
64
|
-
// Use offsetWidth/offsetHeight — unaffected by CSS transform
|
|
65
67
|
const w = this.menuEl.offsetWidth;
|
|
66
68
|
const h = this.menuEl.offsetHeight;
|
|
67
69
|
const vw = window.innerWidth;
|
|
68
70
|
const vh = window.innerHeight;
|
|
69
71
|
const left = x + w > vw ? vw - w - 8 : x;
|
|
70
72
|
const top = y + h > vh ? vh - h - 8 : y;
|
|
71
|
-
// Set transform-origin to match the corner the menu opens from
|
|
72
73
|
const originX = x + w > vw ? 'right' : 'left';
|
|
73
74
|
const originY = y + h > vh ? 'bottom' : 'top';
|
|
74
75
|
this.menuEl.style.left = `${left}px`;
|
|
@@ -82,7 +83,6 @@ class ContextMenu {
|
|
|
82
83
|
const el = this.menuEl;
|
|
83
84
|
this.menuEl = null;
|
|
84
85
|
el.classList.remove('is-visible');
|
|
85
|
-
// Wait for exit transition then remove from DOM
|
|
86
86
|
el.addEventListener('transitionend', () => el.remove(), { once: true });
|
|
87
87
|
setTimeout(() => el.isConnected && el.remove(), 200);
|
|
88
88
|
}
|
|
@@ -116,11 +116,16 @@ class ContextMenu {
|
|
|
116
116
|
li.classList.add('is-destructive');
|
|
117
117
|
if (def.submenu)
|
|
118
118
|
li.classList.add('has-submenu');
|
|
119
|
-
// Always render icon slot — keeps label column aligned across all items
|
|
120
119
|
const iconWrap = document.createElement('span');
|
|
121
120
|
iconWrap.className = 'context-menu-icon';
|
|
122
|
-
if (def.icon) {
|
|
123
|
-
|
|
121
|
+
if (def.icon && this.spritePath) {
|
|
122
|
+
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
123
|
+
svgEl.setAttribute('aria-hidden', 'true');
|
|
124
|
+
svgEl.setAttribute('fill', 'currentColor');
|
|
125
|
+
const useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
126
|
+
useEl.setAttribute('href', `${this.spritePath}#${def.icon}`);
|
|
127
|
+
svgEl.appendChild(useEl);
|
|
128
|
+
iconWrap.appendChild(svgEl);
|
|
124
129
|
}
|
|
125
130
|
li.appendChild(iconWrap);
|
|
126
131
|
const label = document.createElement('span');
|
|
@@ -139,7 +144,6 @@ class ContextMenu {
|
|
|
139
144
|
li.appendChild(chevron);
|
|
140
145
|
const submenuEl = this.buildMenu(def.submenu);
|
|
141
146
|
li.appendChild(submenuEl);
|
|
142
|
-
// Determine flip synchronously from parent position — no rAF flash
|
|
143
147
|
const shouldFlip = () => {
|
|
144
148
|
const rect = li.getBoundingClientRect();
|
|
145
149
|
return rect.right + submenuEl.offsetWidth > window.innerWidth;
|
|
@@ -179,7 +183,6 @@ class ContextMenu {
|
|
|
179
183
|
return li;
|
|
180
184
|
}
|
|
181
185
|
closeAllSubmenus(menu) {
|
|
182
|
-
// Only close direct-child submenus of this menu level
|
|
183
186
|
Array.from(menu.children).forEach((child) => {
|
|
184
187
|
child.classList.remove('is-active');
|
|
185
188
|
});
|
package/js/datepicker.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/** Localised day and month names for the DatePicker. */
|
|
1
2
|
interface DatePickerLocales {
|
|
2
3
|
days: string[];
|
|
3
4
|
months: string[];
|
|
4
5
|
}
|
|
6
|
+
/** Configuration options for the DatePicker. */
|
|
5
7
|
interface DatePickerOptions {
|
|
6
8
|
mode?: 'single' | 'range';
|
|
7
9
|
startDay?: number;
|
|
@@ -10,10 +12,12 @@ interface DatePickerOptions {
|
|
|
10
12
|
format?: (date: Date) => string;
|
|
11
13
|
onSelect?: (date: Date | DateRange) => void;
|
|
12
14
|
}
|
|
15
|
+
/** A date range with optional start and end dates. */
|
|
13
16
|
interface DateRange {
|
|
14
17
|
start: Date | null;
|
|
15
18
|
end: Date | null;
|
|
16
19
|
}
|
|
20
|
+
/** Calendar-based date (or date-range) picker that attaches to an input element. */
|
|
17
21
|
declare class DatePicker {
|
|
18
22
|
private input;
|
|
19
23
|
private options;
|
package/js/datepicker.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
import { computePosition } from './position.js';
|
|
2
|
+
/** Calendar-based date (or date-range) picker that attaches to an input element. */
|
|
1
3
|
class DatePicker {
|
|
4
|
+
input;
|
|
5
|
+
options;
|
|
6
|
+
currentDate;
|
|
7
|
+
selectedDate;
|
|
8
|
+
rangeStart;
|
|
9
|
+
rangeEnd;
|
|
10
|
+
viewYear;
|
|
11
|
+
viewMonth;
|
|
12
|
+
viewMode;
|
|
13
|
+
yearRangeStart;
|
|
14
|
+
selectedHours;
|
|
15
|
+
selectedMinutes;
|
|
16
|
+
calendar;
|
|
17
|
+
backdrop;
|
|
18
|
+
handleDocumentClick;
|
|
19
|
+
abortController = new AbortController();
|
|
2
20
|
constructor(elementOrSelector, options = {}) {
|
|
3
|
-
this.abortController = new AbortController();
|
|
4
21
|
this.input = typeof elementOrSelector === 'string'
|
|
5
22
|
? document.querySelector(elementOrSelector)
|
|
6
23
|
: elementOrSelector;
|
|
@@ -96,14 +113,14 @@ class DatePicker {
|
|
|
96
113
|
this.backdrop.classList.remove('visible');
|
|
97
114
|
document.body.style.overflow = '';
|
|
98
115
|
if (this.input) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
this.calendar.style.
|
|
103
|
-
this.calendar.style.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
116
|
+
this.calendar.style.display = 'block';
|
|
117
|
+
this.calendar.style.visibility = 'hidden';
|
|
118
|
+
const calRect = this.calendar.getBoundingClientRect();
|
|
119
|
+
this.calendar.style.display = '';
|
|
120
|
+
this.calendar.style.visibility = '';
|
|
121
|
+
const { left, top } = computePosition(this.input.getBoundingClientRect(), calRect, { placement: 'bottom', align: 'start', offset: 5 });
|
|
122
|
+
this.calendar.style.top = `${top}px`;
|
|
123
|
+
this.calendar.style.left = `${left}px`;
|
|
107
124
|
}
|
|
108
125
|
setTimeout(() => {
|
|
109
126
|
if (this.calendar.classList.contains('visible')) {
|
|
@@ -345,7 +362,6 @@ class DatePicker {
|
|
|
345
362
|
wrapper.appendChild(label);
|
|
346
363
|
const controls = document.createElement('div');
|
|
347
364
|
controls.className = 'datepicker-time-controls';
|
|
348
|
-
// Hours spinner
|
|
349
365
|
const hoursSpinner = this.createSpinner(this.selectedHours, 0, 23, (value) => {
|
|
350
366
|
this.selectedHours = value;
|
|
351
367
|
this.applyTimeToSelection();
|
|
@@ -353,7 +369,6 @@ class DatePicker {
|
|
|
353
369
|
const separator = document.createElement('span');
|
|
354
370
|
separator.className = 'datepicker-time-separator';
|
|
355
371
|
separator.textContent = ':';
|
|
356
|
-
// Minutes spinner
|
|
357
372
|
const minutesSpinner = this.createSpinner(this.selectedMinutes, 0, 59, (value) => {
|
|
358
373
|
this.selectedMinutes = value;
|
|
359
374
|
this.applyTimeToSelection();
|
package/js/dropdown.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/** Configuration options for a Dropdown instance. */
|
|
1
2
|
interface DropdownOptions {
|
|
2
3
|
closeOnSelect?: boolean;
|
|
3
4
|
allowMultipleOpen?: boolean;
|
|
4
5
|
}
|
|
6
|
+
/** Event detail payload for the `dropdown-select` custom event. */
|
|
5
7
|
interface DropdownSelectDetail {
|
|
6
8
|
text: string;
|
|
7
9
|
element: HTMLElement;
|
|
8
10
|
}
|
|
11
|
+
/** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
|
|
9
12
|
declare class Dropdown {
|
|
10
13
|
private container;
|
|
11
14
|
private trigger;
|
|
@@ -22,9 +25,6 @@ declare class Dropdown {
|
|
|
22
25
|
private toggleSubmenu;
|
|
23
26
|
private closeAllSubmenus;
|
|
24
27
|
private handleSelection;
|
|
25
|
-
/**
|
|
26
|
-
* Cleanup method to remove event listeners
|
|
27
|
-
*/
|
|
28
28
|
destroy(): void;
|
|
29
29
|
}
|
|
30
30
|
export { Dropdown, DropdownSelectDetail };
|
package/js/dropdown.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
|
|
1
2
|
class Dropdown {
|
|
3
|
+
container;
|
|
4
|
+
trigger;
|
|
5
|
+
menu;
|
|
6
|
+
options;
|
|
7
|
+
abortController;
|
|
2
8
|
constructor(selector, options = {}) {
|
|
3
9
|
const container = document.querySelector(selector);
|
|
4
10
|
if (!container) {
|
|
@@ -26,18 +32,15 @@ class Dropdown {
|
|
|
26
32
|
}
|
|
27
33
|
attachEventListeners() {
|
|
28
34
|
const { signal } = this.abortController;
|
|
29
|
-
// Toggle main dropdown
|
|
30
35
|
this.trigger.addEventListener('click', (e) => {
|
|
31
36
|
e.stopPropagation();
|
|
32
37
|
this.toggle();
|
|
33
38
|
}, { signal });
|
|
34
|
-
// Close when clicking outside
|
|
35
39
|
document.addEventListener('click', (e) => {
|
|
36
40
|
if (!this.container.contains(e.target)) {
|
|
37
41
|
this.close();
|
|
38
42
|
}
|
|
39
43
|
}, { signal });
|
|
40
|
-
// Handle item clicks using event delegation
|
|
41
44
|
this.menu.addEventListener('click', (e) => {
|
|
42
45
|
e.stopPropagation();
|
|
43
46
|
const target = e.target;
|
|
@@ -78,7 +81,6 @@ class Dropdown {
|
|
|
78
81
|
}
|
|
79
82
|
toggleSubmenu(li) {
|
|
80
83
|
const isOpening = !li.classList.contains('open');
|
|
81
|
-
// Close siblings if not allowing multiple open menus
|
|
82
84
|
if (isOpening && !this.options.allowMultipleOpen) {
|
|
83
85
|
const parent = li.parentElement;
|
|
84
86
|
if (parent) {
|
|
@@ -86,7 +88,6 @@ class Dropdown {
|
|
|
86
88
|
siblings.forEach((sibling) => {
|
|
87
89
|
if (sibling !== li && sibling.classList.contains('open')) {
|
|
88
90
|
sibling.classList.remove('open');
|
|
89
|
-
// Close deeply nested open items
|
|
90
91
|
const deepOpenItems = sibling.querySelectorAll('.open');
|
|
91
92
|
deepOpenItems.forEach((el) => el.classList.remove('open'));
|
|
92
93
|
}
|
|
@@ -101,7 +102,6 @@ class Dropdown {
|
|
|
101
102
|
}
|
|
102
103
|
handleSelection(item) {
|
|
103
104
|
const text = item.textContent?.trim() ?? '';
|
|
104
|
-
// Dispatch custom event with proper typing
|
|
105
105
|
const event = new CustomEvent('dropdown-select', {
|
|
106
106
|
detail: {
|
|
107
107
|
text,
|
|
@@ -111,9 +111,6 @@ class Dropdown {
|
|
|
111
111
|
});
|
|
112
112
|
this.container.dispatchEvent(event);
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Cleanup method to remove event listeners
|
|
116
|
-
*/
|
|
117
114
|
destroy() {
|
|
118
115
|
this.abortController.abort();
|
|
119
116
|
this.close();
|
package/js/editor.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ interface EditorOptions {
|
|
|
3
3
|
* without #code, #preview, or #sidePanel in the DOM. */
|
|
4
4
|
simple?: boolean;
|
|
5
5
|
}
|
|
6
|
+
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
6
7
|
declare class Editor {
|
|
7
8
|
private readonly editable;
|
|
8
9
|
private readonly code;
|
|
@@ -33,7 +34,6 @@ declare class Editor {
|
|
|
33
34
|
private insertList;
|
|
34
35
|
private setAlignment;
|
|
35
36
|
private setForeColor;
|
|
36
|
-
private sanitizeHTML;
|
|
37
37
|
private downloadHTML;
|
|
38
38
|
private refreshActiveState;
|
|
39
39
|
destroy(): void;
|
package/js/editor.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { sanitizeHtml } from './utils.js';
|
|
2
|
+
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
1
3
|
class Editor {
|
|
4
|
+
editable;
|
|
5
|
+
code;
|
|
6
|
+
preview;
|
|
7
|
+
sidePanel;
|
|
8
|
+
wordCount;
|
|
9
|
+
undoStack = [];
|
|
10
|
+
redoStack = [];
|
|
11
|
+
abortController = new AbortController();
|
|
2
12
|
constructor(options = {}) {
|
|
3
|
-
this.undoStack = [];
|
|
4
|
-
this.redoStack = [];
|
|
5
|
-
this.abortController = new AbortController();
|
|
6
13
|
const editable = document.getElementById('editable');
|
|
7
14
|
if (!editable) {
|
|
8
15
|
throw new Error('Editor: #editable element not found');
|
|
@@ -89,11 +96,11 @@ class Editor {
|
|
|
89
96
|
const code = this.code;
|
|
90
97
|
const codeActions = document.querySelectorAll('.code-actions button');
|
|
91
98
|
codeActions[0]?.addEventListener('click', () => {
|
|
92
|
-
this.editable.innerHTML =
|
|
99
|
+
this.editable.innerHTML = sanitizeHtml(code.value);
|
|
93
100
|
this.onContentChange();
|
|
94
101
|
}, sig);
|
|
95
102
|
codeActions[1]?.addEventListener('click', () => {
|
|
96
|
-
code.value =
|
|
103
|
+
code.value = sanitizeHtml(code.value);
|
|
97
104
|
this.editable.innerHTML = code.value;
|
|
98
105
|
this.onContentChange();
|
|
99
106
|
}, sig);
|
|
@@ -183,7 +190,7 @@ class Editor {
|
|
|
183
190
|
if (this.code)
|
|
184
191
|
this.code.value = this.editable.innerHTML.trim();
|
|
185
192
|
if (this.preview)
|
|
186
|
-
this.preview.innerHTML =
|
|
193
|
+
this.preview.innerHTML = sanitizeHtml(this.editable.innerHTML);
|
|
187
194
|
this.updateWordCount();
|
|
188
195
|
}
|
|
189
196
|
updateWordCount() {
|
|
@@ -407,21 +414,8 @@ class Editor {
|
|
|
407
414
|
sel.addRange(range);
|
|
408
415
|
this.onContentChange();
|
|
409
416
|
}
|
|
410
|
-
sanitizeHTML(html) {
|
|
411
|
-
const parser = new DOMParser();
|
|
412
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
413
|
-
doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
|
|
414
|
-
doc.querySelectorAll('*').forEach(el => {
|
|
415
|
-
for (const attr of Array.from(el.attributes)) {
|
|
416
|
-
if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
|
|
417
|
-
el.removeAttribute(attr.name);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return doc.body.innerHTML;
|
|
422
|
-
}
|
|
423
417
|
downloadHTML() {
|
|
424
|
-
const content =
|
|
418
|
+
const content = sanitizeHtml(this.editable.innerHTML);
|
|
425
419
|
const html = `<!doctype html>
|
|
426
420
|
<html lang="en">
|
|
427
421
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
|
package/js/file-uploader.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
/** Event detail payload for the `upload-completed` custom event. */
|
|
1
2
|
interface UploadCompletedDetail {
|
|
2
3
|
fileCount: number;
|
|
3
4
|
files: File[];
|
|
4
5
|
results: PromiseSettledResult<unknown>[];
|
|
5
6
|
}
|
|
7
|
+
/** Event detail payload for the `file-validation-error` custom event. */
|
|
6
8
|
interface FileValidationErrorDetail {
|
|
7
9
|
file: File;
|
|
8
10
|
reason: 'size' | 'type';
|
|
9
11
|
}
|
|
12
|
+
/** Configuration options for the FileUploader. */
|
|
10
13
|
interface FileUploaderConfig {
|
|
11
14
|
uploadUrl?: string;
|
|
12
15
|
maxFileSize?: number;
|
|
13
16
|
allowedTypes?: string[];
|
|
14
17
|
}
|
|
18
|
+
/** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
|
|
15
19
|
declare class FileUploader {
|
|
16
20
|
private container;
|
|
17
21
|
private dropZone;
|
package/js/file-uploader.js
CHANGED
|
@@ -1,49 +1,17 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
|
+
/** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
|
|
2
3
|
class FileUploader {
|
|
4
|
+
container;
|
|
5
|
+
dropZone;
|
|
6
|
+
fileInput;
|
|
7
|
+
fileList;
|
|
8
|
+
uploadBtn;
|
|
9
|
+
files = new Map();
|
|
10
|
+
uploadUrl;
|
|
11
|
+
maxFileSize;
|
|
12
|
+
allowedTypes;
|
|
13
|
+
abortControllers = new Map();
|
|
3
14
|
constructor(elementOrSelector, config = {}) {
|
|
4
|
-
this.files = new Map();
|
|
5
|
-
this.abortControllers = new Map();
|
|
6
|
-
this.preventDefaults = (e) => {
|
|
7
|
-
e.preventDefault();
|
|
8
|
-
e.stopPropagation();
|
|
9
|
-
};
|
|
10
|
-
this.handleDragEnter = () => {
|
|
11
|
-
this.dropZone.classList.add('drag-over');
|
|
12
|
-
};
|
|
13
|
-
this.handleDragLeave = () => {
|
|
14
|
-
this.dropZone.classList.remove('drag-over');
|
|
15
|
-
};
|
|
16
|
-
this.handleDrop = (e) => {
|
|
17
|
-
const droppedFiles = e.dataTransfer?.files;
|
|
18
|
-
if (droppedFiles) {
|
|
19
|
-
this.handleFiles(droppedFiles);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
this.handleDropZoneClick = () => {
|
|
23
|
-
this.fileInput.click();
|
|
24
|
-
};
|
|
25
|
-
this.handleFileInputChange = (e) => {
|
|
26
|
-
const target = e.target;
|
|
27
|
-
if (target.files) {
|
|
28
|
-
this.handleFiles(target.files);
|
|
29
|
-
target.value = '';
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
this.handleUploadClick = async () => {
|
|
33
|
-
if (this.files.size === 0)
|
|
34
|
-
return;
|
|
35
|
-
this.uploadBtn.disabled = true;
|
|
36
|
-
this.uploadBtn.textContent = 'Uploading...';
|
|
37
|
-
const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
|
|
38
|
-
const results = await Promise.allSettled(uploadPromises);
|
|
39
|
-
this.uploadBtn.textContent = 'Upload Complete';
|
|
40
|
-
setTimeout(() => {
|
|
41
|
-
this.dispatchUploadCompletedEvent(results);
|
|
42
|
-
this.fileList.innerHTML = '';
|
|
43
|
-
this.files.clear();
|
|
44
|
-
this.updateUploadButton();
|
|
45
|
-
}, 1500);
|
|
46
|
-
};
|
|
47
15
|
const container = typeof elementOrSelector === 'string'
|
|
48
16
|
? document.querySelector(elementOrSelector)
|
|
49
17
|
: elementOrSelector;
|
|
@@ -89,6 +57,47 @@ class FileUploader {
|
|
|
89
57
|
this.fileInput.addEventListener('change', this.handleFileInputChange);
|
|
90
58
|
this.uploadBtn.addEventListener('click', this.handleUploadClick);
|
|
91
59
|
}
|
|
60
|
+
preventDefaults = (e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
};
|
|
64
|
+
handleDragEnter = () => {
|
|
65
|
+
this.dropZone.classList.add('drag-over');
|
|
66
|
+
};
|
|
67
|
+
handleDragLeave = () => {
|
|
68
|
+
this.dropZone.classList.remove('drag-over');
|
|
69
|
+
};
|
|
70
|
+
handleDrop = (e) => {
|
|
71
|
+
const droppedFiles = e.dataTransfer?.files;
|
|
72
|
+
if (droppedFiles) {
|
|
73
|
+
this.handleFiles(droppedFiles);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
handleDropZoneClick = () => {
|
|
77
|
+
this.fileInput.click();
|
|
78
|
+
};
|
|
79
|
+
handleFileInputChange = (e) => {
|
|
80
|
+
const target = e.target;
|
|
81
|
+
if (target.files) {
|
|
82
|
+
this.handleFiles(target.files);
|
|
83
|
+
target.value = '';
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
handleUploadClick = async () => {
|
|
87
|
+
if (this.files.size === 0)
|
|
88
|
+
return;
|
|
89
|
+
this.uploadBtn.disabled = true;
|
|
90
|
+
this.uploadBtn.textContent = 'Uploading...';
|
|
91
|
+
const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
|
|
92
|
+
const results = await Promise.allSettled(uploadPromises);
|
|
93
|
+
this.uploadBtn.textContent = 'Upload Complete';
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
this.dispatchUploadCompletedEvent(results);
|
|
96
|
+
this.fileList.innerHTML = '';
|
|
97
|
+
this.files.clear();
|
|
98
|
+
this.updateUploadButton();
|
|
99
|
+
}, 1500);
|
|
100
|
+
};
|
|
92
101
|
handleFiles(fileList) {
|
|
93
102
|
Array.from(fileList).forEach(file => {
|
|
94
103
|
const key = this.fileKey(file);
|
package/js/flyout-menu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Configuration options for the FlyoutMenu. */
|
|
1
2
|
interface FlyoutMenuOptions {
|
|
2
3
|
triggerSelector?: string;
|
|
3
4
|
menuSelector?: string;
|
|
@@ -11,6 +12,7 @@ interface FlyoutMenuOptions {
|
|
|
11
12
|
enableHeader?: boolean;
|
|
12
13
|
enableFooter?: boolean;
|
|
13
14
|
}
|
|
15
|
+
/** Off-canvas flyout navigation with nested submenu support. */
|
|
14
16
|
declare class FlyoutMenu {
|
|
15
17
|
private options;
|
|
16
18
|
private menuTrigger;
|
|
@@ -19,7 +21,7 @@ declare class FlyoutMenu {
|
|
|
19
21
|
private closeBtn;
|
|
20
22
|
private submenuToggles;
|
|
21
23
|
private menuLinks;
|
|
22
|
-
private
|
|
24
|
+
private abortController;
|
|
23
25
|
constructor(options?: FlyoutMenuOptions);
|
|
24
26
|
private init;
|
|
25
27
|
private hydrateMenu;
|
|
@@ -27,8 +29,8 @@ declare class FlyoutMenu {
|
|
|
27
29
|
private renderHeader;
|
|
28
30
|
private renderFooter;
|
|
29
31
|
private bindEvents;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
open: () => void;
|
|
33
|
+
close: () => void;
|
|
32
34
|
private handleSubmenu;
|
|
33
35
|
private handleKeydown;
|
|
34
36
|
setDirection(direction: 'left' | 'right'): void;
|
package/js/flyout-menu.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
/** Off-canvas flyout navigation with nested submenu support. */
|
|
1
2
|
class FlyoutMenu {
|
|
3
|
+
options;
|
|
4
|
+
menuTrigger;
|
|
5
|
+
flyoutMenu;
|
|
6
|
+
flyoutOverlay;
|
|
7
|
+
closeBtn = null;
|
|
8
|
+
submenuToggles = null;
|
|
9
|
+
menuLinks = null;
|
|
10
|
+
abortController = new AbortController();
|
|
2
11
|
constructor(options = {}) {
|
|
3
|
-
this.closeBtn = null;
|
|
4
|
-
this.submenuToggles = null;
|
|
5
|
-
this.menuLinks = null;
|
|
6
|
-
this.submenuHandlers = new Map();
|
|
7
12
|
this.options = {
|
|
8
13
|
triggerSelector: '.menu-trigger',
|
|
9
14
|
menuSelector: '#flyoutMenu',
|
|
@@ -21,10 +26,6 @@ class FlyoutMenu {
|
|
|
21
26
|
this.menuTrigger = document.querySelector(this.options.triggerSelector);
|
|
22
27
|
this.flyoutMenu = document.querySelector(this.options.menuSelector);
|
|
23
28
|
this.flyoutOverlay = document.querySelector(this.options.overlaySelector);
|
|
24
|
-
this.open = this.open.bind(this);
|
|
25
|
-
this.close = this.close.bind(this);
|
|
26
|
-
this.handleSubmenu = this.handleSubmenu.bind(this);
|
|
27
|
-
this.handleKeydown = this.handleKeydown.bind(this);
|
|
28
29
|
this.init();
|
|
29
30
|
}
|
|
30
31
|
init() {
|
|
@@ -54,16 +55,13 @@ class FlyoutMenu {
|
|
|
54
55
|
processListItems(ul) {
|
|
55
56
|
const items = Array.from(ul.children);
|
|
56
57
|
items.forEach((li, index) => {
|
|
57
|
-
// Check if it has a nested UL
|
|
58
58
|
const nestedUl = li.querySelector('ul');
|
|
59
59
|
if (nestedUl) {
|
|
60
60
|
li.classList.add('has-submenu');
|
|
61
61
|
nestedUl.classList.add('submenu');
|
|
62
|
-
// Get text content (excluding nested UL text)
|
|
63
62
|
const textNode = Array.from(li.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '');
|
|
64
63
|
const text = textNode?.textContent?.trim() || 'Menu Item';
|
|
65
64
|
textNode?.remove();
|
|
66
|
-
// Create Toggle Button
|
|
67
65
|
const button = document.createElement('button');
|
|
68
66
|
button.className = 'submenu-toggle';
|
|
69
67
|
button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
@@ -74,11 +72,9 @@ class FlyoutMenu {
|
|
|
74
72
|
</svg>
|
|
75
73
|
`;
|
|
76
74
|
li.insertBefore(button, nestedUl);
|
|
77
|
-
// Recursively process nested UL
|
|
78
75
|
this.processListItems(nestedUl);
|
|
79
76
|
}
|
|
80
77
|
else {
|
|
81
|
-
// Leaf node - ensure it has a link
|
|
82
78
|
const link = li.querySelector('a');
|
|
83
79
|
if (link) {
|
|
84
80
|
link.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
@@ -113,36 +109,30 @@ class FlyoutMenu {
|
|
|
113
109
|
this.flyoutMenu.append(footer);
|
|
114
110
|
}
|
|
115
111
|
bindEvents() {
|
|
116
|
-
|
|
117
|
-
this.menuTrigger?.addEventListener('click', this.open);
|
|
118
|
-
|
|
119
|
-
this.
|
|
120
|
-
this.flyoutOverlay?.addEventListener('click', this.close);
|
|
121
|
-
// Submenus
|
|
112
|
+
const sig = { signal: this.abortController.signal };
|
|
113
|
+
this.menuTrigger?.addEventListener('click', this.open, sig);
|
|
114
|
+
this.closeBtn?.addEventListener('click', this.close, sig);
|
|
115
|
+
this.flyoutOverlay?.addEventListener('click', this.close, sig);
|
|
122
116
|
this.submenuToggles?.forEach(toggle => {
|
|
123
|
-
|
|
124
|
-
this.submenuHandlers.set(toggle, handler);
|
|
125
|
-
toggle.addEventListener('click', handler);
|
|
117
|
+
toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle), sig);
|
|
126
118
|
});
|
|
127
|
-
// Close on Link Click
|
|
128
119
|
this.menuLinks?.forEach(link => {
|
|
129
|
-
link.addEventListener('click', this.close);
|
|
120
|
+
link.addEventListener('click', this.close, sig);
|
|
130
121
|
});
|
|
131
|
-
|
|
132
|
-
document.addEventListener('keydown', this.handleKeydown);
|
|
122
|
+
document.addEventListener('keydown', this.handleKeydown, sig);
|
|
133
123
|
}
|
|
134
|
-
open() {
|
|
124
|
+
open = () => {
|
|
135
125
|
this.flyoutMenu?.classList.add('is-open');
|
|
136
126
|
this.flyoutOverlay?.classList.add('is-visible');
|
|
137
127
|
document.body.style.overflow = 'hidden';
|
|
138
128
|
this.menuTrigger?.setAttribute('aria-expanded', 'true');
|
|
139
|
-
}
|
|
140
|
-
close() {
|
|
129
|
+
};
|
|
130
|
+
close = () => {
|
|
141
131
|
this.flyoutMenu?.classList.remove('is-open');
|
|
142
132
|
this.flyoutOverlay?.classList.remove('is-visible');
|
|
143
133
|
document.body.style.overflow = '';
|
|
144
134
|
this.menuTrigger?.setAttribute('aria-expanded', 'false');
|
|
145
|
-
}
|
|
135
|
+
};
|
|
146
136
|
handleSubmenu(e, toggle) {
|
|
147
137
|
e.preventDefault();
|
|
148
138
|
e.stopPropagation();
|
|
@@ -151,7 +141,6 @@ class FlyoutMenu {
|
|
|
151
141
|
const parentUl = parentLi?.parentElement;
|
|
152
142
|
if (!parentUl || !parentLi)
|
|
153
143
|
return;
|
|
154
|
-
// Close other submenus at the same level
|
|
155
144
|
const siblings = Array.from(parentUl.children);
|
|
156
145
|
siblings.forEach(sibling => {
|
|
157
146
|
if (sibling !== parentLi) {
|
|
@@ -166,11 +155,11 @@ class FlyoutMenu {
|
|
|
166
155
|
toggle.classList.toggle('active');
|
|
167
156
|
submenu?.classList.toggle('is-open');
|
|
168
157
|
}
|
|
169
|
-
handleKeydown(e) {
|
|
158
|
+
handleKeydown = (e) => {
|
|
170
159
|
if (e.key === 'Escape' && this.flyoutMenu?.classList.contains('is-open')) {
|
|
171
160
|
this.close();
|
|
172
161
|
}
|
|
173
|
-
}
|
|
162
|
+
};
|
|
174
163
|
setDirection(direction) {
|
|
175
164
|
if (!this.flyoutMenu)
|
|
176
165
|
return;
|
|
@@ -182,19 +171,7 @@ class FlyoutMenu {
|
|
|
182
171
|
this.options.direction = direction;
|
|
183
172
|
}
|
|
184
173
|
destroy() {
|
|
185
|
-
this.
|
|
186
|
-
this.closeBtn?.removeEventListener('click', this.close);
|
|
187
|
-
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
188
|
-
this.submenuToggles?.forEach(toggle => {
|
|
189
|
-
const handler = this.submenuHandlers.get(toggle);
|
|
190
|
-
if (handler)
|
|
191
|
-
toggle.removeEventListener('click', handler);
|
|
192
|
-
});
|
|
193
|
-
this.submenuHandlers.clear();
|
|
194
|
-
this.menuLinks?.forEach(link => {
|
|
195
|
-
link.removeEventListener('click', this.close);
|
|
196
|
-
});
|
|
197
|
-
document.removeEventListener('keydown', this.handleKeydown);
|
|
174
|
+
this.abortController.abort();
|
|
198
175
|
document.body.style.overflow = '';
|
|
199
176
|
}
|
|
200
177
|
}
|