@dodlhuat/basix 1.1.1 → 1.2.1

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 (352) hide show
  1. package/README.md +706 -482
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/badge.scss +104 -0
  5. package/css/bottom-sheet.scss +192 -0
  6. package/css/breadcrumb.scss +158 -0
  7. package/css/button.scss +48 -0
  8. package/css/calendar.scss +957 -0
  9. package/css/card.scss +65 -65
  10. package/css/chart.scss +270 -157
  11. package/css/chat-bubbles.scss +134 -68
  12. package/css/chips.scss +109 -19
  13. package/css/colors.scss +32 -32
  14. package/css/context-menu.scss +182 -0
  15. package/css/datepicker.scss +336 -336
  16. package/css/defaults.scss +90 -90
  17. package/css/docs.scss +529 -0
  18. package/css/editor.scss +664 -461
  19. package/css/file-uploader.scss +1 -1
  20. package/css/flyout-menu.scss +361 -361
  21. package/css/form.scss +124 -0
  22. package/css/gallery.scss +65 -6
  23. package/css/grid.scss +41 -40
  24. package/css/group-picker.scss +345 -0
  25. package/css/guitar-chords.css +250 -250
  26. package/css/icons.scss +330 -330
  27. package/css/parameters.scss +3 -3
  28. package/css/placeholder.scss +33 -33
  29. package/css/popover.scss +206 -0
  30. package/css/progress.scss +76 -32
  31. package/css/properties.scss +51 -36
  32. package/css/push-menu.scss +302 -174
  33. package/css/reset.scss +39 -39
  34. package/css/scrollbar.scss +62 -5
  35. package/css/sidebar-nav.scss +92 -0
  36. package/css/spinner.scss +65 -65
  37. package/css/stepper.scss +248 -0
  38. package/css/style.css +4603 -273
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.scss +51 -39
  42. package/css/table.scss +199 -199
  43. package/css/tabs.scss +154 -123
  44. package/css/timeline.scss +83 -38
  45. package/css/timepicker.scss +100 -5
  46. package/css/toast.scss +81 -81
  47. package/css/typography.scss +194 -161
  48. package/css/virtual-dropdown.scss +35 -29
  49. package/js/bottom-sheet.js +173 -0
  50. package/js/bottom-sheet.ts +222 -0
  51. package/js/calendar.js +532 -0
  52. package/js/calendar.ts +706 -0
  53. package/js/carousel.js +26 -13
  54. package/js/chart.js +573 -257
  55. package/js/chart.ts +692 -0
  56. package/js/code-viewer.js +10 -10
  57. package/js/code-viewer.ts +188 -188
  58. package/js/context-menu.js +212 -0
  59. package/js/context-menu.ts +252 -0
  60. package/js/datepicker.ts +627 -627
  61. package/js/docs-nav.js +204 -0
  62. package/js/dropdown.ts +179 -179
  63. package/js/editor.js +96 -38
  64. package/js/editor.ts +483 -425
  65. package/js/file-uploader.js +1 -0
  66. package/js/file-uploader.ts +1 -0
  67. package/js/flyout-menu.js +14 -14
  68. package/js/flyout-menu.ts +249 -249
  69. package/js/form-builder.js +106 -106
  70. package/js/gallery.js +13 -6
  71. package/js/gallery.ts +245 -236
  72. package/js/group-picker.js +342 -0
  73. package/js/group-picker.ts +447 -0
  74. package/js/guitar-chords.js +268 -268
  75. package/js/lazy-loader.js +121 -121
  76. package/js/modal.ts +166 -166
  77. package/js/popover.js +163 -0
  78. package/js/popover.ts +219 -0
  79. package/js/position.js +108 -0
  80. package/js/position.ts +111 -0
  81. package/js/push-menu.js +226 -113
  82. package/js/push-menu.ts +284 -145
  83. package/js/request.js +50 -50
  84. package/js/scroll.ts +47 -47
  85. package/js/scrollbar.js +13 -0
  86. package/js/scrollbar.ts +324 -307
  87. package/js/select.ts +216 -216
  88. package/js/sidebar-nav.js +41 -0
  89. package/js/sidebar-nav.ts +66 -0
  90. package/js/stepper.js +80 -0
  91. package/js/stepper.ts +104 -0
  92. package/js/table.ts +452 -452
  93. package/js/tabs.ts +279 -279
  94. package/js/theme.js +17 -6
  95. package/js/theme.ts +234 -224
  96. package/js/timepicker.js +21 -8
  97. package/js/toast.ts +137 -137
  98. package/js/tooltip.js +6 -60
  99. package/js/tooltip.ts +184 -251
  100. package/js/tsconfig.json +18 -18
  101. package/js/utils.ts +83 -83
  102. package/js/virtual-dropdown.js +25 -25
  103. package/js/virtual-dropdown.ts +365 -365
  104. package/package.json +39 -39
  105. package/fonts/Outfit-VariableFont_wght.woff +0 -0
  106. package/fonts/material-icons.woff2 +0 -0
  107. package/icons/activity-outline.svg +0 -1
  108. package/icons/alert-circle-outline.svg +0 -1
  109. package/icons/alert-triangle-outline.svg +0 -1
  110. package/icons/archive-outline.svg +0 -1
  111. package/icons/arrow-back-outline.svg +0 -1
  112. package/icons/arrow-circle-down-outline.svg +0 -1
  113. package/icons/arrow-circle-left-outline.svg +0 -1
  114. package/icons/arrow-circle-right-outline.svg +0 -1
  115. package/icons/arrow-circle-up-outline.svg +0 -1
  116. package/icons/arrow-down-outline.svg +0 -1
  117. package/icons/arrow-downward-outline.svg +0 -1
  118. package/icons/arrow-forward-outline.svg +0 -1
  119. package/icons/arrow-ios-back-outline.svg +0 -1
  120. package/icons/arrow-ios-downward-outline.svg +0 -1
  121. package/icons/arrow-ios-forward-outline.svg +0 -1
  122. package/icons/arrow-ios-upward-outline.svg +0 -1
  123. package/icons/arrow-left-outline.svg +0 -1
  124. package/icons/arrow-right-outline.svg +0 -1
  125. package/icons/arrow-up-outline.svg +0 -1
  126. package/icons/arrow-upward-outline.svg +0 -1
  127. package/icons/arrowhead-down-outline.svg +0 -1
  128. package/icons/arrowhead-left-outline.svg +0 -1
  129. package/icons/arrowhead-right-outline.svg +0 -1
  130. package/icons/arrowhead-up-outline.svg +0 -1
  131. package/icons/at-outline.svg +0 -1
  132. package/icons/attach-2-outline.svg +0 -1
  133. package/icons/attach-outline.svg +0 -1
  134. package/icons/award-outline.svg +0 -1
  135. package/icons/backspace-outline.svg +0 -1
  136. package/icons/bar-chart-2-outline.svg +0 -1
  137. package/icons/bar-chart-outline.svg +0 -1
  138. package/icons/battery-outline.svg +0 -1
  139. package/icons/behance-outline.svg +0 -1
  140. package/icons/bell-off-outline.svg +0 -1
  141. package/icons/bell-outline.svg +0 -1
  142. package/icons/bluetooth-outline.svg +0 -1
  143. package/icons/book-open-outline.svg +0 -1
  144. package/icons/book-outline.svg +0 -1
  145. package/icons/bookmark-outline.svg +0 -1
  146. package/icons/briefcase-outline.svg +0 -1
  147. package/icons/browser-outline.svg +0 -1
  148. package/icons/brush-outline.svg +0 -1
  149. package/icons/bulb-outline.svg +0 -1
  150. package/icons/calendar-outline.svg +0 -1
  151. package/icons/camera-outline.svg +0 -1
  152. package/icons/car-outline.svg +0 -1
  153. package/icons/cast-outline.svg +0 -1
  154. package/icons/charging-outline.svg +0 -1
  155. package/icons/checkmark-circle-2-outline.svg +0 -1
  156. package/icons/checkmark-circle-outline.svg +0 -1
  157. package/icons/checkmark-outline.svg +0 -1
  158. package/icons/checkmark-square-2-outline.svg +0 -1
  159. package/icons/checkmark-square-outline.svg +0 -1
  160. package/icons/chevron-down-outline.svg +0 -1
  161. package/icons/chevron-left-outline.svg +0 -1
  162. package/icons/chevron-right-outline.svg +0 -1
  163. package/icons/chevron-up-outline.svg +0 -1
  164. package/icons/clipboard-outline.svg +0 -1
  165. package/icons/clock-outline.svg +0 -1
  166. package/icons/close-circle-outline.svg +0 -1
  167. package/icons/close-outline.svg +0 -1
  168. package/icons/close-square-outline.svg +0 -1
  169. package/icons/cloud-download-outline.svg +0 -1
  170. package/icons/cloud-upload-outline.svg +0 -1
  171. package/icons/code-download-outline.svg +0 -1
  172. package/icons/code-outline.svg +0 -1
  173. package/icons/collapse-outline.svg +0 -1
  174. package/icons/color-palette-outline.svg +0 -1
  175. package/icons/color-picker-outline.svg +0 -1
  176. package/icons/compass-outline.svg +0 -1
  177. package/icons/copy-outline.svg +0 -1
  178. package/icons/corner-down-left-outline.svg +0 -1
  179. package/icons/corner-down-right-outline.svg +0 -1
  180. package/icons/corner-left-down-outline.svg +0 -1
  181. package/icons/corner-left-up-outline.svg +0 -1
  182. package/icons/corner-right-down-outline.svg +0 -1
  183. package/icons/corner-right-up-outline.svg +0 -1
  184. package/icons/corner-up-left-outline.svg +0 -1
  185. package/icons/corner-up-right-outline.svg +0 -1
  186. package/icons/credit-card-outline.svg +0 -1
  187. package/icons/crop-outline.svg +0 -1
  188. package/icons/cube-outline.svg +0 -1
  189. package/icons/diagonal-arrow-left-down-outline.svg +0 -1
  190. package/icons/diagonal-arrow-left-up-outline.svg +0 -1
  191. package/icons/diagonal-arrow-right-down-outline.svg +0 -1
  192. package/icons/diagonal-arrow-right-up-outline.svg +0 -1
  193. package/icons/done-all-outline.svg +0 -1
  194. package/icons/download-outline.svg +0 -1
  195. package/icons/droplet-off-outline.svg +0 -1
  196. package/icons/droplet-outline.svg +0 -1
  197. package/icons/edit-2-outline.svg +0 -1
  198. package/icons/edit-outline.svg +0 -1
  199. package/icons/email-outline.svg +0 -1
  200. package/icons/expand-outline.svg +0 -1
  201. package/icons/external-link-outline.svg +0 -1
  202. package/icons/eye-off-2-outline.svg +0 -1
  203. package/icons/eye-off-outline.svg +0 -1
  204. package/icons/eye-outline.svg +0 -1
  205. package/icons/facebook-outline.svg +0 -1
  206. package/icons/file-add-outline.svg +0 -1
  207. package/icons/file-outline.svg +0 -1
  208. package/icons/file-remove-outline.svg +0 -1
  209. package/icons/file-text-outline.svg +0 -1
  210. package/icons/film-outline.svg +0 -1
  211. package/icons/flag-outline.svg +0 -1
  212. package/icons/flash-off-outline.svg +0 -1
  213. package/icons/flash-outline.svg +0 -1
  214. package/icons/flip-2-outline.svg +0 -1
  215. package/icons/flip-outline.svg +0 -1
  216. package/icons/folder-add-outline.svg +0 -1
  217. package/icons/folder-outline.svg +0 -1
  218. package/icons/folder-remove-outline.svg +0 -1
  219. package/icons/funnel-outline.svg +0 -1
  220. package/icons/gift-outline.svg +0 -1
  221. package/icons/github-outline.svg +0 -1
  222. package/icons/globe-2-outline.svg +0 -1
  223. package/icons/globe-outline.svg +0 -1
  224. package/icons/google-outline.svg +0 -1
  225. package/icons/grid-outline.svg +0 -1
  226. package/icons/hard-drive-outline.svg +0 -1
  227. package/icons/hash-outline.svg +0 -1
  228. package/icons/headphones-outline.svg +0 -1
  229. package/icons/heart-outline.svg +0 -1
  230. package/icons/home-outline.svg +0 -1
  231. package/icons/image-outline.svg +0 -1
  232. package/icons/inbox-outline.svg +0 -1
  233. package/icons/info-outline.svg +0 -1
  234. package/icons/keypad-outline.svg +0 -1
  235. package/icons/layers-outline.svg +0 -1
  236. package/icons/layout-outline.svg +0 -1
  237. package/icons/link-2-outline.svg +0 -1
  238. package/icons/link-outline.svg +0 -1
  239. package/icons/linkedin-outline.svg +0 -1
  240. package/icons/list-outline.svg +0 -1
  241. package/icons/loader-outline.svg +0 -1
  242. package/icons/lock-outline.svg +0 -1
  243. package/icons/log-in-outline.svg +0 -1
  244. package/icons/log-out-outline.svg +0 -1
  245. package/icons/map-outline.svg +0 -1
  246. package/icons/maximize-outline.svg +0 -1
  247. package/icons/menu-2-outline.svg +0 -1
  248. package/icons/menu-arrow-outline.svg +0 -1
  249. package/icons/menu-outline.svg +0 -1
  250. package/icons/message-circle-outline.svg +0 -1
  251. package/icons/message-square-outline.svg +0 -1
  252. package/icons/mic-off-outline.svg +0 -1
  253. package/icons/mic-outline.svg +0 -1
  254. package/icons/minimize-outline.svg +0 -1
  255. package/icons/minus-circle-outline.svg +0 -1
  256. package/icons/minus-outline.svg +0 -1
  257. package/icons/minus-square-outline.svg +0 -1
  258. package/icons/monitor-outline.svg +0 -1
  259. package/icons/moon-outline.svg +0 -1
  260. package/icons/more-horizontal-outline.svg +0 -1
  261. package/icons/more-vertical-outline.svg +0 -1
  262. package/icons/move-outline.svg +0 -1
  263. package/icons/music-outline.svg +0 -1
  264. package/icons/navigation-2-outline.svg +0 -1
  265. package/icons/navigation-outline.svg +0 -1
  266. package/icons/npm-outline.svg +0 -1
  267. package/icons/options-2-outline.svg +0 -1
  268. package/icons/options-outline.svg +0 -1
  269. package/icons/pantone-outline.svg +0 -1
  270. package/icons/paper-plane-outline.svg +0 -1
  271. package/icons/pause-circle-outline.svg +0 -1
  272. package/icons/people-outline.svg +0 -1
  273. package/icons/percent-outline.svg +0 -1
  274. package/icons/person-add-outline.svg +0 -1
  275. package/icons/person-delete-outline.svg +0 -1
  276. package/icons/person-done-outline.svg +0 -1
  277. package/icons/person-outline.svg +0 -1
  278. package/icons/person-remove-outline.svg +0 -1
  279. package/icons/phone-call-outline.svg +0 -1
  280. package/icons/phone-missed-outline.svg +0 -1
  281. package/icons/phone-off-outline.svg +0 -1
  282. package/icons/phone-outline.svg +0 -1
  283. package/icons/pie-chart-outline.svg +0 -1
  284. package/icons/pin-outline.svg +0 -1
  285. package/icons/play-circle-outline.svg +0 -1
  286. package/icons/plus-circle-outline.svg +0 -1
  287. package/icons/plus-outline.svg +0 -1
  288. package/icons/plus-square-outline.svg +0 -1
  289. package/icons/power-outline.svg +0 -1
  290. package/icons/pricetags-outline.svg +0 -1
  291. package/icons/printer-outline.svg +0 -1
  292. package/icons/question-mark-circle-outline.svg +0 -1
  293. package/icons/question-mark-outline.svg +0 -1
  294. package/icons/radio-button-off-outline.svg +0 -1
  295. package/icons/radio-button-on-outline.svg +0 -1
  296. package/icons/radio-outline.svg +0 -1
  297. package/icons/recording-outline.svg +0 -1
  298. package/icons/refresh-outline.svg +0 -1
  299. package/icons/repeat-outline.svg +0 -1
  300. package/icons/rewind-left-outline.svg +0 -1
  301. package/icons/rewind-right-outline.svg +0 -1
  302. package/icons/save-outline.svg +0 -1
  303. package/icons/scissors-outline.svg +0 -1
  304. package/icons/search-outline.svg +0 -1
  305. package/icons/settings-2-outline.svg +0 -1
  306. package/icons/settings-outline.svg +0 -1
  307. package/icons/shake-outline.svg +0 -1
  308. package/icons/share-outline.svg +0 -1
  309. package/icons/shield-off-outline.svg +0 -1
  310. package/icons/shield-outline.svg +0 -1
  311. package/icons/shopping-bag-outline.svg +0 -1
  312. package/icons/shopping-cart-outline.svg +0 -1
  313. package/icons/shuffle-2-outline.svg +0 -1
  314. package/icons/shuffle-outline.svg +0 -1
  315. package/icons/skip-back-outline.svg +0 -1
  316. package/icons/skip-forward-outline.svg +0 -1
  317. package/icons/slash-outline.svg +0 -1
  318. package/icons/smartphone-outline.svg +0 -1
  319. package/icons/smiling-face-outline.svg +0 -1
  320. package/icons/speaker-outline.svg +0 -1
  321. package/icons/square-outline.svg +0 -1
  322. package/icons/star-outline.svg +0 -1
  323. package/icons/stop-circle-outline.svg +0 -1
  324. package/icons/sun-outline.svg +0 -1
  325. package/icons/swap-outline.svg +0 -1
  326. package/icons/sync-outline.svg +0 -1
  327. package/icons/text-outline.svg +0 -1
  328. package/icons/thermometer-minus-outline.svg +0 -1
  329. package/icons/thermometer-outline.svg +0 -1
  330. package/icons/thermometer-plus-outline.svg +0 -1
  331. package/icons/toggle-left-outline.svg +0 -1
  332. package/icons/toggle-right-outline.svg +0 -1
  333. package/icons/trash-2-outline.svg +0 -1
  334. package/icons/trash-outline.svg +0 -1
  335. package/icons/trending-down-outline.svg +0 -1
  336. package/icons/trending-up-outline.svg +0 -1
  337. package/icons/tv-outline.svg +0 -1
  338. package/icons/twitter-outline.svg +0 -1
  339. package/icons/umbrella-outline.svg +0 -1
  340. package/icons/undo-outline.svg +0 -1
  341. package/icons/unlock-outline.svg +0 -1
  342. package/icons/upload-outline.svg +0 -1
  343. package/icons/video-off-outline.svg +0 -1
  344. package/icons/video-outline.svg +0 -1
  345. package/icons/volume-down-outline.svg +0 -1
  346. package/icons/volume-mute-outline.svg +0 -1
  347. package/icons/volume-off-outline.svg +0 -1
  348. package/icons/volume-up-outline.svg +0 -1
  349. package/icons/wifi-off-outline.svg +0 -1
  350. package/icons/wifi-outline.svg +0 -1
  351. package/js/index.js +0 -718
  352. package/js/index.ts +0 -873
@@ -1,366 +1,366 @@
1
- interface DropdownOption {
2
- label: string;
3
- value: string | number;
4
- }
5
-
6
- interface VirtualDropdownConfig {
7
- container: string | HTMLElement;
8
- options: DropdownOption[];
9
- multiSelect?: boolean;
10
- searchable?: boolean;
11
- placeholder?: string;
12
- renderLimit?: number;
13
- itemHeight?: number;
14
- onSelect?: (selectedValues: Array<string | number>) => void;
15
- }
16
-
17
- class VirtualDropdown {
18
- private readonly container: HTMLElement;
19
- private readonly options: DropdownOption[];
20
- private readonly multiSelect: boolean;
21
- private readonly searchable: boolean;
22
- private readonly placeholder: string;
23
- private readonly renderLimit: number;
24
- private readonly itemHeight: number;
25
- private readonly onSelect: ((selectedValues: Array<string | number>) => void) | null;
26
-
27
- private trigger!: HTMLElement;
28
- private triggerText!: HTMLElement;
29
- private menu!: HTMLElement;
30
- private listWrapper!: HTMLElement;
31
- private scroller!: HTMLElement;
32
- private spacer!: HTMLElement;
33
- private content!: HTMLElement;
34
- private searchInput?: HTMLInputElement;
35
-
36
- private selectedValues: Set<string | number>;
37
- private filteredOptions: DropdownOption[];
38
- private isOpen: boolean;
39
- private scrollTop: number;
40
-
41
- private boundHandlers: Map<string, EventListener>;
42
-
43
- constructor(config: VirtualDropdownConfig) {
44
- const containerElement = typeof config.container === 'string'
45
- ? document.querySelector<HTMLElement>(config.container)
46
- : config.container;
47
-
48
- if (!containerElement) {
49
- throw new Error('Container element not found');
50
- }
51
-
52
- this.container = containerElement;
53
- this.options = config.options || [];
54
- this.multiSelect = config.multiSelect ?? false;
55
- this.searchable = config.searchable ?? false;
56
- this.placeholder = config.placeholder || 'Select...';
57
- this.renderLimit = config.renderLimit || 20;
58
- this.itemHeight = config.itemHeight || 40;
59
- this.onSelect = config.onSelect ?? null;
60
-
61
- this.selectedValues = new Set();
62
- this.filteredOptions = [...this.options];
63
- this.isOpen = false;
64
- this.scrollTop = 0;
65
- this.boundHandlers = new Map();
66
-
67
- this.init();
68
- }
69
-
70
- private init(): void {
71
- this.container.classList.add('custom-dropdown');
72
- this.renderBase();
73
- this.bindEvents();
74
- this.updateTrigger();
75
- }
76
-
77
- private renderBase(): void {
78
- this.container.innerHTML = `
79
- <div class="dropdown-trigger" tabindex="0" role="button" aria-haspopup="listbox" aria-expanded="false">
80
- <span class="trigger-text">${this.escapeHtml(this.placeholder)}</span>
81
- <span class="trigger-arrow" aria-hidden="true">▼</span>
82
- </div>
83
- <div class="dropdown-menu" role="listbox">
84
- ${this.searchable ? '<div class="dropdown-search"><input type="text" placeholder="Search..." aria-label="Search options"></div>' : ''}
85
- <div class="dropdown-list-wrapper">
86
- <div class="dropdown-list-scroller">
87
- <div class="virtual-spacer"></div>
88
- <div class="virtual-content"></div>
89
- </div>
90
- </div>
91
- </div>
92
- `;
93
-
94
- this.trigger = this.querySelector('.dropdown-trigger');
95
- this.triggerText = this.querySelector('.trigger-text');
96
- this.menu = this.querySelector('.dropdown-menu');
97
- this.listWrapper = this.querySelector('.dropdown-list-wrapper');
98
- this.scroller = this.querySelector('.dropdown-list-scroller');
99
- this.spacer = this.querySelector('.virtual-spacer');
100
- this.content = this.querySelector('.virtual-content');
101
-
102
- if (this.searchable) {
103
- this.searchInput = this.querySelector('.dropdown-search input');
104
- }
105
- }
106
-
107
- private querySelector<T extends HTMLElement>(selector: string): T {
108
- const element = this.container.querySelector<T>(selector);
109
- if (!element) {
110
- throw new Error(`Required element not found: ${selector}`);
111
- }
112
- return element;
113
- }
114
-
115
- private bindEvents(): void {
116
- const handleTriggerClick = (): void => this.toggle();
117
- this.trigger.addEventListener('click', handleTriggerClick);
118
- this.boundHandlers.set('triggerClick', handleTriggerClick);
119
-
120
- const handleTriggerKeydown = (e: KeyboardEvent): void => {
121
- if (e.key === 'Enter' || e.key === ' ') {
122
- e.preventDefault();
123
- this.toggle();
124
- } else if (e.key === 'Escape' && this.isOpen) {
125
- this.close();
126
- }
127
- };
128
- this.trigger.addEventListener('keydown', handleTriggerKeydown as EventListener);
129
- this.boundHandlers.set('triggerKeydown', handleTriggerKeydown as EventListener);
130
-
131
- const handleDocumentClick = (e: MouseEvent): void => {
132
- if (!this.container.contains(e.target as Node)) {
133
- this.close();
134
- }
135
- };
136
- document.addEventListener('click', handleDocumentClick as EventListener);
137
- this.boundHandlers.set('documentClick', handleDocumentClick as EventListener);
138
-
139
- if (this.searchable && this.searchInput) {
140
- const handleSearchInput = (e: Event): void => {
141
- const target = e.target as HTMLInputElement;
142
- this.handleSearch(target.value);
143
- };
144
- this.searchInput.addEventListener('input', handleSearchInput);
145
- this.boundHandlers.set('searchInput', handleSearchInput);
146
- }
147
-
148
- const handleScroll = (e: Event): void => {
149
- const target = e.target as HTMLElement;
150
- this.scrollTop = target.scrollTop;
151
- this.renderList();
152
- };
153
- this.listWrapper.addEventListener('scroll', handleScroll);
154
- this.boundHandlers.set('scroll', handleScroll);
155
- }
156
-
157
- private toggle(): void {
158
- this.isOpen ? this.close() : this.open();
159
- }
160
-
161
- private open(): void {
162
- this.isOpen = true;
163
- this.container.classList.add('open');
164
- this.menu.classList.add('open');
165
- this.trigger.setAttribute('aria-expanded', 'true');
166
- this.renderList();
167
-
168
- if (this.searchable && this.searchInput) {
169
- this.searchInput.focus();
170
- }
171
- }
172
-
173
- private close(): void {
174
- this.isOpen = false;
175
- this.container.classList.remove('open');
176
- this.menu.classList.remove('open');
177
- this.trigger.setAttribute('aria-expanded', 'false');
178
- }
179
-
180
- private handleSearch(query: string): void {
181
- if (!query.trim()) {
182
- this.filteredOptions = [...this.options];
183
- } else {
184
- const lowerQuery = query.toLowerCase();
185
- this.filteredOptions = this.options.filter(opt =>
186
- opt.label.toLowerCase().includes(lowerQuery)
187
- );
188
- }
189
-
190
- this.listWrapper.scrollTop = 0;
191
- this.scrollTop = 0;
192
- this.renderList();
193
- }
194
-
195
- private renderList(): void {
196
- const totalHeight = this.filteredOptions.length * this.itemHeight;
197
- this.spacer.style.height = `${totalHeight}px`;
198
-
199
- const startIdx = Math.floor(this.scrollTop / this.itemHeight);
200
- const buffer = 5;
201
- const renderStart = Math.max(0, startIdx - buffer);
202
- const renderEnd = Math.min(
203
- this.filteredOptions.length,
204
- startIdx + this.renderLimit + buffer
205
- );
206
-
207
- const offsetY = renderStart * this.itemHeight;
208
- this.content.style.transform = `translateY(${offsetY}px)`;
209
-
210
- const visibleOptions = this.filteredOptions.slice(renderStart, renderEnd);
211
-
212
- this.content.innerHTML = visibleOptions
213
- .map((opt, idx) => {
214
- const realIdx = renderStart + idx;
215
- const isSelected = this.selectedValues.has(opt.value);
216
- return `
217
- <div class="dropdown-item ${isSelected ? 'selected' : ''}"
218
- data-value="${this.escapeHtml(String(opt.value))}"
219
- data-idx="${realIdx}"
220
- role="option"
221
- aria-selected="${isSelected}"
222
- tabindex="0"
223
- style="height: ${this.itemHeight}px; line-height: ${this.itemHeight}px;">
224
- ${this.multiSelect ? `<input type="checkbox" ${isSelected ? 'checked' : ''} tabindex="-1" aria-hidden="true">` : ''}
225
- <span class="item-label">${this.escapeHtml(opt.label)}</span>
226
- </div>
227
- `;
228
- })
229
- .join('');
230
-
231
- this.content.querySelectorAll('.dropdown-item').forEach(item => {
232
- const handleItemClick = (e: Event): void => {
233
- e.stopPropagation();
234
- const value = (item as HTMLElement).dataset.value;
235
- if (value) {
236
- this.handleSelect(value);
237
- }
238
- };
239
-
240
- const handleItemKeydown = (e: KeyboardEvent): void => {
241
- if (e.key === 'Enter' || e.key === ' ') {
242
- e.preventDefault();
243
- const value = (item as HTMLElement).dataset.value;
244
- if (value) {
245
- this.handleSelect(value);
246
- }
247
- }
248
- };
249
-
250
- item.addEventListener('click', handleItemClick);
251
- item.addEventListener('keydown', handleItemKeydown as EventListener);
252
- });
253
- }
254
-
255
- private handleSelect(valueString: string): void {
256
- const selectedOpt = this.filteredOptions.find(
257
- o => String(o.value) === valueString
258
- );
259
-
260
- if (!selectedOpt) return;
261
-
262
- const val = selectedOpt.value;
263
-
264
- if (this.multiSelect) {
265
- if (this.selectedValues.has(val)) {
266
- this.selectedValues.delete(val);
267
- } else {
268
- this.selectedValues.add(val);
269
- }
270
- this.renderList();
271
- } else {
272
- this.selectedValues.clear();
273
- this.selectedValues.add(val);
274
- this.close();
275
- }
276
-
277
- this.updateTrigger();
278
-
279
- if (this.onSelect) {
280
- this.onSelect(Array.from(this.selectedValues));
281
- }
282
- }
283
-
284
- private updateTrigger(): void {
285
- if (this.selectedValues.size === 0) {
286
- this.triggerText.textContent = this.placeholder;
287
- this.triggerText.classList.remove('has-value');
288
- } else {
289
- this.triggerText.classList.add('has-value');
290
-
291
- if (this.multiSelect) {
292
- const count = this.selectedValues.size;
293
- this.triggerText.textContent = `${count} item${count !== 1 ? 's' : ''} selected`;
294
- } else {
295
- const val = Array.from(this.selectedValues)[0];
296
- const opt = this.options.find(o => o.value === val);
297
- this.triggerText.textContent = opt ? opt.label : String(val);
298
- }
299
- }
300
- }
301
-
302
- private escapeHtml(text: string): string {
303
- const div = document.createElement('div');
304
- div.textContent = text;
305
- return div.innerHTML;
306
- }
307
-
308
- public getValue(): Array<string | number> {
309
- return Array.from(this.selectedValues);
310
- }
311
-
312
- public setValue(values: Array<string | number>): void {
313
- this.selectedValues.clear();
314
- values.forEach(val => {
315
- if (this.options.some(opt => opt.value === val)) {
316
- this.selectedValues.add(val);
317
- }
318
- });
319
- this.updateTrigger();
320
- if (this.isOpen) {
321
- this.renderList();
322
- }
323
- }
324
-
325
- public clearSelection(): void {
326
- this.selectedValues.clear();
327
- this.updateTrigger();
328
- if (this.isOpen) {
329
- this.renderList();
330
- }
331
- }
332
-
333
- public destroy(): void {
334
- const triggerClick = this.boundHandlers.get('triggerClick');
335
- if (triggerClick) {
336
- this.trigger.removeEventListener('click', triggerClick);
337
- }
338
-
339
- const triggerKeydown = this.boundHandlers.get('triggerKeydown');
340
- if (triggerKeydown) {
341
- this.trigger.removeEventListener('keydown', triggerKeydown);
342
- }
343
-
344
- const documentClick = this.boundHandlers.get('documentClick');
345
- if (documentClick) {
346
- document.removeEventListener('click', documentClick);
347
- }
348
-
349
- const searchInput = this.boundHandlers.get('searchInput');
350
- if (searchInput && this.searchInput) {
351
- this.searchInput.removeEventListener('input', searchInput);
352
- }
353
-
354
- const scroll = this.boundHandlers.get('scroll');
355
- if (scroll) {
356
- this.listWrapper.removeEventListener('scroll', scroll);
357
- }
358
-
359
- this.boundHandlers.clear();
360
-
361
- this.container.innerHTML = '';
362
- this.container.classList.remove('custom-dropdown', 'open');
363
- }
364
- }
365
-
1
+ interface DropdownOption {
2
+ label: string;
3
+ value: string | number;
4
+ }
5
+
6
+ interface VirtualDropdownConfig {
7
+ container: string | HTMLElement;
8
+ options: DropdownOption[];
9
+ multiSelect?: boolean;
10
+ searchable?: boolean;
11
+ placeholder?: string;
12
+ renderLimit?: number;
13
+ itemHeight?: number;
14
+ onSelect?: (selectedValues: Array<string | number>) => void;
15
+ }
16
+
17
+ class VirtualDropdown {
18
+ private readonly container: HTMLElement;
19
+ private readonly options: DropdownOption[];
20
+ private readonly multiSelect: boolean;
21
+ private readonly searchable: boolean;
22
+ private readonly placeholder: string;
23
+ private readonly renderLimit: number;
24
+ private readonly itemHeight: number;
25
+ private readonly onSelect: ((selectedValues: Array<string | number>) => void) | null;
26
+
27
+ private trigger!: HTMLElement;
28
+ private triggerText!: HTMLElement;
29
+ private menu!: HTMLElement;
30
+ private listWrapper!: HTMLElement;
31
+ private scroller!: HTMLElement;
32
+ private spacer!: HTMLElement;
33
+ private content!: HTMLElement;
34
+ private searchInput?: HTMLInputElement;
35
+
36
+ private selectedValues: Set<string | number>;
37
+ private filteredOptions: DropdownOption[];
38
+ private isOpen: boolean;
39
+ private scrollTop: number;
40
+
41
+ private boundHandlers: Map<string, EventListener>;
42
+
43
+ constructor(config: VirtualDropdownConfig) {
44
+ const containerElement = typeof config.container === 'string'
45
+ ? document.querySelector<HTMLElement>(config.container)
46
+ : config.container;
47
+
48
+ if (!containerElement) {
49
+ throw new Error('Container element not found');
50
+ }
51
+
52
+ this.container = containerElement;
53
+ this.options = config.options || [];
54
+ this.multiSelect = config.multiSelect ?? false;
55
+ this.searchable = config.searchable ?? false;
56
+ this.placeholder = config.placeholder || 'Select...';
57
+ this.renderLimit = config.renderLimit || 20;
58
+ this.itemHeight = config.itemHeight || 40;
59
+ this.onSelect = config.onSelect ?? null;
60
+
61
+ this.selectedValues = new Set();
62
+ this.filteredOptions = [...this.options];
63
+ this.isOpen = false;
64
+ this.scrollTop = 0;
65
+ this.boundHandlers = new Map();
66
+
67
+ this.init();
68
+ }
69
+
70
+ private init(): void {
71
+ this.container.classList.add('custom-dropdown');
72
+ this.renderBase();
73
+ this.bindEvents();
74
+ this.updateTrigger();
75
+ }
76
+
77
+ private renderBase(): void {
78
+ this.container.innerHTML = `
79
+ <div class="dropdown-trigger" tabindex="0" role="button" aria-haspopup="listbox" aria-expanded="false">
80
+ <span class="trigger-text">${this.escapeHtml(this.placeholder)}</span>
81
+ <span class="trigger-arrow" aria-hidden="true">▼</span>
82
+ </div>
83
+ <div class="dropdown-menu" role="listbox">
84
+ ${this.searchable ? '<div class="dropdown-search"><input type="text" placeholder="Search..." aria-label="Search options"></div>' : ''}
85
+ <div class="dropdown-list-wrapper">
86
+ <div class="dropdown-list-scroller">
87
+ <div class="virtual-spacer"></div>
88
+ <div class="virtual-content"></div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ `;
93
+
94
+ this.trigger = this.querySelector('.dropdown-trigger');
95
+ this.triggerText = this.querySelector('.trigger-text');
96
+ this.menu = this.querySelector('.dropdown-menu');
97
+ this.listWrapper = this.querySelector('.dropdown-list-wrapper');
98
+ this.scroller = this.querySelector('.dropdown-list-scroller');
99
+ this.spacer = this.querySelector('.virtual-spacer');
100
+ this.content = this.querySelector('.virtual-content');
101
+
102
+ if (this.searchable) {
103
+ this.searchInput = this.querySelector('.dropdown-search input');
104
+ }
105
+ }
106
+
107
+ private querySelector<T extends HTMLElement>(selector: string): T {
108
+ const element = this.container.querySelector<T>(selector);
109
+ if (!element) {
110
+ throw new Error(`Required element not found: ${selector}`);
111
+ }
112
+ return element;
113
+ }
114
+
115
+ private bindEvents(): void {
116
+ const handleTriggerClick = (): void => this.toggle();
117
+ this.trigger.addEventListener('click', handleTriggerClick);
118
+ this.boundHandlers.set('triggerClick', handleTriggerClick);
119
+
120
+ const handleTriggerKeydown = (e: KeyboardEvent): void => {
121
+ if (e.key === 'Enter' || e.key === ' ') {
122
+ e.preventDefault();
123
+ this.toggle();
124
+ } else if (e.key === 'Escape' && this.isOpen) {
125
+ this.close();
126
+ }
127
+ };
128
+ this.trigger.addEventListener('keydown', handleTriggerKeydown as EventListener);
129
+ this.boundHandlers.set('triggerKeydown', handleTriggerKeydown as EventListener);
130
+
131
+ const handleDocumentClick = (e: MouseEvent): void => {
132
+ if (!this.container.contains(e.target as Node)) {
133
+ this.close();
134
+ }
135
+ };
136
+ document.addEventListener('click', handleDocumentClick as EventListener);
137
+ this.boundHandlers.set('documentClick', handleDocumentClick as EventListener);
138
+
139
+ if (this.searchable && this.searchInput) {
140
+ const handleSearchInput = (e: Event): void => {
141
+ const target = e.target as HTMLInputElement;
142
+ this.handleSearch(target.value);
143
+ };
144
+ this.searchInput.addEventListener('input', handleSearchInput);
145
+ this.boundHandlers.set('searchInput', handleSearchInput);
146
+ }
147
+
148
+ const handleScroll = (e: Event): void => {
149
+ const target = e.target as HTMLElement;
150
+ this.scrollTop = target.scrollTop;
151
+ this.renderList();
152
+ };
153
+ this.listWrapper.addEventListener('scroll', handleScroll);
154
+ this.boundHandlers.set('scroll', handleScroll);
155
+ }
156
+
157
+ private toggle(): void {
158
+ this.isOpen ? this.close() : this.open();
159
+ }
160
+
161
+ private open(): void {
162
+ this.isOpen = true;
163
+ this.container.classList.add('open');
164
+ this.menu.classList.add('open');
165
+ this.trigger.setAttribute('aria-expanded', 'true');
166
+ this.renderList();
167
+
168
+ if (this.searchable && this.searchInput) {
169
+ this.searchInput.focus();
170
+ }
171
+ }
172
+
173
+ private close(): void {
174
+ this.isOpen = false;
175
+ this.container.classList.remove('open');
176
+ this.menu.classList.remove('open');
177
+ this.trigger.setAttribute('aria-expanded', 'false');
178
+ }
179
+
180
+ private handleSearch(query: string): void {
181
+ if (!query.trim()) {
182
+ this.filteredOptions = [...this.options];
183
+ } else {
184
+ const lowerQuery = query.toLowerCase();
185
+ this.filteredOptions = this.options.filter(opt =>
186
+ opt.label.toLowerCase().includes(lowerQuery)
187
+ );
188
+ }
189
+
190
+ this.listWrapper.scrollTop = 0;
191
+ this.scrollTop = 0;
192
+ this.renderList();
193
+ }
194
+
195
+ private renderList(): void {
196
+ const totalHeight = this.filteredOptions.length * this.itemHeight;
197
+ this.spacer.style.height = `${totalHeight}px`;
198
+
199
+ const startIdx = Math.floor(this.scrollTop / this.itemHeight);
200
+ const buffer = 5;
201
+ const renderStart = Math.max(0, startIdx - buffer);
202
+ const renderEnd = Math.min(
203
+ this.filteredOptions.length,
204
+ startIdx + this.renderLimit + buffer
205
+ );
206
+
207
+ const offsetY = renderStart * this.itemHeight;
208
+ this.content.style.transform = `translateY(${offsetY}px)`;
209
+
210
+ const visibleOptions = this.filteredOptions.slice(renderStart, renderEnd);
211
+
212
+ this.content.innerHTML = visibleOptions
213
+ .map((opt, idx) => {
214
+ const realIdx = renderStart + idx;
215
+ const isSelected = this.selectedValues.has(opt.value);
216
+ return `
217
+ <div class="dropdown-item ${isSelected ? 'selected' : ''}"
218
+ data-value="${this.escapeHtml(String(opt.value))}"
219
+ data-idx="${realIdx}"
220
+ role="option"
221
+ aria-selected="${isSelected}"
222
+ tabindex="0"
223
+ style="height: ${this.itemHeight}px; line-height: ${this.itemHeight}px;">
224
+ ${this.multiSelect ? `<input type="checkbox" ${isSelected ? 'checked' : ''} tabindex="-1" aria-hidden="true">` : ''}
225
+ <span class="item-label">${this.escapeHtml(opt.label)}</span>
226
+ </div>
227
+ `;
228
+ })
229
+ .join('');
230
+
231
+ this.content.querySelectorAll('.dropdown-item').forEach(item => {
232
+ const handleItemClick = (e: Event): void => {
233
+ e.stopPropagation();
234
+ const value = (item as HTMLElement).dataset.value;
235
+ if (value) {
236
+ this.handleSelect(value);
237
+ }
238
+ };
239
+
240
+ const handleItemKeydown = (e: KeyboardEvent): void => {
241
+ if (e.key === 'Enter' || e.key === ' ') {
242
+ e.preventDefault();
243
+ const value = (item as HTMLElement).dataset.value;
244
+ if (value) {
245
+ this.handleSelect(value);
246
+ }
247
+ }
248
+ };
249
+
250
+ item.addEventListener('click', handleItemClick);
251
+ item.addEventListener('keydown', handleItemKeydown as EventListener);
252
+ });
253
+ }
254
+
255
+ private handleSelect(valueString: string): void {
256
+ const selectedOpt = this.filteredOptions.find(
257
+ o => String(o.value) === valueString
258
+ );
259
+
260
+ if (!selectedOpt) return;
261
+
262
+ const val = selectedOpt.value;
263
+
264
+ if (this.multiSelect) {
265
+ if (this.selectedValues.has(val)) {
266
+ this.selectedValues.delete(val);
267
+ } else {
268
+ this.selectedValues.add(val);
269
+ }
270
+ this.renderList();
271
+ } else {
272
+ this.selectedValues.clear();
273
+ this.selectedValues.add(val);
274
+ this.close();
275
+ }
276
+
277
+ this.updateTrigger();
278
+
279
+ if (this.onSelect) {
280
+ this.onSelect(Array.from(this.selectedValues));
281
+ }
282
+ }
283
+
284
+ private updateTrigger(): void {
285
+ if (this.selectedValues.size === 0) {
286
+ this.triggerText.textContent = this.placeholder;
287
+ this.triggerText.classList.remove('has-value');
288
+ } else {
289
+ this.triggerText.classList.add('has-value');
290
+
291
+ if (this.multiSelect) {
292
+ const count = this.selectedValues.size;
293
+ this.triggerText.textContent = `${count} item${count !== 1 ? 's' : ''} selected`;
294
+ } else {
295
+ const val = Array.from(this.selectedValues)[0];
296
+ const opt = this.options.find(o => o.value === val);
297
+ this.triggerText.textContent = opt ? opt.label : String(val);
298
+ }
299
+ }
300
+ }
301
+
302
+ private escapeHtml(text: string): string {
303
+ const div = document.createElement('div');
304
+ div.textContent = text;
305
+ return div.innerHTML;
306
+ }
307
+
308
+ public getValue(): Array<string | number> {
309
+ return Array.from(this.selectedValues);
310
+ }
311
+
312
+ public setValue(values: Array<string | number>): void {
313
+ this.selectedValues.clear();
314
+ values.forEach(val => {
315
+ if (this.options.some(opt => opt.value === val)) {
316
+ this.selectedValues.add(val);
317
+ }
318
+ });
319
+ this.updateTrigger();
320
+ if (this.isOpen) {
321
+ this.renderList();
322
+ }
323
+ }
324
+
325
+ public clearSelection(): void {
326
+ this.selectedValues.clear();
327
+ this.updateTrigger();
328
+ if (this.isOpen) {
329
+ this.renderList();
330
+ }
331
+ }
332
+
333
+ public destroy(): void {
334
+ const triggerClick = this.boundHandlers.get('triggerClick');
335
+ if (triggerClick) {
336
+ this.trigger.removeEventListener('click', triggerClick);
337
+ }
338
+
339
+ const triggerKeydown = this.boundHandlers.get('triggerKeydown');
340
+ if (triggerKeydown) {
341
+ this.trigger.removeEventListener('keydown', triggerKeydown);
342
+ }
343
+
344
+ const documentClick = this.boundHandlers.get('documentClick');
345
+ if (documentClick) {
346
+ document.removeEventListener('click', documentClick);
347
+ }
348
+
349
+ const searchInput = this.boundHandlers.get('searchInput');
350
+ if (searchInput && this.searchInput) {
351
+ this.searchInput.removeEventListener('input', searchInput);
352
+ }
353
+
354
+ const scroll = this.boundHandlers.get('scroll');
355
+ if (scroll) {
356
+ this.listWrapper.removeEventListener('scroll', scroll);
357
+ }
358
+
359
+ this.boundHandlers.clear();
360
+
361
+ this.container.innerHTML = '';
362
+ this.container.classList.remove('custom-dropdown', 'open');
363
+ }
364
+ }
365
+
366
366
  export { VirtualDropdown, DropdownOption, VirtualDropdownConfig };