vanduo-framework 1.1.8-docs-update

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +216 -0
  3. package/css/components/alerts.css +224 -0
  4. package/css/components/avatar.css +275 -0
  5. package/css/components/badges.css +230 -0
  6. package/css/components/breadcrumbs.css +146 -0
  7. package/css/components/button-group.css +82 -0
  8. package/css/components/buttons.css +530 -0
  9. package/css/components/cards.css +304 -0
  10. package/css/components/chips.css +259 -0
  11. package/css/components/code-snippet.css +555 -0
  12. package/css/components/collapsible.css +267 -0
  13. package/css/components/collections.css +253 -0
  14. package/css/components/doc-search.css +464 -0
  15. package/css/components/doc-tabs.css +38 -0
  16. package/css/components/draggable.css +317 -0
  17. package/css/components/dropdown.css +266 -0
  18. package/css/components/footer.css +375 -0
  19. package/css/components/forms.css +1774 -0
  20. package/css/components/image-box.css +279 -0
  21. package/css/components/modals.css +285 -0
  22. package/css/components/navbar.css +530 -0
  23. package/css/components/pagination.css +186 -0
  24. package/css/components/preloader.css +340 -0
  25. package/css/components/progress.css +107 -0
  26. package/css/components/sidenav.css +301 -0
  27. package/css/components/skeleton.css +241 -0
  28. package/css/components/spinner.css +144 -0
  29. package/css/components/tabs.css +327 -0
  30. package/css/components/theme-customizer.css +835 -0
  31. package/css/components/toast.css +357 -0
  32. package/css/components/tooltips.css +270 -0
  33. package/css/core/colors.css +1017 -0
  34. package/css/core/fonts.css +266 -0
  35. package/css/core/grid.css +1699 -0
  36. package/css/core/helpers.css +2202 -0
  37. package/css/core/reset.css +128 -0
  38. package/css/core/tokens.css +213 -0
  39. package/css/core/typography.css +405 -0
  40. package/css/core/vd-aliases.css +47 -0
  41. package/css/effects/parallax.css +113 -0
  42. package/css/icons/icons-all.css +23 -0
  43. package/css/icons/icons.css +25 -0
  44. package/css/utilities/media.css +167 -0
  45. package/css/utilities/print.css +111 -0
  46. package/css/utilities/shadow.css +243 -0
  47. package/css/utilities/table.css +381 -0
  48. package/css/utilities/transforms.css +71 -0
  49. package/css/utilities/transitions.css +87 -0
  50. package/css/vanduo.css +80 -0
  51. package/dist/build-info.json +6 -0
  52. package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  53. package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  54. package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  55. package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  56. package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  57. package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  58. package/dist/fonts/inter/inter-bold.woff2 +0 -0
  59. package/dist/fonts/inter/inter-medium.woff2 +0 -0
  60. package/dist/fonts/inter/inter-regular.woff2 +0 -0
  61. package/dist/fonts/inter/inter-semibold.woff2 +0 -0
  62. package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  63. package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  64. package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
  65. package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
  66. package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
  67. package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
  68. package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
  69. package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
  70. package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
  71. package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
  72. package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  73. package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  74. package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  75. package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  76. package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  77. package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  78. package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  79. package/dist/icons/phosphor/LICENSE +21 -0
  80. package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  81. package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  82. package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  83. package/dist/icons/phosphor/bold/style.css +4627 -0
  84. package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  85. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  86. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  87. package/dist/icons/phosphor/duotone/style.css +12115 -0
  88. package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  89. package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  90. package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  91. package/dist/icons/phosphor/fill/style.css +4627 -0
  92. package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  93. package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
  94. package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  95. package/dist/icons/phosphor/light/style.css +4627 -0
  96. package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
  97. package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
  98. package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
  99. package/dist/icons/phosphor/regular/style.css +4627 -0
  100. package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  101. package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  102. package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  103. package/dist/icons/phosphor/thin/style.css +4627 -0
  104. package/dist/vanduo.cjs.js +5569 -0
  105. package/dist/vanduo.cjs.js.map +7 -0
  106. package/dist/vanduo.cjs.min.js +48 -0
  107. package/dist/vanduo.cjs.min.js.map +7 -0
  108. package/dist/vanduo.css +60666 -0
  109. package/dist/vanduo.css.map +1 -0
  110. package/dist/vanduo.esm.js +5548 -0
  111. package/dist/vanduo.esm.js.map +7 -0
  112. package/dist/vanduo.esm.min.js +48 -0
  113. package/dist/vanduo.esm.min.js.map +7 -0
  114. package/dist/vanduo.js +5545 -0
  115. package/dist/vanduo.js.map +7 -0
  116. package/dist/vanduo.min.css +2 -0
  117. package/dist/vanduo.min.css.map +1 -0
  118. package/dist/vanduo.min.js +48 -0
  119. package/dist/vanduo.min.js.map +7 -0
  120. package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  121. package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  122. package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  123. package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  124. package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  125. package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  126. package/fonts/inter/inter-bold.woff2 +0 -0
  127. package/fonts/inter/inter-medium.woff2 +0 -0
  128. package/fonts/inter/inter-regular.woff2 +0 -0
  129. package/fonts/inter/inter-semibold.woff2 +0 -0
  130. package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  131. package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  132. package/fonts/open-sans/open-sans-bold.woff2 +0 -0
  133. package/fonts/open-sans/open-sans-medium.woff2 +0 -0
  134. package/fonts/open-sans/open-sans-regular.woff2 +0 -0
  135. package/fonts/rubik/rubik-bold.woff2 +0 -0
  136. package/fonts/rubik/rubik-medium.woff2 +0 -0
  137. package/fonts/rubik/rubik-regular.woff2 +0 -0
  138. package/fonts/source-sans/source-sans-bold.woff2 +0 -0
  139. package/fonts/source-sans/source-sans-regular.woff2 +0 -0
  140. package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  141. package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  142. package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  143. package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  144. package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  145. package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  146. package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  147. package/icons/phosphor/LICENSE +21 -0
  148. package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  149. package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  150. package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  151. package/icons/phosphor/bold/style.css +4627 -0
  152. package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  153. package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  154. package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  155. package/icons/phosphor/duotone/style.css +12115 -0
  156. package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  157. package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  158. package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  159. package/icons/phosphor/fill/style.css +4627 -0
  160. package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  161. package/icons/phosphor/light/Phosphor-Light.woff +0 -0
  162. package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  163. package/icons/phosphor/light/style.css +4627 -0
  164. package/icons/phosphor/regular/Phosphor.ttf +0 -0
  165. package/icons/phosphor/regular/Phosphor.woff +0 -0
  166. package/icons/phosphor/regular/Phosphor.woff2 +0 -0
  167. package/icons/phosphor/regular/style.css +4627 -0
  168. package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  169. package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  170. package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  171. package/icons/phosphor/thin/style.css +4627 -0
  172. package/js/components/code-snippet.js +639 -0
  173. package/js/components/collapsible.js +226 -0
  174. package/js/components/doc-search.js +936 -0
  175. package/js/components/draggable.js +725 -0
  176. package/js/components/dropdown.js +362 -0
  177. package/js/components/font-switcher.js +253 -0
  178. package/js/components/grid.js +279 -0
  179. package/js/components/image-box.js +372 -0
  180. package/js/components/modals.js +367 -0
  181. package/js/components/navbar.js +264 -0
  182. package/js/components/pagination.js +286 -0
  183. package/js/components/parallax.js +216 -0
  184. package/js/components/preloader.js +183 -0
  185. package/js/components/select.js +444 -0
  186. package/js/components/sidenav.js +303 -0
  187. package/js/components/tabs.js +303 -0
  188. package/js/components/theme-customizer.js +784 -0
  189. package/js/components/theme-switcher.js +183 -0
  190. package/js/components/toast.js +343 -0
  191. package/js/components/tooltips.js +306 -0
  192. package/js/index.js +52 -0
  193. package/js/utils/helpers.js +306 -0
  194. package/js/utils/lifecycle.js +135 -0
  195. package/js/vanduo.js +120 -0
  196. package/package.json +78 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Vanduo Framework - Select Component
3
+ * Custom select dropdown with enhanced functionality
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Select Component
11
+ */
12
+ const Select = {
13
+ // Store initialized selects and their cleanup functions
14
+ instances: new Map(),
15
+ // Typeahead state
16
+ _typeaheadBuffer: '',
17
+ _typeaheadTimer: null,
18
+
19
+ /**
20
+ * Initialize select components
21
+ */
22
+ init: function () {
23
+ const selects = document.querySelectorAll('select.vd-custom-select-input, select[data-custom-select]');
24
+
25
+ selects.forEach(select => {
26
+ if (this.instances.has(select)) {
27
+ return;
28
+ }
29
+ this.initSelect(select);
30
+ });
31
+ },
32
+
33
+ /**
34
+ * Initialize a single select
35
+ * @param {HTMLSelectElement} select - Select element
36
+ */
37
+ initSelect: function (select) {
38
+ // Skip if already has custom wrapper
39
+ if (select.closest('.vd-custom-select-wrapper')) {
40
+ return;
41
+ }
42
+
43
+ const cleanupFunctions = [];
44
+
45
+ // Create wrapper
46
+ const wrapper = document.createElement('div');
47
+ wrapper.className = 'custom-select-wrapper';
48
+ select.parentNode.insertBefore(wrapper, select);
49
+ wrapper.appendChild(select);
50
+
51
+ // Create custom button
52
+ const button = document.createElement('button');
53
+ button.type = 'button';
54
+ button.className = 'custom-select-button';
55
+ button.setAttribute('aria-haspopup', 'listbox');
56
+ button.setAttribute('aria-expanded', 'false');
57
+ button.setAttribute('aria-labelledby', select.id || this.generateId(select));
58
+
59
+ // Create dropdown
60
+ const dropdown = document.createElement('div');
61
+ dropdown.className = 'custom-select-dropdown';
62
+ dropdown.setAttribute('role', 'listbox');
63
+
64
+ // Create search input if searchable
65
+ if (select.dataset.searchable === 'true') {
66
+ const searchWrapper = document.createElement('div');
67
+ searchWrapper.className = 'custom-select-search';
68
+ const searchInput = document.createElement('input');
69
+ searchInput.type = 'text';
70
+ searchInput.className = 'input input-sm';
71
+ searchInput.placeholder = 'Search...';
72
+ searchInput.setAttribute('aria-label', 'Search options');
73
+ searchWrapper.appendChild(searchInput);
74
+ dropdown.appendChild(searchWrapper);
75
+
76
+ const filterFn = (e) => {
77
+ this.filterOptions(dropdown, e.target.value);
78
+ };
79
+ const searchHandler = typeof debounce === 'function' ? debounce(filterFn, 150) : filterFn;
80
+ searchInput.addEventListener('input', searchHandler);
81
+ cleanupFunctions.push(() => searchInput.removeEventListener('input', searchHandler));
82
+ }
83
+
84
+ // Build options
85
+ this.buildOptions(select, dropdown, button);
86
+
87
+ wrapper.appendChild(button);
88
+ wrapper.appendChild(dropdown);
89
+
90
+ // Update button text
91
+ this.updateButtonText(select, button);
92
+
93
+ // Event listeners
94
+ const buttonClickHandler = (e) => {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ this.toggleDropdown(button, dropdown);
98
+ };
99
+ button.addEventListener('click', buttonClickHandler);
100
+ cleanupFunctions.push(() => button.removeEventListener('click', buttonClickHandler));
101
+
102
+ // Close on outside click
103
+ const documentClickHandler = (e) => {
104
+ if (!wrapper.contains(e.target) && dropdown.classList.contains('is-open')) {
105
+ this.closeDropdown(button, dropdown);
106
+ }
107
+ };
108
+ document.addEventListener('click', documentClickHandler);
109
+ cleanupFunctions.push(() => document.removeEventListener('click', documentClickHandler));
110
+
111
+ // Keyboard navigation
112
+ const keydownHandler = (e) => {
113
+ this.handleKeydown(e, select, button, dropdown);
114
+ };
115
+ button.addEventListener('keydown', keydownHandler);
116
+ cleanupFunctions.push(() => button.removeEventListener('keydown', keydownHandler));
117
+
118
+ // Update on select change
119
+ const changeHandler = () => {
120
+ this.updateButtonText(select, button);
121
+ this.updateSelectedOptions(select, dropdown);
122
+ };
123
+ select.addEventListener('change', changeHandler);
124
+ cleanupFunctions.push(() => select.removeEventListener('change', changeHandler));
125
+
126
+ this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions });
127
+ },
128
+
129
+ /**
130
+ * Build options in dropdown
131
+ * @param {HTMLSelectElement} select - Select element
132
+ * @param {HTMLElement} dropdown - Dropdown container
133
+ * @param {HTMLElement} button - Button element
134
+ */
135
+ buildOptions: function (select, dropdown, button) {
136
+ const options = select.querySelectorAll('option');
137
+ const fragment = document.createDocumentFragment();
138
+
139
+ options.forEach((option, index) => {
140
+ if (option.parentElement.tagName === 'OPTGROUP') {
141
+ // Handle option groups
142
+ const group = option.parentElement;
143
+ if (!dropdown.querySelector(`[data-group="${group.label}"]`)) {
144
+ const groupElement = document.createElement('div');
145
+ groupElement.className = 'custom-select-option-group';
146
+ groupElement.textContent = group.label;
147
+ groupElement.dataset.group = group.label;
148
+ fragment.appendChild(groupElement);
149
+ }
150
+ }
151
+
152
+ if (option.value === '' && !option.textContent.trim()) {
153
+ return; // Skip empty options
154
+ }
155
+
156
+ const optionElement = document.createElement('div');
157
+ optionElement.className = 'custom-select-option';
158
+ optionElement.textContent = option.textContent;
159
+ optionElement.setAttribute('role', 'option');
160
+ optionElement.setAttribute('data-value', option.value);
161
+ optionElement.setAttribute('data-index', index);
162
+
163
+ if (option.selected) {
164
+ optionElement.classList.add('is-selected');
165
+ optionElement.setAttribute('aria-selected', 'true');
166
+ }
167
+
168
+ if (option.disabled) {
169
+ optionElement.classList.add('is-disabled');
170
+ optionElement.setAttribute('aria-disabled', 'true');
171
+ }
172
+
173
+ optionElement.addEventListener('click', (_e) => {
174
+ if (!option.disabled) {
175
+ this.selectOption(select, option, optionElement, button, dropdown);
176
+ }
177
+ });
178
+
179
+ fragment.appendChild(optionElement);
180
+ });
181
+
182
+ dropdown.appendChild(fragment);
183
+ },
184
+
185
+ /**
186
+ * Select an option
187
+ * @param {HTMLSelectElement} select - Select element
188
+ * @param {HTMLOptionElement} option - Option element
189
+ * @param {HTMLElement} optionElement - Custom option element
190
+ * @param {HTMLElement} button - Button element
191
+ * @param {HTMLElement} dropdown - Dropdown container
192
+ */
193
+ selectOption: function (select, option, optionElement, button, dropdown) {
194
+ if (select.multiple) {
195
+ // Multi-select
196
+ option.selected = !option.selected;
197
+ optionElement.classList.toggle('is-selected');
198
+ optionElement.setAttribute('aria-selected', option.selected);
199
+ } else {
200
+ // Single select
201
+ select.value = option.value;
202
+ select.dispatchEvent(new Event('change', { bubbles: true }));
203
+ this.closeDropdown(button, dropdown);
204
+ }
205
+
206
+ this.updateButtonText(select, button);
207
+ },
208
+
209
+ /**
210
+ * Update button text
211
+ * @param {HTMLSelectElement} select - Select element
212
+ * @param {HTMLElement} button - Button element
213
+ */
214
+ updateButtonText: function (select, button) {
215
+ if (select.multiple) {
216
+ const selected = Array.from(select.selectedOptions);
217
+ if (selected.length === 0) {
218
+ button.textContent = select.dataset.placeholder || 'Select options...';
219
+ } else if (selected.length === 1) {
220
+ button.textContent = selected[0].textContent;
221
+ } else {
222
+ button.textContent = `${selected.length} selected`;
223
+ }
224
+ } else {
225
+ const selectedOption = select.options[select.selectedIndex];
226
+ button.textContent = selectedOption ? selectedOption.textContent : (select.dataset.placeholder || 'Select...');
227
+ }
228
+ },
229
+
230
+ /**
231
+ * Update selected options in dropdown
232
+ * @param {HTMLSelectElement} select - Select element
233
+ * @param {HTMLElement} dropdown - Dropdown container
234
+ */
235
+ updateSelectedOptions: function (select, dropdown) {
236
+ const options = dropdown.querySelectorAll('.vd-custom-select-option');
237
+ const selectedValues = Array.from(select.selectedOptions).map(opt => opt.value);
238
+
239
+ options.forEach(optionEl => {
240
+ const value = optionEl.dataset.value;
241
+ if (selectedValues.includes(value)) {
242
+ optionEl.classList.add('is-selected');
243
+ optionEl.setAttribute('aria-selected', 'true');
244
+ } else {
245
+ optionEl.classList.remove('is-selected');
246
+ optionEl.setAttribute('aria-selected', 'false');
247
+ }
248
+ });
249
+ },
250
+
251
+ /**
252
+ * Toggle dropdown
253
+ * @param {HTMLElement} button - Button element
254
+ * @param {HTMLElement} dropdown - Dropdown container
255
+ */
256
+ toggleDropdown: function (button, dropdown) {
257
+ const isOpen = dropdown.classList.contains('is-open');
258
+
259
+ if (isOpen) {
260
+ this.closeDropdown(button, dropdown);
261
+ } else {
262
+ this.openDropdown(button, dropdown);
263
+ }
264
+ },
265
+
266
+ /**
267
+ * Open dropdown
268
+ * @param {HTMLElement} button - Button element
269
+ * @param {HTMLElement} dropdown - Dropdown container
270
+ */
271
+ openDropdown: function (button, dropdown) {
272
+ dropdown.classList.add('is-open');
273
+ button.setAttribute('aria-expanded', 'true');
274
+
275
+ // Focus first option
276
+ const firstOption = dropdown.querySelector('.vd-custom-select-option:not(.is-disabled)');
277
+ if (firstOption) {
278
+ firstOption.focus();
279
+ }
280
+ },
281
+
282
+ /**
283
+ * Close dropdown
284
+ * @param {HTMLElement} button - Button element
285
+ * @param {HTMLElement} dropdown - Dropdown container
286
+ */
287
+ closeDropdown: function (button, dropdown) {
288
+ dropdown.classList.remove('is-open');
289
+ button.setAttribute('aria-expanded', 'false');
290
+ },
291
+
292
+ /**
293
+ * Handle keyboard navigation
294
+ * @param {KeyboardEvent} e - Keyboard event
295
+ * @param {HTMLSelectElement} select - Select element
296
+ * @param {HTMLElement} button - Button element
297
+ * @param {HTMLElement} dropdown - Dropdown container
298
+ */
299
+ handleKeydown: function (e, select, button, dropdown) {
300
+ const isOpen = dropdown.classList.contains('is-open');
301
+ const options = Array.from(dropdown.querySelectorAll('.vd-custom-select-option:not(.is-disabled)'));
302
+ const currentIndex = options.findIndex(opt => opt === document.activeElement);
303
+
304
+ switch (e.key) {
305
+ case 'Enter':
306
+ case ' ':
307
+ e.preventDefault();
308
+ if (isOpen && currentIndex >= 0) {
309
+ const optionEl = options[currentIndex];
310
+ const option = select.options[parseInt(optionEl.dataset.index)];
311
+ this.selectOption(select, option, optionEl, button, dropdown);
312
+ } else {
313
+ this.openDropdown(button, dropdown);
314
+ }
315
+ break;
316
+
317
+ case 'Escape':
318
+ if (isOpen) {
319
+ e.preventDefault();
320
+ this.closeDropdown(button, dropdown);
321
+ button.focus();
322
+ }
323
+ break;
324
+
325
+ case 'ArrowDown':
326
+ e.preventDefault();
327
+ if (!isOpen) {
328
+ this.openDropdown(button, dropdown);
329
+ } else {
330
+ const nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0;
331
+ options[nextIndex].focus();
332
+ }
333
+ break;
334
+
335
+ case 'ArrowUp':
336
+ e.preventDefault();
337
+ if (isOpen) {
338
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1;
339
+ options[prevIndex].focus();
340
+ }
341
+ break;
342
+
343
+ case 'Home':
344
+ if (isOpen) {
345
+ e.preventDefault();
346
+ options[0].focus();
347
+ }
348
+ break;
349
+
350
+ case 'End':
351
+ if (isOpen) {
352
+ e.preventDefault();
353
+ options[options.length - 1].focus();
354
+ }
355
+ break;
356
+
357
+ default:
358
+ // Typeahead: jump to matching option when typing printable characters
359
+ if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
360
+ clearTimeout(this._typeaheadTimer);
361
+ this._typeaheadBuffer += e.key.toLowerCase();
362
+
363
+ const match = options.find(opt =>
364
+ opt.textContent.trim().toLowerCase().startsWith(this._typeaheadBuffer)
365
+ );
366
+ if (match) {
367
+ match.focus();
368
+ }
369
+
370
+ this._typeaheadTimer = setTimeout(() => {
371
+ this._typeaheadBuffer = '';
372
+ }, 500);
373
+ }
374
+ break;
375
+ }
376
+ },
377
+
378
+ /**
379
+ * Filter options by search term
380
+ * @param {HTMLElement} dropdown - Dropdown container
381
+ * @param {string} searchTerm - Search term
382
+ */
383
+ filterOptions: function (dropdown, searchTerm) {
384
+ const options = dropdown.querySelectorAll('.vd-custom-select-option');
385
+ const term = searchTerm.toLowerCase();
386
+
387
+ options.forEach(option => {
388
+ const text = option.textContent.toLowerCase();
389
+ if (text.includes(term)) {
390
+ option.style.display = 'block';
391
+ } else {
392
+ option.style.display = 'none';
393
+ }
394
+ });
395
+ },
396
+
397
+ /**
398
+ * Generate unique ID
399
+ * @param {HTMLElement} element - Element
400
+ * @returns {string} Generated ID
401
+ */
402
+ generateId: function (element) {
403
+ if (element.id) {
404
+ return element.id;
405
+ }
406
+ return 'select-' + Math.random().toString(36).substr(2, 9);
407
+ },
408
+
409
+ /**
410
+ * Destroy a select instance and clean up event listeners
411
+ * @param {HTMLSelectElement} select - Select element
412
+ */
413
+ destroy: function (select) {
414
+ const instance = this.instances.get(select);
415
+ if (!instance) return;
416
+
417
+ instance.cleanup.forEach(fn => fn());
418
+
419
+ // Unwrap the select element back to its original parent
420
+ if (instance.wrapper && instance.wrapper.parentNode) {
421
+ instance.wrapper.parentNode.insertBefore(select, instance.wrapper);
422
+ instance.wrapper.parentNode.removeChild(instance.wrapper);
423
+ }
424
+
425
+ this.instances.delete(select);
426
+ },
427
+
428
+ /**
429
+ * Destroy all select instances
430
+ */
431
+ destroyAll: function () {
432
+ this.instances.forEach((instance, select) => {
433
+ this.destroy(select);
434
+ });
435
+ }
436
+ };
437
+
438
+ // Register with Vanduo framework if available
439
+ if (typeof window.Vanduo !== 'undefined') {
440
+ window.Vanduo.register('select', Select);
441
+ }
442
+
443
+ })();
444
+