vanduo-framework 1.1.8
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/LICENSE +35 -0
- package/README.md +205 -0
- package/css/components/alerts.css +224 -0
- package/css/components/avatar.css +275 -0
- package/css/components/badges.css +230 -0
- package/css/components/breadcrumbs.css +146 -0
- package/css/components/button-group.css +82 -0
- package/css/components/buttons.css +530 -0
- package/css/components/cards.css +304 -0
- package/css/components/chips.css +259 -0
- package/css/components/code-snippet.css +555 -0
- package/css/components/collapsible.css +267 -0
- package/css/components/collections.css +253 -0
- package/css/components/doc-search.css +464 -0
- package/css/components/doc-tabs.css +38 -0
- package/css/components/draggable.css +317 -0
- package/css/components/dropdown.css +266 -0
- package/css/components/footer.css +375 -0
- package/css/components/forms.css +1774 -0
- package/css/components/image-box.css +279 -0
- package/css/components/modals.css +285 -0
- package/css/components/navbar.css +530 -0
- package/css/components/pagination.css +186 -0
- package/css/components/preloader.css +340 -0
- package/css/components/progress.css +107 -0
- package/css/components/sidenav.css +301 -0
- package/css/components/skeleton.css +241 -0
- package/css/components/spinner.css +144 -0
- package/css/components/tabs.css +327 -0
- package/css/components/theme-customizer.css +835 -0
- package/css/components/toast.css +357 -0
- package/css/components/tooltips.css +270 -0
- package/css/core/colors.css +1017 -0
- package/css/core/fonts.css +266 -0
- package/css/core/grid.css +1699 -0
- package/css/core/helpers.css +2202 -0
- package/css/core/reset.css +128 -0
- package/css/core/tokens.css +213 -0
- package/css/core/typography.css +405 -0
- package/css/core/vd-aliases.css +47 -0
- package/css/effects/parallax.css +113 -0
- package/css/icons/icons-all.css +23 -0
- package/css/icons/icons.css +25 -0
- package/css/utilities/media.css +167 -0
- package/css/utilities/print.css +111 -0
- package/css/utilities/shadow.css +243 -0
- package/css/utilities/table.css +381 -0
- package/css/utilities/transforms.css +71 -0
- package/css/utilities/transitions.css +87 -0
- package/css/vanduo.css +80 -0
- package/dist/build-info.json +6 -0
- package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-bold.woff2 +0 -0
- package/dist/fonts/inter/inter-medium.woff2 +0 -0
- package/dist/fonts/inter/inter-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-semibold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
- package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
- package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/dist/icons/phosphor/LICENSE +21 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/dist/icons/phosphor/bold/style.css +4627 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/dist/icons/phosphor/duotone/style.css +12115 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/dist/icons/phosphor/fill/style.css +4627 -0
- package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/dist/icons/phosphor/light/style.css +4627 -0
- package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/dist/icons/phosphor/regular/style.css +4627 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/dist/icons/phosphor/thin/style.css +4627 -0
- package/dist/vanduo.cjs.js +5569 -0
- package/dist/vanduo.cjs.js.map +7 -0
- package/dist/vanduo.cjs.min.js +48 -0
- package/dist/vanduo.cjs.min.js.map +7 -0
- package/dist/vanduo.css +60666 -0
- package/dist/vanduo.css.map +1 -0
- package/dist/vanduo.esm.js +5548 -0
- package/dist/vanduo.esm.js.map +7 -0
- package/dist/vanduo.esm.min.js +48 -0
- package/dist/vanduo.esm.min.js.map +7 -0
- package/dist/vanduo.js +5545 -0
- package/dist/vanduo.js.map +7 -0
- package/dist/vanduo.min.css +2 -0
- package/dist/vanduo.min.css.map +1 -0
- package/dist/vanduo.min.js +48 -0
- package/dist/vanduo.min.js.map +7 -0
- package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/fonts/inter/inter-bold.woff2 +0 -0
- package/fonts/inter/inter-medium.woff2 +0 -0
- package/fonts/inter/inter-regular.woff2 +0 -0
- package/fonts/inter/inter-semibold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/fonts/rubik/rubik-bold.woff2 +0 -0
- package/fonts/rubik/rubik-medium.woff2 +0 -0
- package/fonts/rubik/rubik-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/icons/phosphor/bold/style.css +4627 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/icons/phosphor/duotone/style.css +12115 -0
- package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/icons/phosphor/fill/style.css +4627 -0
- package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/icons/phosphor/light/style.css +4627 -0
- package/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/icons/phosphor/regular/Phosphor.woff +0 -0
- package/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/icons/phosphor/regular/style.css +4627 -0
- package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/icons/phosphor/thin/style.css +4627 -0
- package/js/components/code-snippet.js +639 -0
- package/js/components/collapsible.js +226 -0
- package/js/components/doc-search.js +936 -0
- package/js/components/draggable.js +725 -0
- package/js/components/dropdown.js +362 -0
- package/js/components/font-switcher.js +253 -0
- package/js/components/grid.js +279 -0
- package/js/components/image-box.js +372 -0
- package/js/components/modals.js +367 -0
- package/js/components/navbar.js +264 -0
- package/js/components/pagination.js +286 -0
- package/js/components/parallax.js +216 -0
- package/js/components/preloader.js +183 -0
- package/js/components/select.js +444 -0
- package/js/components/sidenav.js +303 -0
- package/js/components/tabs.js +303 -0
- package/js/components/theme-customizer.js +784 -0
- package/js/components/theme-switcher.js +183 -0
- package/js/components/toast.js +343 -0
- package/js/components/tooltips.js +306 -0
- package/js/index.js +52 -0
- package/js/utils/helpers.js +306 -0
- package/js/utils/lifecycle.js +135 -0
- package/js/vanduo.js +120 -0
- package/package.json +78 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Dropdown Component
|
|
3
|
+
* JavaScript functionality for dropdown menus
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dropdown Component
|
|
11
|
+
*/
|
|
12
|
+
const Dropdown = {
|
|
13
|
+
// Store initialized dropdowns and their cleanup functions
|
|
14
|
+
instances: new Map(),
|
|
15
|
+
// Typeahead state
|
|
16
|
+
_typeaheadBuffer: '',
|
|
17
|
+
_typeaheadTimer: null,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize dropdown components
|
|
21
|
+
*/
|
|
22
|
+
init: function() {
|
|
23
|
+
const dropdowns = document.querySelectorAll('.vd-dropdown');
|
|
24
|
+
|
|
25
|
+
dropdowns.forEach(dropdown => {
|
|
26
|
+
if (this.instances.has(dropdown)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.initDropdown(dropdown);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize a single dropdown
|
|
35
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
36
|
+
*/
|
|
37
|
+
initDropdown: function(dropdown) {
|
|
38
|
+
const toggle = dropdown.querySelector('.vd-dropdown-toggle');
|
|
39
|
+
const menu = dropdown.querySelector('.vd-dropdown-menu');
|
|
40
|
+
|
|
41
|
+
if (!toggle || !menu) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cleanupFunctions = [];
|
|
46
|
+
|
|
47
|
+
// Set ARIA attributes
|
|
48
|
+
toggle.setAttribute('aria-haspopup', 'true');
|
|
49
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
50
|
+
menu.setAttribute('role', 'menu');
|
|
51
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
52
|
+
|
|
53
|
+
// Toggle on click
|
|
54
|
+
const toggleClickHandler = (e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
this.toggleDropdown(dropdown, toggle, menu);
|
|
58
|
+
};
|
|
59
|
+
toggle.addEventListener('click', toggleClickHandler);
|
|
60
|
+
cleanupFunctions.push(() => toggle.removeEventListener('click', toggleClickHandler));
|
|
61
|
+
|
|
62
|
+
// Close on outside click
|
|
63
|
+
const documentClickHandler = (e) => {
|
|
64
|
+
if (!dropdown.contains(e.target) && menu.classList.contains('is-open')) {
|
|
65
|
+
this.closeDropdown(dropdown, toggle, menu);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
document.addEventListener('click', documentClickHandler);
|
|
69
|
+
cleanupFunctions.push(() => document.removeEventListener('click', documentClickHandler));
|
|
70
|
+
|
|
71
|
+
// Keyboard navigation
|
|
72
|
+
const keydownHandler = (e) => {
|
|
73
|
+
this.handleKeydown(e, dropdown, toggle, menu);
|
|
74
|
+
};
|
|
75
|
+
toggle.addEventListener('keydown', keydownHandler);
|
|
76
|
+
cleanupFunctions.push(() => toggle.removeEventListener('keydown', keydownHandler));
|
|
77
|
+
|
|
78
|
+
// Handle item clicks
|
|
79
|
+
const items = menu.querySelectorAll('.vd-dropdown-item:not(.disabled):not(.is-disabled)');
|
|
80
|
+
items.forEach(item => {
|
|
81
|
+
const itemClickHandler = (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
this.selectItem(item, dropdown, toggle, menu);
|
|
84
|
+
};
|
|
85
|
+
item.addEventListener('click', itemClickHandler);
|
|
86
|
+
cleanupFunctions.push(() => item.removeEventListener('click', itemClickHandler));
|
|
87
|
+
|
|
88
|
+
const itemKeydownHandler = (e) => {
|
|
89
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
this.selectItem(item, dropdown, toggle, menu);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
item.addEventListener('keydown', itemKeydownHandler);
|
|
95
|
+
cleanupFunctions.push(() => item.removeEventListener('keydown', itemKeydownHandler));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Toggle dropdown
|
|
103
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
104
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
105
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
106
|
+
*/
|
|
107
|
+
toggleDropdown: function(dropdown, toggle, menu) {
|
|
108
|
+
const isOpen = menu.classList.contains('is-open');
|
|
109
|
+
|
|
110
|
+
if (isOpen) {
|
|
111
|
+
this.closeDropdown(dropdown, toggle, menu);
|
|
112
|
+
} else {
|
|
113
|
+
this.openDropdown(dropdown, toggle, menu);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Open dropdown
|
|
119
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
120
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
121
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
122
|
+
*/
|
|
123
|
+
openDropdown: function(dropdown, toggle, menu) {
|
|
124
|
+
// Close other open dropdowns
|
|
125
|
+
const otherOpen = document.querySelectorAll('.vd-dropdown-menu.is-open');
|
|
126
|
+
otherOpen.forEach(otherMenu => {
|
|
127
|
+
if (otherMenu !== menu) {
|
|
128
|
+
const otherDropdown = otherMenu.closest('.vd-dropdown');
|
|
129
|
+
const otherToggle = otherDropdown.querySelector('.vd-dropdown-toggle');
|
|
130
|
+
this.closeDropdown(otherDropdown, otherToggle, otherMenu);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
dropdown.classList.add('is-open');
|
|
135
|
+
menu.classList.add('is-open');
|
|
136
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
137
|
+
menu.setAttribute('aria-hidden', 'false');
|
|
138
|
+
|
|
139
|
+
// Position menu
|
|
140
|
+
this.positionMenu(dropdown, menu);
|
|
141
|
+
|
|
142
|
+
// Focus first item
|
|
143
|
+
const firstItem = menu.querySelector('.vd-dropdown-item:not(.disabled):not(.is-disabled)');
|
|
144
|
+
if (firstItem) {
|
|
145
|
+
setTimeout(() => firstItem.focus(), 0);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Close dropdown
|
|
151
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
152
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
153
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
154
|
+
*/
|
|
155
|
+
closeDropdown: function(dropdown, toggle, menu) {
|
|
156
|
+
dropdown.classList.remove('is-open');
|
|
157
|
+
menu.classList.remove('is-open');
|
|
158
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
159
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
160
|
+
|
|
161
|
+
// Return focus to toggle
|
|
162
|
+
toggle.focus();
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Position dropdown menu
|
|
167
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
168
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
169
|
+
*/
|
|
170
|
+
positionMenu: function(dropdown, menu) {
|
|
171
|
+
const rect = dropdown.getBoundingClientRect();
|
|
172
|
+
const menuRect = menu.getBoundingClientRect();
|
|
173
|
+
const viewportWidth = window.innerWidth;
|
|
174
|
+
const viewportHeight = window.innerHeight;
|
|
175
|
+
const padding = 8;
|
|
176
|
+
|
|
177
|
+
// Check if menu overflows right
|
|
178
|
+
if (rect.left + menuRect.width > viewportWidth - padding) {
|
|
179
|
+
menu.classList.add('vd-dropdown-menu-end');
|
|
180
|
+
menu.classList.remove('vd-dropdown-menu-start');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if menu overflows bottom (for top positioning)
|
|
184
|
+
if (menu.classList.contains('dropdown-menu-top')) {
|
|
185
|
+
if (rect.top - menuRect.height < padding) {
|
|
186
|
+
menu.classList.remove('vd-dropdown-menu-top');
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
if (rect.bottom + menuRect.height > viewportHeight - padding) {
|
|
190
|
+
menu.classList.add('vd-dropdown-menu-top');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle keyboard navigation
|
|
197
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
198
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
199
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
200
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
201
|
+
*/
|
|
202
|
+
handleKeydown: function(e, dropdown, toggle, menu) {
|
|
203
|
+
const isOpen = menu.classList.contains('is-open');
|
|
204
|
+
const items = Array.from(menu.querySelectorAll('.vd-dropdown-item:not(.disabled):not(.is-disabled)'));
|
|
205
|
+
const currentIndex = items.findIndex(item => item === document.activeElement);
|
|
206
|
+
|
|
207
|
+
switch (e.key) {
|
|
208
|
+
case 'Enter':
|
|
209
|
+
case ' ':
|
|
210
|
+
case 'ArrowDown':
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
if (!isOpen) {
|
|
213
|
+
this.openDropdown(dropdown, toggle, menu);
|
|
214
|
+
} else if (e.key === 'ArrowDown') {
|
|
215
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
216
|
+
items[nextIndex].focus();
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'ArrowUp':
|
|
221
|
+
if (isOpen) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
224
|
+
items[prevIndex].focus();
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'Escape':
|
|
229
|
+
if (isOpen) {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
this.closeDropdown(dropdown, toggle, menu);
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case 'Home':
|
|
236
|
+
if (isOpen) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
items[0].focus();
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'End':
|
|
243
|
+
if (isOpen) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
items[items.length - 1].focus();
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
// Typeahead: jump to matching item when typing printable characters
|
|
251
|
+
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
252
|
+
clearTimeout(this._typeaheadTimer);
|
|
253
|
+
this._typeaheadBuffer += e.key.toLowerCase();
|
|
254
|
+
|
|
255
|
+
const match = items.find(item =>
|
|
256
|
+
item.textContent.trim().toLowerCase().startsWith(this._typeaheadBuffer)
|
|
257
|
+
);
|
|
258
|
+
if (match) {
|
|
259
|
+
match.focus();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this._typeaheadTimer = setTimeout(() => {
|
|
263
|
+
this._typeaheadBuffer = '';
|
|
264
|
+
}, 500);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Select dropdown item
|
|
272
|
+
* @param {HTMLElement} item - Dropdown item
|
|
273
|
+
* @param {HTMLElement} dropdown - Dropdown container
|
|
274
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
275
|
+
* @param {HTMLElement} menu - Dropdown menu
|
|
276
|
+
*/
|
|
277
|
+
selectItem: function(item, dropdown, toggle, menu) {
|
|
278
|
+
// Remove active from all items
|
|
279
|
+
menu.querySelectorAll('.vd-dropdown-item').forEach(i => {
|
|
280
|
+
i.classList.remove('active', 'is-active');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Add active to selected item
|
|
284
|
+
item.classList.add('active', 'is-active');
|
|
285
|
+
|
|
286
|
+
// Update toggle text if it's a button
|
|
287
|
+
if (toggle.tagName === 'BUTTON' || toggle.classList.contains('btn')) {
|
|
288
|
+
toggle.textContent = item.textContent.trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Close dropdown
|
|
292
|
+
this.closeDropdown(dropdown, toggle, menu);
|
|
293
|
+
|
|
294
|
+
// Dispatch event
|
|
295
|
+
item.dispatchEvent(new CustomEvent('dropdown:select', {
|
|
296
|
+
bubbles: true,
|
|
297
|
+
detail: { item, value: item.dataset.value || item.textContent }
|
|
298
|
+
}));
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Open dropdown programmatically
|
|
303
|
+
* @param {HTMLElement|string} dropdown - Dropdown container or selector
|
|
304
|
+
*/
|
|
305
|
+
open: function(dropdown) {
|
|
306
|
+
const el = typeof dropdown === 'string' ? document.querySelector(dropdown) : dropdown;
|
|
307
|
+
if (el) {
|
|
308
|
+
const toggle = el.querySelector('.vd-dropdown-toggle');
|
|
309
|
+
const menu = el.querySelector('.vd-dropdown-menu');
|
|
310
|
+
if (toggle && menu) {
|
|
311
|
+
this.openDropdown(el, toggle, menu);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Close dropdown programmatically
|
|
318
|
+
* @param {HTMLElement|string} dropdown - Dropdown container or selector
|
|
319
|
+
*/
|
|
320
|
+
close: function(dropdown) {
|
|
321
|
+
const el = typeof dropdown === 'string' ? document.querySelector(dropdown) : dropdown;
|
|
322
|
+
if (el) {
|
|
323
|
+
const toggle = el.querySelector('.vd-dropdown-toggle');
|
|
324
|
+
const menu = el.querySelector('.vd-dropdown-menu');
|
|
325
|
+
if (toggle && menu) {
|
|
326
|
+
this.closeDropdown(el, toggle, menu);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Destroy a dropdown instance and clean up event listeners
|
|
333
|
+
* @param {HTMLElement} dropdown - Dropdown element
|
|
334
|
+
*/
|
|
335
|
+
destroy: function(dropdown) {
|
|
336
|
+
const instance = this.instances.get(dropdown);
|
|
337
|
+
if (!instance) return;
|
|
338
|
+
|
|
339
|
+
instance.cleanup.forEach(fn => fn());
|
|
340
|
+
this.instances.delete(dropdown);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Destroy all dropdown instances
|
|
345
|
+
*/
|
|
346
|
+
destroyAll: function() {
|
|
347
|
+
this.instances.forEach((instance, dropdown) => {
|
|
348
|
+
this.destroy(dropdown);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// Register with Vanduo framework if available
|
|
354
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
355
|
+
window.Vanduo.register('dropdown', Dropdown);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Expose globally
|
|
359
|
+
window.VanduoDropdown = Dropdown;
|
|
360
|
+
|
|
361
|
+
})();
|
|
362
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Font Switcher
|
|
3
|
+
* Handles font selection and persistence for previewing different typefaces
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const FontSwitcher = {
|
|
10
|
+
STORAGE_KEY: 'vanduo-font-preference',
|
|
11
|
+
isInitialized: false,
|
|
12
|
+
|
|
13
|
+
// Available fonts configuration
|
|
14
|
+
fonts: {
|
|
15
|
+
'system': {
|
|
16
|
+
name: 'System Default',
|
|
17
|
+
family: null // Uses CSS default
|
|
18
|
+
},
|
|
19
|
+
'inter': {
|
|
20
|
+
name: 'Inter',
|
|
21
|
+
family: "'Inter', sans-serif"
|
|
22
|
+
},
|
|
23
|
+
'source-sans': {
|
|
24
|
+
name: 'Source Sans 3',
|
|
25
|
+
family: "'Source Sans 3', sans-serif"
|
|
26
|
+
},
|
|
27
|
+
'fira-sans': {
|
|
28
|
+
name: 'Fira Sans',
|
|
29
|
+
family: "'Fira Sans', sans-serif"
|
|
30
|
+
},
|
|
31
|
+
'ibm-plex': {
|
|
32
|
+
name: 'IBM Plex Sans',
|
|
33
|
+
family: "'IBM Plex Sans', sans-serif"
|
|
34
|
+
},
|
|
35
|
+
'jetbrains-mono': {
|
|
36
|
+
name: 'JetBrains Mono',
|
|
37
|
+
family: "'JetBrains Mono', monospace"
|
|
38
|
+
},
|
|
39
|
+
'ubuntu': {
|
|
40
|
+
name: 'Ubuntu',
|
|
41
|
+
family: "'Ubuntu', sans-serif",
|
|
42
|
+
category: 'sans-serif',
|
|
43
|
+
description: 'Friendly, humanist sans-serif'
|
|
44
|
+
},
|
|
45
|
+
'open-sans': {
|
|
46
|
+
name: 'Open Sans',
|
|
47
|
+
family: "'Open Sans', sans-serif",
|
|
48
|
+
category: 'sans-serif',
|
|
49
|
+
description: 'Neutral, highly readable'
|
|
50
|
+
},
|
|
51
|
+
'rubik': {
|
|
52
|
+
name: 'Rubik',
|
|
53
|
+
family: "'Rubik', sans-serif",
|
|
54
|
+
category: 'sans-serif',
|
|
55
|
+
description: 'Modern, geometric'
|
|
56
|
+
},
|
|
57
|
+
'titillium-web': {
|
|
58
|
+
name: 'Titillium Web',
|
|
59
|
+
family: "'Titillium Web', sans-serif",
|
|
60
|
+
category: 'sans-serif',
|
|
61
|
+
description: 'Technical, elegant'
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
init: function() {
|
|
66
|
+
this.state = {
|
|
67
|
+
preference: this.getPreference()
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (this.isInitialized) {
|
|
71
|
+
this.applyFont();
|
|
72
|
+
this.renderUI();
|
|
73
|
+
this.updateUI();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.isInitialized = true;
|
|
78
|
+
|
|
79
|
+
this.applyFont();
|
|
80
|
+
this.renderUI();
|
|
81
|
+
|
|
82
|
+
console.log('Vanduo Font Switcher initialized');
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get saved font preference from localStorage
|
|
87
|
+
* @returns {string} Font key or 'ubuntu' (default)
|
|
88
|
+
*/
|
|
89
|
+
getPreference: function() {
|
|
90
|
+
return this.getStorageValue(this.STORAGE_KEY, 'ubuntu');
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Set font preference and apply it
|
|
95
|
+
* @param {string} fontKey - The font key to apply
|
|
96
|
+
*/
|
|
97
|
+
setPreference: function(fontKey) {
|
|
98
|
+
if (!this.fonts[fontKey]) {
|
|
99
|
+
console.warn('Unknown font:', fontKey);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.state.preference = fontKey;
|
|
104
|
+
this.setStorageValue(this.STORAGE_KEY, fontKey);
|
|
105
|
+
this.applyFont();
|
|
106
|
+
this.updateUI();
|
|
107
|
+
|
|
108
|
+
// Dispatch custom event for other components to listen to
|
|
109
|
+
const event = new CustomEvent('font:change', {
|
|
110
|
+
bubbles: true,
|
|
111
|
+
detail: { font: fontKey, fontData: this.fonts[fontKey] }
|
|
112
|
+
});
|
|
113
|
+
document.dispatchEvent(event);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply the current font preference to the document
|
|
118
|
+
*/
|
|
119
|
+
applyFont: function() {
|
|
120
|
+
const fontKey = this.state.preference;
|
|
121
|
+
|
|
122
|
+
if (fontKey === 'system') {
|
|
123
|
+
// Remove data-font attribute to use system default
|
|
124
|
+
document.documentElement.removeAttribute('data-font');
|
|
125
|
+
} else {
|
|
126
|
+
// Set data-font attribute which triggers CSS variable override
|
|
127
|
+
document.documentElement.setAttribute('data-font', fontKey);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Initialize UI elements with data-toggle="font"
|
|
133
|
+
*/
|
|
134
|
+
renderUI: function() {
|
|
135
|
+
const toggles = document.querySelectorAll('[data-toggle="font"]');
|
|
136
|
+
|
|
137
|
+
toggles.forEach(toggle => {
|
|
138
|
+
if (toggle.getAttribute('data-font-initialized') === 'true') {
|
|
139
|
+
if (toggle.tagName === 'SELECT') {
|
|
140
|
+
toggle.value = this.state.preference;
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (toggle.tagName === 'SELECT') {
|
|
146
|
+
// Set initial value
|
|
147
|
+
toggle.value = this.state.preference;
|
|
148
|
+
|
|
149
|
+
// Listen for changes
|
|
150
|
+
const onChange = (e) => {
|
|
151
|
+
this.setPreference(e.target.value);
|
|
152
|
+
};
|
|
153
|
+
toggle.addEventListener('change', onChange);
|
|
154
|
+
toggle._fontToggleHandler = onChange;
|
|
155
|
+
} else {
|
|
156
|
+
// Button implementation - cycle through fonts
|
|
157
|
+
const onClick = () => {
|
|
158
|
+
const fontKeys = Object.keys(this.fonts);
|
|
159
|
+
const currentIndex = fontKeys.indexOf(this.state.preference);
|
|
160
|
+
const nextIndex = (currentIndex + 1) % fontKeys.length;
|
|
161
|
+
this.setPreference(fontKeys[nextIndex]);
|
|
162
|
+
};
|
|
163
|
+
toggle.addEventListener('click', onClick);
|
|
164
|
+
toggle._fontToggleHandler = onClick;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
toggle.setAttribute('data-font-initialized', 'true');
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update all UI elements to reflect current state
|
|
173
|
+
*/
|
|
174
|
+
updateUI: function() {
|
|
175
|
+
const toggles = document.querySelectorAll('[data-toggle="font"]');
|
|
176
|
+
|
|
177
|
+
toggles.forEach(toggle => {
|
|
178
|
+
if (toggle.tagName === 'SELECT') {
|
|
179
|
+
toggle.value = this.state.preference;
|
|
180
|
+
} else {
|
|
181
|
+
// Update button text if it has a label span
|
|
182
|
+
const label = toggle.querySelector('.font-current-label');
|
|
183
|
+
if (label) {
|
|
184
|
+
label.textContent = this.fonts[this.state.preference].name;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get the current font preference
|
|
192
|
+
* @returns {string} Current font key
|
|
193
|
+
*/
|
|
194
|
+
getCurrentFont: function() {
|
|
195
|
+
return this.state.preference;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get font data for a given key
|
|
200
|
+
* @param {string} fontKey - The font key
|
|
201
|
+
* @returns {Object|null} Font data or null
|
|
202
|
+
*/
|
|
203
|
+
getFontData: function(fontKey) {
|
|
204
|
+
return this.fonts[fontKey] || null;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
destroyAll: function() {
|
|
208
|
+
const toggles = document.querySelectorAll('[data-toggle="font"][data-font-initialized="true"]');
|
|
209
|
+
toggles.forEach(toggle => {
|
|
210
|
+
if (toggle._fontToggleHandler) {
|
|
211
|
+
const eventName = toggle.tagName === 'SELECT' ? 'change' : 'click';
|
|
212
|
+
toggle.removeEventListener(eventName, toggle._fontToggleHandler);
|
|
213
|
+
delete toggle._fontToggleHandler;
|
|
214
|
+
}
|
|
215
|
+
toggle.removeAttribute('data-font-initialized');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.isInitialized = false;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
getStorageValue: function(key, fallback) {
|
|
222
|
+
if (typeof window.safeStorageGet === 'function') {
|
|
223
|
+
return window.safeStorageGet(key, fallback);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const value = localStorage.getItem(key);
|
|
227
|
+
return value !== null ? value : fallback;
|
|
228
|
+
} catch (_e) {
|
|
229
|
+
return fallback;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
setStorageValue: function(key, value) {
|
|
234
|
+
if (typeof window.safeStorageSet === 'function') {
|
|
235
|
+
return window.safeStorageSet(key, value);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
localStorage.setItem(key, value);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (_e) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Register component
|
|
247
|
+
if (window.Vanduo) {
|
|
248
|
+
window.Vanduo.register('fontSwitcher', FontSwitcher);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Expose globally for convenience
|
|
252
|
+
window.FontSwitcher = FontSwitcher;
|
|
253
|
+
})();
|