@dodlhuat/basix 1.2.0 → 1.2.2
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 +266 -6
- package/css/accordion.scss +86 -87
- package/css/alert.scss +137 -137
- package/css/button.scss +48 -0
- package/css/calendar.scss +957 -0
- package/css/card.scss +65 -65
- package/css/chart.scss +270 -157
- package/css/chat-bubbles.scss +134 -68
- package/css/chips.scss +109 -19
- package/css/colors.scss +32 -32
- package/css/datepicker.scss +336 -336
- package/css/defaults.scss +90 -90
- package/css/docs.scss +529 -0
- package/css/editor.scss +36 -0
- package/css/file-uploader.scss +1 -1
- package/css/flyout-menu.scss +361 -361
- package/css/form.scss +0 -15
- package/css/gallery.scss +65 -6
- package/css/grid.scss +41 -40
- package/css/group-picker.scss +345 -0
- package/css/guitar-chords.css +250 -250
- package/css/icons.scss +330 -330
- package/css/parameters.scss +3 -3
- package/css/placeholder.scss +33 -33
- package/css/popover.scss +206 -0
- package/css/progress.scss +76 -32
- package/css/properties.scss +51 -36
- package/css/push-menu.scss +302 -174
- package/css/reset.scss +39 -39
- package/css/scrollbar.scss +62 -5
- package/css/sidebar-nav.scss +92 -0
- package/css/spinner.scss +65 -65
- package/css/stepper.scss +48 -12
- package/css/style.css +3155 -254
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.scss +51 -45
- package/css/table.scss +199 -199
- package/css/tabs.scss +154 -123
- package/css/timeline.scss +83 -38
- package/css/timepicker.scss +100 -5
- package/css/toast.scss +81 -81
- package/css/virtual-dropdown.scss +35 -29
- package/js/calendar.js +532 -0
- package/js/calendar.ts +706 -0
- package/js/chart.js +573 -257
- package/js/chart.ts +692 -0
- package/js/code-viewer.js +10 -10
- package/js/code-viewer.ts +188 -188
- package/js/datepicker.ts +627 -627
- package/js/docs-nav.js +204 -0
- package/js/dropdown.ts +179 -179
- package/js/editor.js +50 -6
- package/js/editor.ts +483 -444
- package/js/file-uploader.js +1 -0
- package/js/file-uploader.ts +1 -0
- package/js/flyout-menu.js +14 -14
- package/js/flyout-menu.ts +249 -249
- package/js/form-builder.js +106 -106
- package/js/gallery.js +14 -8
- package/js/gallery.ts +245 -236
- package/js/group-picker.js +342 -0
- package/js/group-picker.ts +447 -0
- package/js/guitar-chords.js +268 -268
- package/js/lazy-loader.js +121 -121
- package/js/modal.ts +166 -166
- package/js/popover.js +163 -0
- package/js/popover.ts +219 -0
- package/js/position.js +108 -0
- package/js/position.ts +111 -0
- package/js/push-menu.js +113 -0
- package/js/push-menu.ts +284 -145
- package/js/request.js +50 -50
- package/js/scroll.ts +47 -47
- package/js/scrollbar.js +13 -0
- package/js/scrollbar.ts +324 -307
- package/js/select.ts +216 -216
- package/js/sidebar-nav.js +41 -0
- package/js/sidebar-nav.ts +66 -0
- package/js/table.ts +452 -452
- package/js/tabs.ts +279 -279
- package/js/theme.js +17 -6
- package/js/theme.ts +234 -224
- package/js/toast.ts +137 -137
- package/js/tooltip.js +6 -60
- package/js/tooltip.ts +184 -251
- package/js/tsconfig.json +18 -18
- package/js/utils.ts +83 -83
- package/js/virtual-dropdown.js +25 -25
- package/js/virtual-dropdown.ts +365 -365
- package/package.json +37 -39
- package/js/index.js +0 -816
- package/js/index.ts +0 -987
package/js/file-uploader.js
CHANGED
|
@@ -50,6 +50,7 @@ class FileUploader {
|
|
|
50
50
|
throw new Error(`FileUploader: Element not found for selector "${elementOrSelector}"`);
|
|
51
51
|
}
|
|
52
52
|
this.container = container;
|
|
53
|
+
this.container.classList.add('file-uploader');
|
|
53
54
|
const dropZone = container.querySelector('#drop-zone');
|
|
54
55
|
const fileInput = container.querySelector('#file-input');
|
|
55
56
|
const fileList = container.querySelector('#file-list');
|
package/js/file-uploader.ts
CHANGED
|
@@ -42,6 +42,7 @@ class FileUploader {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
this.container = container;
|
|
45
|
+
this.container.classList.add('file-uploader');
|
|
45
46
|
|
|
46
47
|
const dropZone = container.querySelector<HTMLElement>('#drop-zone');
|
|
47
48
|
const fileInput = container.querySelector<HTMLInputElement>('#file-input');
|
package/js/flyout-menu.js
CHANGED
|
@@ -66,11 +66,11 @@ class FlyoutMenu {
|
|
|
66
66
|
const button = document.createElement('button');
|
|
67
67
|
button.className = 'submenu-toggle';
|
|
68
68
|
button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
69
|
-
button.innerHTML = `
|
|
70
|
-
${text}
|
|
71
|
-
<svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
72
|
-
<polyline points="6 9 12 15 18 9"></polyline>
|
|
73
|
-
</svg>
|
|
69
|
+
button.innerHTML = `
|
|
70
|
+
${text}
|
|
71
|
+
<svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
72
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
73
|
+
</svg>
|
|
74
74
|
`;
|
|
75
75
|
li.insertBefore(button, nestedUl);
|
|
76
76
|
// Recursively process nested UL
|
|
@@ -90,15 +90,15 @@ class FlyoutMenu {
|
|
|
90
90
|
return;
|
|
91
91
|
const header = document.createElement('div');
|
|
92
92
|
header.className = 'flyout-header';
|
|
93
|
-
header.innerHTML = `
|
|
94
|
-
<span class="flyout-title">${this.options.title}</span>
|
|
95
|
-
<button class="close-menu" aria-label="Close Menu">
|
|
96
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
97
|
-
stroke-linecap="round" stroke-linejoin="round">
|
|
98
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
99
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
100
|
-
</svg>
|
|
101
|
-
</button>
|
|
93
|
+
header.innerHTML = `
|
|
94
|
+
<span class="flyout-title">${this.options.title}</span>
|
|
95
|
+
<button class="close-menu" aria-label="Close Menu">
|
|
96
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
97
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
98
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
99
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
102
102
|
`;
|
|
103
103
|
this.flyoutMenu.prepend(header);
|
|
104
104
|
}
|
package/js/flyout-menu.ts
CHANGED
|
@@ -1,250 +1,250 @@
|
|
|
1
|
-
interface FlyoutMenuOptions {
|
|
2
|
-
triggerSelector?: string;
|
|
3
|
-
menuSelector?: string;
|
|
4
|
-
overlaySelector?: string;
|
|
5
|
-
closeSelector?: string;
|
|
6
|
-
submenuToggleSelector?: string;
|
|
7
|
-
linkSelector?: string;
|
|
8
|
-
direction?: 'right' | 'left';
|
|
9
|
-
title?: string;
|
|
10
|
-
footerText?: string;
|
|
11
|
-
enableHeader?: boolean;
|
|
12
|
-
enableFooter?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class FlyoutMenu {
|
|
16
|
-
private options: Required<FlyoutMenuOptions>;
|
|
17
|
-
private menuTrigger: HTMLElement | null;
|
|
18
|
-
private readonly flyoutMenu: HTMLElement | null;
|
|
19
|
-
private flyoutOverlay: HTMLElement | null;
|
|
20
|
-
private closeBtn: HTMLElement | null = null;
|
|
21
|
-
private submenuToggles: NodeListOf<HTMLElement> | null = null;
|
|
22
|
-
private menuLinks: NodeListOf<HTMLAnchorElement> | null = null;
|
|
23
|
-
|
|
24
|
-
constructor(options: FlyoutMenuOptions = {}) {
|
|
25
|
-
this.options = {
|
|
26
|
-
triggerSelector: '.menu-trigger',
|
|
27
|
-
menuSelector: '#flyoutMenu',
|
|
28
|
-
overlaySelector: '#flyoutOverlay',
|
|
29
|
-
closeSelector: '.close-menu',
|
|
30
|
-
submenuToggleSelector: '.submenu-toggle',
|
|
31
|
-
linkSelector: '.flyout-links > li > a',
|
|
32
|
-
direction: 'right',
|
|
33
|
-
title: 'Menu',
|
|
34
|
-
footerText: '© 2025 Brand Inc.',
|
|
35
|
-
enableHeader: true,
|
|
36
|
-
enableFooter: true,
|
|
37
|
-
...options
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
this.menuTrigger = document.querySelector(this.options.triggerSelector);
|
|
41
|
-
this.flyoutMenu = document.querySelector(this.options.menuSelector);
|
|
42
|
-
this.flyoutOverlay = document.querySelector(this.options.overlaySelector);
|
|
43
|
-
|
|
44
|
-
this.open = this.open.bind(this);
|
|
45
|
-
this.close = this.close.bind(this);
|
|
46
|
-
this.handleSubmenu = this.handleSubmenu.bind(this);
|
|
47
|
-
this.handleKeydown = this.handleKeydown.bind(this);
|
|
48
|
-
|
|
49
|
-
this.init();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private init(): void {
|
|
53
|
-
if (!this.flyoutMenu) {
|
|
54
|
-
throw new Error(`FlyoutMenu: Menu element not found for selector "${this.options.menuSelector}"`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
this.hydrateMenu();
|
|
58
|
-
|
|
59
|
-
if (this.options.enableHeader) this.renderHeader();
|
|
60
|
-
if (this.options.enableFooter) this.renderFooter();
|
|
61
|
-
|
|
62
|
-
this.closeBtn = document.querySelector(this.options.closeSelector);
|
|
63
|
-
this.submenuToggles = this.flyoutMenu.querySelectorAll(this.options.submenuToggleSelector);
|
|
64
|
-
this.menuLinks = this.flyoutMenu.querySelectorAll('a');
|
|
65
|
-
|
|
66
|
-
this.setDirection(this.options.direction);
|
|
67
|
-
|
|
68
|
-
this.bindEvents();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private hydrateMenu(): void {
|
|
72
|
-
if (!this.flyoutMenu) return;
|
|
73
|
-
|
|
74
|
-
const rootUl = this.flyoutMenu.querySelector('ul');
|
|
75
|
-
if (rootUl) {
|
|
76
|
-
rootUl.classList.add('flyout-links');
|
|
77
|
-
this.processListItems(rootUl);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private processListItems(ul: HTMLUListElement): void {
|
|
82
|
-
const items = Array.from(ul.children) as HTMLLIElement[];
|
|
83
|
-
items.forEach((li, index) => {
|
|
84
|
-
// Check if it has a nested UL
|
|
85
|
-
const nestedUl = li.querySelector('ul') as HTMLUListElement | null;
|
|
86
|
-
if (nestedUl) {
|
|
87
|
-
li.classList.add('has-submenu');
|
|
88
|
-
nestedUl.classList.add('submenu');
|
|
89
|
-
|
|
90
|
-
// Get text content (excluding nested UL text)
|
|
91
|
-
const textNode = Array.from(li.childNodes).find(
|
|
92
|
-
node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== ''
|
|
93
|
-
) as Text | undefined;
|
|
94
|
-
const text = textNode?.textContent?.trim() || 'Menu Item';
|
|
95
|
-
textNode?.remove();
|
|
96
|
-
|
|
97
|
-
// Create Toggle Button
|
|
98
|
-
const button = document.createElement('button');
|
|
99
|
-
button.className = 'submenu-toggle';
|
|
100
|
-
button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
101
|
-
button.innerHTML = `
|
|
102
|
-
${text}
|
|
103
|
-
<svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
104
|
-
<polyline points="6 9 12 15 18 9"></polyline>
|
|
105
|
-
</svg>
|
|
106
|
-
`;
|
|
107
|
-
|
|
108
|
-
li.insertBefore(button, nestedUl);
|
|
109
|
-
|
|
110
|
-
// Recursively process nested UL
|
|
111
|
-
this.processListItems(nestedUl);
|
|
112
|
-
} else {
|
|
113
|
-
// Leaf node - ensure it has a link
|
|
114
|
-
const link = li.querySelector('a');
|
|
115
|
-
if (link) {
|
|
116
|
-
link.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private renderHeader(): void {
|
|
123
|
-
if (!this.flyoutMenu) return;
|
|
124
|
-
|
|
125
|
-
const header = document.createElement('div');
|
|
126
|
-
header.className = 'flyout-header';
|
|
127
|
-
header.innerHTML = `
|
|
128
|
-
<span class="flyout-title">${this.options.title}</span>
|
|
129
|
-
<button class="close-menu" aria-label="Close Menu">
|
|
130
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
131
|
-
stroke-linecap="round" stroke-linejoin="round">
|
|
132
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
133
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
134
|
-
</svg>
|
|
135
|
-
</button>
|
|
136
|
-
`;
|
|
137
|
-
this.flyoutMenu.prepend(header);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private renderFooter(): void {
|
|
141
|
-
if (!this.flyoutMenu) return;
|
|
142
|
-
|
|
143
|
-
const footer = document.createElement('div');
|
|
144
|
-
footer.className = 'flyout-footer';
|
|
145
|
-
footer.style.setProperty('--delay', '0.6s');
|
|
146
|
-
footer.innerHTML = `<p>${this.options.footerText}</p>`;
|
|
147
|
-
this.flyoutMenu.append(footer);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private bindEvents(): void {
|
|
151
|
-
// Open
|
|
152
|
-
this.menuTrigger?.addEventListener('click', this.open);
|
|
153
|
-
|
|
154
|
-
// Close
|
|
155
|
-
this.closeBtn?.addEventListener('click', this.close);
|
|
156
|
-
this.flyoutOverlay?.addEventListener('click', this.close);
|
|
157
|
-
|
|
158
|
-
// Submenus
|
|
159
|
-
this.submenuToggles?.forEach(toggle => {
|
|
160
|
-
toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle));
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Close on Link Click
|
|
164
|
-
this.menuLinks?.forEach(link => {
|
|
165
|
-
link.addEventListener('click', this.close);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Keyboard navigation
|
|
169
|
-
document.addEventListener('keydown', this.handleKeydown);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private open(): void {
|
|
173
|
-
this.flyoutMenu?.classList.add('is-open');
|
|
174
|
-
this.flyoutOverlay?.classList.add('is-visible');
|
|
175
|
-
document.body.style.overflow = 'hidden';
|
|
176
|
-
this.menuTrigger?.setAttribute('aria-expanded', 'true');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private close(): void {
|
|
180
|
-
this.flyoutMenu?.classList.remove('is-open');
|
|
181
|
-
this.flyoutOverlay?.classList.remove('is-visible');
|
|
182
|
-
document.body.style.overflow = '';
|
|
183
|
-
this.menuTrigger?.setAttribute('aria-expanded', 'false');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private handleSubmenu(e: Event, toggle: HTMLElement): void {
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
e.stopPropagation();
|
|
189
|
-
|
|
190
|
-
const submenu = toggle.nextElementSibling as HTMLElement | null;
|
|
191
|
-
const parentLi = toggle.parentElement as HTMLLIElement | null;
|
|
192
|
-
const parentUl = parentLi?.parentElement as HTMLUListElement | null;
|
|
193
|
-
|
|
194
|
-
if (!parentUl || !parentLi) return;
|
|
195
|
-
|
|
196
|
-
// Close other submenus at the same level
|
|
197
|
-
const siblings = Array.from(parentUl.children) as HTMLLIElement[];
|
|
198
|
-
siblings.forEach(sibling => {
|
|
199
|
-
if (sibling !== parentLi) {
|
|
200
|
-
const siblingSubmenu = sibling.querySelector('.submenu');
|
|
201
|
-
const siblingToggle = sibling.querySelector('.submenu-toggle');
|
|
202
|
-
if (siblingSubmenu?.classList.contains('is-open')) {
|
|
203
|
-
siblingSubmenu.classList.remove('is-open');
|
|
204
|
-
siblingToggle?.classList.remove('active');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
toggle.classList.toggle('active');
|
|
210
|
-
submenu?.classList.toggle('is-open');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private handleKeydown(e: KeyboardEvent): void {
|
|
214
|
-
if (e.key === 'Escape' && this.flyoutMenu?.classList.contains('is-open')) {
|
|
215
|
-
this.close();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
public setDirection(direction: 'left' | 'right'): void {
|
|
220
|
-
if (!this.flyoutMenu) return;
|
|
221
|
-
|
|
222
|
-
const validDirections: Array<'left' | 'right'> = ['left', 'right'];
|
|
223
|
-
if (!validDirections.includes(direction)) return;
|
|
224
|
-
|
|
225
|
-
this.flyoutMenu.classList.remove('flyout-from-right', 'flyout-from-left');
|
|
226
|
-
this.flyoutMenu.classList.add(`flyout-from-${direction}`);
|
|
227
|
-
|
|
228
|
-
this.options.direction = direction;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
public destroy(): void {
|
|
232
|
-
this.menuTrigger?.removeEventListener('click', this.open);
|
|
233
|
-
this.closeBtn?.removeEventListener('click', this.close);
|
|
234
|
-
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
235
|
-
|
|
236
|
-
this.submenuToggles?.forEach(toggle => {
|
|
237
|
-
toggle.removeEventListener('click', (e) => this.handleSubmenu(e, toggle));
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
this.menuLinks?.forEach(link => {
|
|
241
|
-
link.removeEventListener('click', this.close);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
document.removeEventListener('keydown', this.handleKeydown);
|
|
245
|
-
|
|
246
|
-
document.body.style.overflow = '';
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
1
|
+
interface FlyoutMenuOptions {
|
|
2
|
+
triggerSelector?: string;
|
|
3
|
+
menuSelector?: string;
|
|
4
|
+
overlaySelector?: string;
|
|
5
|
+
closeSelector?: string;
|
|
6
|
+
submenuToggleSelector?: string;
|
|
7
|
+
linkSelector?: string;
|
|
8
|
+
direction?: 'right' | 'left';
|
|
9
|
+
title?: string;
|
|
10
|
+
footerText?: string;
|
|
11
|
+
enableHeader?: boolean;
|
|
12
|
+
enableFooter?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class FlyoutMenu {
|
|
16
|
+
private options: Required<FlyoutMenuOptions>;
|
|
17
|
+
private menuTrigger: HTMLElement | null;
|
|
18
|
+
private readonly flyoutMenu: HTMLElement | null;
|
|
19
|
+
private flyoutOverlay: HTMLElement | null;
|
|
20
|
+
private closeBtn: HTMLElement | null = null;
|
|
21
|
+
private submenuToggles: NodeListOf<HTMLElement> | null = null;
|
|
22
|
+
private menuLinks: NodeListOf<HTMLAnchorElement> | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(options: FlyoutMenuOptions = {}) {
|
|
25
|
+
this.options = {
|
|
26
|
+
triggerSelector: '.menu-trigger',
|
|
27
|
+
menuSelector: '#flyoutMenu',
|
|
28
|
+
overlaySelector: '#flyoutOverlay',
|
|
29
|
+
closeSelector: '.close-menu',
|
|
30
|
+
submenuToggleSelector: '.submenu-toggle',
|
|
31
|
+
linkSelector: '.flyout-links > li > a',
|
|
32
|
+
direction: 'right',
|
|
33
|
+
title: 'Menu',
|
|
34
|
+
footerText: '© 2025 Brand Inc.',
|
|
35
|
+
enableHeader: true,
|
|
36
|
+
enableFooter: true,
|
|
37
|
+
...options
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.menuTrigger = document.querySelector(this.options.triggerSelector);
|
|
41
|
+
this.flyoutMenu = document.querySelector(this.options.menuSelector);
|
|
42
|
+
this.flyoutOverlay = document.querySelector(this.options.overlaySelector);
|
|
43
|
+
|
|
44
|
+
this.open = this.open.bind(this);
|
|
45
|
+
this.close = this.close.bind(this);
|
|
46
|
+
this.handleSubmenu = this.handleSubmenu.bind(this);
|
|
47
|
+
this.handleKeydown = this.handleKeydown.bind(this);
|
|
48
|
+
|
|
49
|
+
this.init();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private init(): void {
|
|
53
|
+
if (!this.flyoutMenu) {
|
|
54
|
+
throw new Error(`FlyoutMenu: Menu element not found for selector "${this.options.menuSelector}"`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.hydrateMenu();
|
|
58
|
+
|
|
59
|
+
if (this.options.enableHeader) this.renderHeader();
|
|
60
|
+
if (this.options.enableFooter) this.renderFooter();
|
|
61
|
+
|
|
62
|
+
this.closeBtn = document.querySelector(this.options.closeSelector);
|
|
63
|
+
this.submenuToggles = this.flyoutMenu.querySelectorAll(this.options.submenuToggleSelector);
|
|
64
|
+
this.menuLinks = this.flyoutMenu.querySelectorAll('a');
|
|
65
|
+
|
|
66
|
+
this.setDirection(this.options.direction);
|
|
67
|
+
|
|
68
|
+
this.bindEvents();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private hydrateMenu(): void {
|
|
72
|
+
if (!this.flyoutMenu) return;
|
|
73
|
+
|
|
74
|
+
const rootUl = this.flyoutMenu.querySelector('ul');
|
|
75
|
+
if (rootUl) {
|
|
76
|
+
rootUl.classList.add('flyout-links');
|
|
77
|
+
this.processListItems(rootUl);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private processListItems(ul: HTMLUListElement): void {
|
|
82
|
+
const items = Array.from(ul.children) as HTMLLIElement[];
|
|
83
|
+
items.forEach((li, index) => {
|
|
84
|
+
// Check if it has a nested UL
|
|
85
|
+
const nestedUl = li.querySelector('ul') as HTMLUListElement | null;
|
|
86
|
+
if (nestedUl) {
|
|
87
|
+
li.classList.add('has-submenu');
|
|
88
|
+
nestedUl.classList.add('submenu');
|
|
89
|
+
|
|
90
|
+
// Get text content (excluding nested UL text)
|
|
91
|
+
const textNode = Array.from(li.childNodes).find(
|
|
92
|
+
node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== ''
|
|
93
|
+
) as Text | undefined;
|
|
94
|
+
const text = textNode?.textContent?.trim() || 'Menu Item';
|
|
95
|
+
textNode?.remove();
|
|
96
|
+
|
|
97
|
+
// Create Toggle Button
|
|
98
|
+
const button = document.createElement('button');
|
|
99
|
+
button.className = 'submenu-toggle';
|
|
100
|
+
button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
101
|
+
button.innerHTML = `
|
|
102
|
+
${text}
|
|
103
|
+
<svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
104
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
105
|
+
</svg>
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
li.insertBefore(button, nestedUl);
|
|
109
|
+
|
|
110
|
+
// Recursively process nested UL
|
|
111
|
+
this.processListItems(nestedUl);
|
|
112
|
+
} else {
|
|
113
|
+
// Leaf node - ensure it has a link
|
|
114
|
+
const link = li.querySelector('a');
|
|
115
|
+
if (link) {
|
|
116
|
+
link.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private renderHeader(): void {
|
|
123
|
+
if (!this.flyoutMenu) return;
|
|
124
|
+
|
|
125
|
+
const header = document.createElement('div');
|
|
126
|
+
header.className = 'flyout-header';
|
|
127
|
+
header.innerHTML = `
|
|
128
|
+
<span class="flyout-title">${this.options.title}</span>
|
|
129
|
+
<button class="close-menu" aria-label="Close Menu">
|
|
130
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
131
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
132
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
133
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
`;
|
|
137
|
+
this.flyoutMenu.prepend(header);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private renderFooter(): void {
|
|
141
|
+
if (!this.flyoutMenu) return;
|
|
142
|
+
|
|
143
|
+
const footer = document.createElement('div');
|
|
144
|
+
footer.className = 'flyout-footer';
|
|
145
|
+
footer.style.setProperty('--delay', '0.6s');
|
|
146
|
+
footer.innerHTML = `<p>${this.options.footerText}</p>`;
|
|
147
|
+
this.flyoutMenu.append(footer);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private bindEvents(): void {
|
|
151
|
+
// Open
|
|
152
|
+
this.menuTrigger?.addEventListener('click', this.open);
|
|
153
|
+
|
|
154
|
+
// Close
|
|
155
|
+
this.closeBtn?.addEventListener('click', this.close);
|
|
156
|
+
this.flyoutOverlay?.addEventListener('click', this.close);
|
|
157
|
+
|
|
158
|
+
// Submenus
|
|
159
|
+
this.submenuToggles?.forEach(toggle => {
|
|
160
|
+
toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Close on Link Click
|
|
164
|
+
this.menuLinks?.forEach(link => {
|
|
165
|
+
link.addEventListener('click', this.close);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Keyboard navigation
|
|
169
|
+
document.addEventListener('keydown', this.handleKeydown);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private open(): void {
|
|
173
|
+
this.flyoutMenu?.classList.add('is-open');
|
|
174
|
+
this.flyoutOverlay?.classList.add('is-visible');
|
|
175
|
+
document.body.style.overflow = 'hidden';
|
|
176
|
+
this.menuTrigger?.setAttribute('aria-expanded', 'true');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private close(): void {
|
|
180
|
+
this.flyoutMenu?.classList.remove('is-open');
|
|
181
|
+
this.flyoutOverlay?.classList.remove('is-visible');
|
|
182
|
+
document.body.style.overflow = '';
|
|
183
|
+
this.menuTrigger?.setAttribute('aria-expanded', 'false');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleSubmenu(e: Event, toggle: HTMLElement): void {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
|
|
190
|
+
const submenu = toggle.nextElementSibling as HTMLElement | null;
|
|
191
|
+
const parentLi = toggle.parentElement as HTMLLIElement | null;
|
|
192
|
+
const parentUl = parentLi?.parentElement as HTMLUListElement | null;
|
|
193
|
+
|
|
194
|
+
if (!parentUl || !parentLi) return;
|
|
195
|
+
|
|
196
|
+
// Close other submenus at the same level
|
|
197
|
+
const siblings = Array.from(parentUl.children) as HTMLLIElement[];
|
|
198
|
+
siblings.forEach(sibling => {
|
|
199
|
+
if (sibling !== parentLi) {
|
|
200
|
+
const siblingSubmenu = sibling.querySelector('.submenu');
|
|
201
|
+
const siblingToggle = sibling.querySelector('.submenu-toggle');
|
|
202
|
+
if (siblingSubmenu?.classList.contains('is-open')) {
|
|
203
|
+
siblingSubmenu.classList.remove('is-open');
|
|
204
|
+
siblingToggle?.classList.remove('active');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
toggle.classList.toggle('active');
|
|
210
|
+
submenu?.classList.toggle('is-open');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private handleKeydown(e: KeyboardEvent): void {
|
|
214
|
+
if (e.key === 'Escape' && this.flyoutMenu?.classList.contains('is-open')) {
|
|
215
|
+
this.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public setDirection(direction: 'left' | 'right'): void {
|
|
220
|
+
if (!this.flyoutMenu) return;
|
|
221
|
+
|
|
222
|
+
const validDirections: Array<'left' | 'right'> = ['left', 'right'];
|
|
223
|
+
if (!validDirections.includes(direction)) return;
|
|
224
|
+
|
|
225
|
+
this.flyoutMenu.classList.remove('flyout-from-right', 'flyout-from-left');
|
|
226
|
+
this.flyoutMenu.classList.add(`flyout-from-${direction}`);
|
|
227
|
+
|
|
228
|
+
this.options.direction = direction;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public destroy(): void {
|
|
232
|
+
this.menuTrigger?.removeEventListener('click', this.open);
|
|
233
|
+
this.closeBtn?.removeEventListener('click', this.close);
|
|
234
|
+
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
235
|
+
|
|
236
|
+
this.submenuToggles?.forEach(toggle => {
|
|
237
|
+
toggle.removeEventListener('click', (e) => this.handleSubmenu(e, toggle));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.menuLinks?.forEach(link => {
|
|
241
|
+
link.removeEventListener('click', this.close);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
document.removeEventListener('keydown', this.handleKeydown);
|
|
245
|
+
|
|
246
|
+
document.body.style.overflow = '';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
250
|
export { FlyoutMenu, type FlyoutMenuOptions };
|