@keenthemes/ktui 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +88 -0
- package/LICENSE.md +21 -0
- package/README.md +124 -0
- package/dist/ktui.js +19201 -0
- package/dist/ktui.min.js +2 -0
- package/dist/ktui.min.js.map +1 -0
- package/lib/cjs/components/accordion/accordion.js +168 -0
- package/lib/cjs/components/accordion/accordion.js.map +1 -0
- package/lib/cjs/components/accordion/index.js +6 -0
- package/lib/cjs/components/accordion/index.js.map +1 -0
- package/lib/cjs/components/accordion/types.js +3 -0
- package/lib/cjs/components/accordion/types.js.map +1 -0
- package/lib/cjs/components/collapse/collapse.js +169 -0
- package/lib/cjs/components/collapse/collapse.js.map +1 -0
- package/lib/cjs/components/collapse/index.js +6 -0
- package/lib/cjs/components/collapse/index.js.map +1 -0
- package/lib/cjs/components/collapse/types.js +3 -0
- package/lib/cjs/components/collapse/types.js.map +1 -0
- package/lib/cjs/components/component.js +135 -0
- package/lib/cjs/components/component.js.map +1 -0
- package/lib/cjs/components/config.js +26 -0
- package/lib/cjs/components/config.js.map +1 -0
- package/lib/cjs/components/config.umd.js +23 -0
- package/lib/cjs/components/config.umd.js.map +1 -0
- package/lib/cjs/components/constants.js +15 -0
- package/lib/cjs/components/constants.js.map +1 -0
- package/lib/cjs/components/datatable/datatable.js +1464 -0
- package/lib/cjs/components/datatable/datatable.js.map +1 -0
- package/lib/cjs/components/datatable/index.js +6 -0
- package/lib/cjs/components/datatable/index.js.map +1 -0
- package/lib/cjs/components/datatable/types.js +3 -0
- package/lib/cjs/components/datatable/types.js.map +1 -0
- package/lib/cjs/components/dismiss/dismiss.js +131 -0
- package/lib/cjs/components/dismiss/dismiss.js.map +1 -0
- package/lib/cjs/components/dismiss/index.js +6 -0
- package/lib/cjs/components/dismiss/index.js.map +1 -0
- package/lib/cjs/components/dismiss/types.js +3 -0
- package/lib/cjs/components/dismiss/types.js.map +1 -0
- package/lib/cjs/components/drawer/drawer.js +347 -0
- package/lib/cjs/components/drawer/drawer.js.map +1 -0
- package/lib/cjs/components/drawer/index.js +6 -0
- package/lib/cjs/components/drawer/index.js.map +1 -0
- package/lib/cjs/components/drawer/types.js +3 -0
- package/lib/cjs/components/drawer/types.js.map +1 -0
- package/lib/cjs/components/dropdown/dropdown.js +403 -0
- package/lib/cjs/components/dropdown/dropdown.js.map +1 -0
- package/lib/cjs/components/dropdown/index.js +6 -0
- package/lib/cjs/components/dropdown/index.js.map +1 -0
- package/lib/cjs/components/dropdown/types.js +3 -0
- package/lib/cjs/components/dropdown/types.js.map +1 -0
- package/lib/cjs/components/image-input/image-input.js +191 -0
- package/lib/cjs/components/image-input/image-input.js.map +1 -0
- package/lib/cjs/components/image-input/index.js +6 -0
- package/lib/cjs/components/image-input/index.js.map +1 -0
- package/lib/cjs/components/image-input/types.js +3 -0
- package/lib/cjs/components/image-input/types.js.map +1 -0
- package/lib/cjs/components/menu/index.js +6 -0
- package/lib/cjs/components/menu/index.js.map +1 -0
- package/lib/cjs/components/menu/menu.js +1021 -0
- package/lib/cjs/components/menu/menu.js.map +1 -0
- package/lib/cjs/components/menu/types.js +3 -0
- package/lib/cjs/components/menu/types.js.map +1 -0
- package/lib/cjs/components/modal/index.js +6 -0
- package/lib/cjs/components/modal/index.js.map +1 -0
- package/lib/cjs/components/modal/modal.js +316 -0
- package/lib/cjs/components/modal/modal.js.map +1 -0
- package/lib/cjs/components/modal/types.js +3 -0
- package/lib/cjs/components/modal/types.js.map +1 -0
- package/lib/cjs/components/reparent/index.js +6 -0
- package/lib/cjs/components/reparent/index.js.map +1 -0
- package/lib/cjs/components/reparent/reparent.js +93 -0
- package/lib/cjs/components/reparent/reparent.js.map +1 -0
- package/lib/cjs/components/reparent/types.js +3 -0
- package/lib/cjs/components/reparent/types.js.map +1 -0
- package/lib/cjs/components/scrollable/index.js +6 -0
- package/lib/cjs/components/scrollable/index.js.map +1 -0
- package/lib/cjs/components/scrollable/scrollable.js +259 -0
- package/lib/cjs/components/scrollable/scrollable.js.map +1 -0
- package/lib/cjs/components/scrollable/types.js +3 -0
- package/lib/cjs/components/scrollable/types.js.map +1 -0
- package/lib/cjs/components/scrollspy/index.js +6 -0
- package/lib/cjs/components/scrollspy/index.js.map +1 -0
- package/lib/cjs/components/scrollspy/scrollspy.js +174 -0
- package/lib/cjs/components/scrollspy/scrollspy.js.map +1 -0
- package/lib/cjs/components/scrollspy/types.js +3 -0
- package/lib/cjs/components/scrollspy/types.js.map +1 -0
- package/lib/cjs/components/scrollto/index.js +6 -0
- package/lib/cjs/components/scrollto/index.js.map +1 -0
- package/lib/cjs/components/scrollto/scrollto.js +103 -0
- package/lib/cjs/components/scrollto/scrollto.js.map +1 -0
- package/lib/cjs/components/scrollto/types.js +3 -0
- package/lib/cjs/components/scrollto/types.js.map +1 -0
- package/lib/cjs/components/stepper/index.js +6 -0
- package/lib/cjs/components/stepper/index.js.map +1 -0
- package/lib/cjs/components/stepper/stepper.js +258 -0
- package/lib/cjs/components/stepper/stepper.js.map +1 -0
- package/lib/cjs/components/stepper/types.js +3 -0
- package/lib/cjs/components/stepper/types.js.map +1 -0
- package/lib/cjs/components/sticky/index.js +6 -0
- package/lib/cjs/components/sticky/index.js.map +1 -0
- package/lib/cjs/components/sticky/sticky.js +297 -0
- package/lib/cjs/components/sticky/sticky.js.map +1 -0
- package/lib/cjs/components/sticky/types.js +3 -0
- package/lib/cjs/components/sticky/types.js.map +1 -0
- package/lib/cjs/components/tabs/index.js +6 -0
- package/lib/cjs/components/tabs/index.js.map +1 -0
- package/lib/cjs/components/tabs/tabs.js +146 -0
- package/lib/cjs/components/tabs/tabs.js.map +1 -0
- package/lib/cjs/components/tabs/types.js +3 -0
- package/lib/cjs/components/tabs/types.js.map +1 -0
- package/lib/cjs/components/theme/index.js +6 -0
- package/lib/cjs/components/theme/index.js.map +1 -0
- package/lib/cjs/components/theme/theme.js +147 -0
- package/lib/cjs/components/theme/theme.js.map +1 -0
- package/lib/cjs/components/theme/types.js +3 -0
- package/lib/cjs/components/theme/types.js.map +1 -0
- package/lib/cjs/components/toggle/index.js +6 -0
- package/lib/cjs/components/toggle/index.js.map +1 -0
- package/lib/cjs/components/toggle/toggle.js +139 -0
- package/lib/cjs/components/toggle/toggle.js.map +1 -0
- package/lib/cjs/components/toggle/types.js +3 -0
- package/lib/cjs/components/toggle/types.js.map +1 -0
- package/lib/cjs/components/toggle-password/index.js +6 -0
- package/lib/cjs/components/toggle-password/index.js.map +1 -0
- package/lib/cjs/components/toggle-password/toggle-password.js +131 -0
- package/lib/cjs/components/toggle-password/toggle-password.js.map +1 -0
- package/lib/cjs/components/toggle-password/types.js +3 -0
- package/lib/cjs/components/toggle-password/types.js.map +1 -0
- package/lib/cjs/components/tooltip/index.js +6 -0
- package/lib/cjs/components/tooltip/index.js.map +1 -0
- package/lib/cjs/components/tooltip/tooltip.js +271 -0
- package/lib/cjs/components/tooltip/tooltip.js.map +1 -0
- package/lib/cjs/components/tooltip/types.js +3 -0
- package/lib/cjs/components/tooltip/types.js.map +1 -0
- package/lib/cjs/helpers/data.js +33 -0
- package/lib/cjs/helpers/data.js.map +1 -0
- package/lib/cjs/helpers/dom.js +297 -0
- package/lib/cjs/helpers/dom.js.map +1 -0
- package/lib/cjs/helpers/event-handler.js +36 -0
- package/lib/cjs/helpers/event-handler.js.map +1 -0
- package/lib/cjs/helpers/utils.js +94 -0
- package/lib/cjs/helpers/utils.js.map +1 -0
- package/lib/cjs/index.js +105 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/types.js +3 -0
- package/lib/cjs/types.js.map +1 -0
- package/lib/esm/components/accordion/accordion.js +165 -0
- package/lib/esm/components/accordion/accordion.js.map +1 -0
- package/lib/esm/components/accordion/index.js +2 -0
- package/lib/esm/components/accordion/index.js.map +1 -0
- package/lib/esm/components/accordion/types.js +2 -0
- package/lib/esm/components/accordion/types.js.map +1 -0
- package/lib/esm/components/collapse/collapse.js +166 -0
- package/lib/esm/components/collapse/collapse.js.map +1 -0
- package/lib/esm/components/collapse/index.js +2 -0
- package/lib/esm/components/collapse/index.js.map +1 -0
- package/lib/esm/components/collapse/types.js +2 -0
- package/lib/esm/components/collapse/types.js.map +1 -0
- package/lib/esm/components/component.js +133 -0
- package/lib/esm/components/component.js.map +1 -0
- package/lib/esm/components/config.js +24 -0
- package/lib/esm/components/config.js.map +1 -0
- package/lib/esm/components/config.umd.js +23 -0
- package/lib/esm/components/config.umd.js.map +1 -0
- package/lib/esm/components/constants.js +12 -0
- package/lib/esm/components/constants.js.map +1 -0
- package/lib/esm/components/datatable/datatable.js +1461 -0
- package/lib/esm/components/datatable/datatable.js.map +1 -0
- package/lib/esm/components/datatable/index.js +2 -0
- package/lib/esm/components/datatable/index.js.map +1 -0
- package/lib/esm/components/datatable/types.js +2 -0
- package/lib/esm/components/datatable/types.js.map +1 -0
- package/lib/esm/components/dismiss/dismiss.js +128 -0
- package/lib/esm/components/dismiss/dismiss.js.map +1 -0
- package/lib/esm/components/dismiss/index.js +2 -0
- package/lib/esm/components/dismiss/index.js.map +1 -0
- package/lib/esm/components/dismiss/types.js +2 -0
- package/lib/esm/components/dismiss/types.js.map +1 -0
- package/lib/esm/components/drawer/drawer.js +344 -0
- package/lib/esm/components/drawer/drawer.js.map +1 -0
- package/lib/esm/components/drawer/index.js +2 -0
- package/lib/esm/components/drawer/index.js.map +1 -0
- package/lib/esm/components/drawer/types.js +2 -0
- package/lib/esm/components/drawer/types.js.map +1 -0
- package/lib/esm/components/dropdown/dropdown.js +400 -0
- package/lib/esm/components/dropdown/dropdown.js.map +1 -0
- package/lib/esm/components/dropdown/index.js +2 -0
- package/lib/esm/components/dropdown/index.js.map +1 -0
- package/lib/esm/components/dropdown/types.js +2 -0
- package/lib/esm/components/dropdown/types.js.map +1 -0
- package/lib/esm/components/image-input/image-input.js +188 -0
- package/lib/esm/components/image-input/image-input.js.map +1 -0
- package/lib/esm/components/image-input/index.js +2 -0
- package/lib/esm/components/image-input/index.js.map +1 -0
- package/lib/esm/components/image-input/types.js +2 -0
- package/lib/esm/components/image-input/types.js.map +1 -0
- package/lib/esm/components/menu/index.js +2 -0
- package/lib/esm/components/menu/index.js.map +1 -0
- package/lib/esm/components/menu/menu.js +1018 -0
- package/lib/esm/components/menu/menu.js.map +1 -0
- package/lib/esm/components/menu/types.js +2 -0
- package/lib/esm/components/menu/types.js.map +1 -0
- package/lib/esm/components/modal/index.js +2 -0
- package/lib/esm/components/modal/index.js.map +1 -0
- package/lib/esm/components/modal/modal.js +313 -0
- package/lib/esm/components/modal/modal.js.map +1 -0
- package/lib/esm/components/modal/types.js +2 -0
- package/lib/esm/components/modal/types.js.map +1 -0
- package/lib/esm/components/reparent/index.js +2 -0
- package/lib/esm/components/reparent/index.js.map +1 -0
- package/lib/esm/components/reparent/reparent.js +90 -0
- package/lib/esm/components/reparent/reparent.js.map +1 -0
- package/lib/esm/components/reparent/types.js +2 -0
- package/lib/esm/components/reparent/types.js.map +1 -0
- package/lib/esm/components/scrollable/index.js +2 -0
- package/lib/esm/components/scrollable/index.js.map +1 -0
- package/lib/esm/components/scrollable/scrollable.js +256 -0
- package/lib/esm/components/scrollable/scrollable.js.map +1 -0
- package/lib/esm/components/scrollable/types.js +2 -0
- package/lib/esm/components/scrollable/types.js.map +1 -0
- package/lib/esm/components/scrollspy/index.js +2 -0
- package/lib/esm/components/scrollspy/index.js.map +1 -0
- package/lib/esm/components/scrollspy/scrollspy.js +171 -0
- package/lib/esm/components/scrollspy/scrollspy.js.map +1 -0
- package/lib/esm/components/scrollspy/types.js +2 -0
- package/lib/esm/components/scrollspy/types.js.map +1 -0
- package/lib/esm/components/scrollto/index.js +2 -0
- package/lib/esm/components/scrollto/index.js.map +1 -0
- package/lib/esm/components/scrollto/scrollto.js +100 -0
- package/lib/esm/components/scrollto/scrollto.js.map +1 -0
- package/lib/esm/components/scrollto/types.js +2 -0
- package/lib/esm/components/scrollto/types.js.map +1 -0
- package/lib/esm/components/stepper/index.js +2 -0
- package/lib/esm/components/stepper/index.js.map +1 -0
- package/lib/esm/components/stepper/stepper.js +255 -0
- package/lib/esm/components/stepper/stepper.js.map +1 -0
- package/lib/esm/components/stepper/types.js +2 -0
- package/lib/esm/components/stepper/types.js.map +1 -0
- package/lib/esm/components/sticky/index.js +2 -0
- package/lib/esm/components/sticky/index.js.map +1 -0
- package/lib/esm/components/sticky/sticky.js +294 -0
- package/lib/esm/components/sticky/sticky.js.map +1 -0
- package/lib/esm/components/sticky/types.js +2 -0
- package/lib/esm/components/sticky/types.js.map +1 -0
- package/lib/esm/components/tabs/index.js +2 -0
- package/lib/esm/components/tabs/index.js.map +1 -0
- package/lib/esm/components/tabs/tabs.js +143 -0
- package/lib/esm/components/tabs/tabs.js.map +1 -0
- package/lib/esm/components/tabs/types.js +2 -0
- package/lib/esm/components/tabs/types.js.map +1 -0
- package/lib/esm/components/theme/index.js +2 -0
- package/lib/esm/components/theme/index.js.map +1 -0
- package/lib/esm/components/theme/theme.js +144 -0
- package/lib/esm/components/theme/theme.js.map +1 -0
- package/lib/esm/components/theme/types.js +2 -0
- package/lib/esm/components/theme/types.js.map +1 -0
- package/lib/esm/components/toggle/index.js +2 -0
- package/lib/esm/components/toggle/index.js.map +1 -0
- package/lib/esm/components/toggle/toggle.js +136 -0
- package/lib/esm/components/toggle/toggle.js.map +1 -0
- package/lib/esm/components/toggle/types.js +2 -0
- package/lib/esm/components/toggle/types.js.map +1 -0
- package/lib/esm/components/toggle-password/index.js +2 -0
- package/lib/esm/components/toggle-password/index.js.map +1 -0
- package/lib/esm/components/toggle-password/toggle-password.js +128 -0
- package/lib/esm/components/toggle-password/toggle-password.js.map +1 -0
- package/lib/esm/components/toggle-password/types.js +2 -0
- package/lib/esm/components/toggle-password/types.js.map +1 -0
- package/lib/esm/components/tooltip/index.js +2 -0
- package/lib/esm/components/tooltip/index.js.map +1 -0
- package/lib/esm/components/tooltip/tooltip.js +268 -0
- package/lib/esm/components/tooltip/tooltip.js.map +1 -0
- package/lib/esm/components/tooltip/types.js +2 -0
- package/lib/esm/components/tooltip/types.js.map +1 -0
- package/lib/esm/helpers/data.js +31 -0
- package/lib/esm/helpers/data.js.map +1 -0
- package/lib/esm/helpers/dom.js +295 -0
- package/lib/esm/helpers/dom.js.map +1 -0
- package/lib/esm/helpers/event-handler.js +34 -0
- package/lib/esm/helpers/event-handler.js.map +1 -0
- package/lib/esm/helpers/utils.js +92 -0
- package/lib/esm/helpers/utils.js.map +1 -0
- package/lib/esm/index.js +79 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/types.js +2 -0
- package/lib/esm/types.js.map +1 -0
- package/package.json +85 -0
- package/prettier.config.js +9 -0
- package/src/components/accordion/accordion-menu.css +51 -0
- package/src/components/accordion/accordion.css +86 -0
- package/src/components/accordion/accordion.ts +221 -0
- package/src/components/accordion/index.ts +7 -0
- package/src/components/accordion/types.ts +16 -0
- package/src/components/alert/alert.css +282 -0
- package/src/components/avatar/avatar.css +46 -0
- package/src/components/badge/badge.css +176 -0
- package/src/components/breadcrumb/breadcrumb.css +38 -0
- package/src/components/btn/btn.css +227 -0
- package/src/components/card/card.css +158 -0
- package/src/components/checkbox/checkbox.css +74 -0
- package/src/components/collapse/collapse.css +14 -0
- package/src/components/collapse/collapse.ts +200 -0
- package/src/components/collapse/index.ts +7 -0
- package/src/components/collapse/types.ts +16 -0
- package/src/components/component.ts +132 -0
- package/src/components/constants.ts +16 -0
- package/src/components/datatable/datatable-checkbox.ts +236 -0
- package/src/components/datatable/datatable-sort.ts +154 -0
- package/src/components/datatable/datatable.css +110 -0
- package/src/components/datatable/datatable.ts +1657 -0
- package/src/components/datatable/index.ts +19 -0
- package/src/components/datatable/types.ts +203 -0
- package/src/components/datepicker/calendar.ts +1397 -0
- package/src/components/datepicker/config.ts +368 -0
- package/src/components/datepicker/datepicker.css +7 -0
- package/src/components/datepicker/datepicker.ts +1287 -0
- package/src/components/datepicker/dropdown.ts +757 -0
- package/src/components/datepicker/events.ts +149 -0
- package/src/components/datepicker/index.ts +10 -0
- package/src/components/datepicker/keyboard.ts +646 -0
- package/src/components/datepicker/locales.ts +80 -0
- package/src/components/datepicker/templates.ts +792 -0
- package/src/components/datepicker/types.ts +154 -0
- package/src/components/datepicker/utils.ts +631 -0
- package/src/components/dismiss/dismiss.css +10 -0
- package/src/components/dismiss/dismiss.ts +152 -0
- package/src/components/dismiss/index.ts +7 -0
- package/src/components/dismiss/types.ts +17 -0
- package/src/components/drawer/drawer.css +97 -0
- package/src/components/drawer/drawer.ts +437 -0
- package/src/components/drawer/index.ts +7 -0
- package/src/components/drawer/types.ts +25 -0
- package/src/components/dropdown/dropdown-menu.css +56 -0
- package/src/components/dropdown/dropdown.css +46 -0
- package/src/components/dropdown/dropdown.ts +549 -0
- package/src/components/dropdown/index.ts +7 -0
- package/src/components/dropdown/types.ts +28 -0
- package/src/components/form/form.css +54 -0
- package/src/components/image-input/image-input.css +56 -0
- package/src/components/image-input/image-input.ts +249 -0
- package/src/components/image-input/index.ts +10 -0
- package/src/components/image-input/types.ts +12 -0
- package/src/components/input/input-group.css +42 -0
- package/src/components/input/input.css +136 -0
- package/src/components/kbd/kbd.css +30 -0
- package/src/components/label/label.css +20 -0
- package/src/components/link/link.css +81 -0
- package/src/components/modal/index.ts +7 -0
- package/src/components/modal/modal.css +73 -0
- package/src/components/modal/modal.ts +382 -0
- package/src/components/modal/types.ts +21 -0
- package/src/components/pagination/pagination.css +26 -0
- package/src/components/popover/popover.css +22 -0
- package/src/components/progress/progress.css +51 -0
- package/src/components/radio/radio.css +51 -0
- package/src/components/reparent/index.ts +7 -0
- package/src/components/reparent/reparent.ts +109 -0
- package/src/components/reparent/types.ts +15 -0
- package/src/components/scrollable/index.ts +10 -0
- package/src/components/scrollable/scrollable.css +29 -0
- package/src/components/scrollable/scrollable.ts +297 -0
- package/src/components/scrollable/types.ts +16 -0
- package/src/components/scrollspy/index.ts +7 -0
- package/src/components/scrollspy/scrollspy.css +13 -0
- package/src/components/scrollspy/scrollspy.ts +224 -0
- package/src/components/scrollspy/types.ts +15 -0
- package/src/components/scrollto/index.ts +7 -0
- package/src/components/scrollto/scrollto.ts +127 -0
- package/src/components/scrollto/types.ts +15 -0
- package/src/components/select/combobox.ts +305 -0
- package/src/components/select/config.ts +324 -0
- package/src/components/select/dropdown.ts +510 -0
- package/src/components/select/index.ts +13 -0
- package/src/components/select/option.ts +43 -0
- package/src/components/select/remote.ts +477 -0
- package/src/components/select/search.ts +430 -0
- package/src/components/select/select.css +105 -0
- package/src/components/select/select.ts +1916 -0
- package/src/components/select/tags.ts +123 -0
- package/src/components/select/templates.ts +531 -0
- package/src/components/select/types.ts +36 -0
- package/src/components/select/utils.ts +747 -0
- package/src/components/select/variants.css +5 -0
- package/src/components/separator/separator.css +14 -0
- package/src/components/skeleton/skeleton.css +10 -0
- package/src/components/stepper/index.ts +7 -0
- package/src/components/stepper/stepper.css +49 -0
- package/src/components/stepper/stepper.ts +308 -0
- package/src/components/stepper/types.ts +13 -0
- package/src/components/sticky/index.ts +7 -0
- package/src/components/sticky/sticky.css +22 -0
- package/src/components/sticky/sticky.ts +381 -0
- package/src/components/sticky/types.ts +23 -0
- package/src/components/switch/switch.css +110 -0
- package/src/components/table/table.css +168 -0
- package/src/components/tabs/index.ts +7 -0
- package/src/components/tabs/tabs.css +40 -0
- package/src/components/tabs/tabs.ts +190 -0
- package/src/components/tabs/types.ts +13 -0
- package/src/components/textarea/textarea.css +35 -0
- package/src/components/theme-switch/index.ts +10 -0
- package/src/components/theme-switch/theme-switch.css +22 -0
- package/src/components/theme-switch/theme-switch.ts +176 -0
- package/src/components/theme-switch/types.ts +15 -0
- package/src/components/toggle/index.ts +7 -0
- package/src/components/toggle/toggle.css +13 -0
- package/src/components/toggle/toggle.ts +173 -0
- package/src/components/toggle/types.ts +18 -0
- package/src/components/toggle-group/toggle-group.css +55 -0
- package/src/components/toggle-password/index.ts +10 -0
- package/src/components/toggle-password/toggle-password.css +13 -0
- package/src/components/toggle-password/toggle-password.ts +159 -0
- package/src/components/toggle-password/types.ts +13 -0
- package/src/components/tooltip/index.ts +7 -0
- package/src/components/tooltip/tooltip.css +18 -0
- package/src/components/tooltip/tooltip.ts +361 -0
- package/src/components/tooltip/types.ts +28 -0
- package/src/helpers/data.ts +46 -0
- package/src/helpers/dom.ts +405 -0
- package/src/helpers/event-handler.ts +61 -0
- package/src/helpers/utils.ts +183 -0
- package/src/index.ts +113 -0
- package/src/types.ts +23 -0
- package/styles.css +48 -0
- package/tsconfig.json +17 -0
- package/webpack.config.js +113 -0
|
@@ -0,0 +1,1916 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
|
|
3
|
+
* Copyright 2025 by Keenthemes Inc
|
|
4
|
+
* @version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
import KTData from '../../helpers/data';
|
|
7
|
+
import KTDom from '../../helpers/dom';
|
|
8
|
+
import KTComponent from '../component';
|
|
9
|
+
import {
|
|
10
|
+
KTSelectConfigInterface,
|
|
11
|
+
KTSelectState,
|
|
12
|
+
KTSelectOption as KTSelectOptionData,
|
|
13
|
+
} from './config';
|
|
14
|
+
import { KTSelectOption } from './option';
|
|
15
|
+
import { KTSelectRemote } from './remote';
|
|
16
|
+
import { KTSelectSearch } from './search';
|
|
17
|
+
import { defaultTemplates } from './templates';
|
|
18
|
+
import { KTSelectCombobox } from './combobox';
|
|
19
|
+
import { KTSelectDropdown } from './dropdown';
|
|
20
|
+
import {
|
|
21
|
+
handleDropdownKeyNavigation,
|
|
22
|
+
filterOptions,
|
|
23
|
+
FocusManager,
|
|
24
|
+
EventManager,
|
|
25
|
+
} from './utils';
|
|
26
|
+
import { KTSelectTags } from './tags';
|
|
27
|
+
import { SelectMode } from './types';
|
|
28
|
+
|
|
29
|
+
export class KTSelect extends KTComponent {
|
|
30
|
+
// Core properties
|
|
31
|
+
protected override readonly _name: string = 'select';
|
|
32
|
+
protected override readonly _dataOptionPrefix: string = 'kt-'; // Use 'kt-' prefix to support data-kt-select-option attributes
|
|
33
|
+
protected override readonly _config: KTSelectConfigInterface;
|
|
34
|
+
protected override _defaultConfig: KTSelectConfigInterface;
|
|
35
|
+
|
|
36
|
+
// DOM elements
|
|
37
|
+
private _wrapperElement: HTMLElement;
|
|
38
|
+
private _displayElement: HTMLElement;
|
|
39
|
+
private _dropdownContentElement: HTMLElement;
|
|
40
|
+
private _searchInputElement: HTMLInputElement | null;
|
|
41
|
+
private _valueDisplayElement: HTMLElement;
|
|
42
|
+
private _options: NodeListOf<HTMLElement>;
|
|
43
|
+
|
|
44
|
+
// State
|
|
45
|
+
private _dropdownIsOpen: boolean = false;
|
|
46
|
+
private _state: KTSelectState;
|
|
47
|
+
private _searchModule: KTSelectSearch;
|
|
48
|
+
private _remoteModule: KTSelectRemote;
|
|
49
|
+
private _comboboxModule: KTSelectCombobox | null = null;
|
|
50
|
+
private _tagsModule: KTSelectTags | null = null;
|
|
51
|
+
private _dropdownModule: KTSelectDropdown | null = null;
|
|
52
|
+
private _loadMoreIndicator: HTMLElement | null = null;
|
|
53
|
+
private _focusManager: FocusManager;
|
|
54
|
+
private _eventManager: EventManager;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Constructor: Initializes the select component
|
|
58
|
+
*/
|
|
59
|
+
constructor(element: HTMLElement, config?: KTSelectConfigInterface) {
|
|
60
|
+
super();
|
|
61
|
+
|
|
62
|
+
if (KTData.has(element, this._name)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._init(element);
|
|
67
|
+
this._buildConfig(config);
|
|
68
|
+
|
|
69
|
+
this._state = new KTSelectState(this._config);
|
|
70
|
+
this._config = this._state.getConfig();
|
|
71
|
+
|
|
72
|
+
(element as any).instance = this;
|
|
73
|
+
|
|
74
|
+
// Initialize event manager
|
|
75
|
+
this._eventManager = new EventManager();
|
|
76
|
+
|
|
77
|
+
// Initialize remote module if remote data is enabled
|
|
78
|
+
if (this._config.remote) {
|
|
79
|
+
this._remoteModule = new KTSelectRemote(this._config, this._element);
|
|
80
|
+
this._initializeRemoteData();
|
|
81
|
+
} else {
|
|
82
|
+
this._state
|
|
83
|
+
.setItems()
|
|
84
|
+
.then(() => {
|
|
85
|
+
if (this._config.debug)
|
|
86
|
+
console.log('Setting up component after remote data is loaded');
|
|
87
|
+
this._setupComponent();
|
|
88
|
+
})
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
console.error('Error setting items:', error);
|
|
91
|
+
// Handle the error, e.g., display an error message to the user
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize remote data fetching
|
|
98
|
+
*/
|
|
99
|
+
private _initializeRemoteData() {
|
|
100
|
+
if (!this._remoteModule || !this._config.remote) return;
|
|
101
|
+
|
|
102
|
+
if (this._config.debug)
|
|
103
|
+
console.log('Initializing remote data with URL:', this._config.dataUrl);
|
|
104
|
+
|
|
105
|
+
// Show loading state
|
|
106
|
+
this._renderLoadingState();
|
|
107
|
+
|
|
108
|
+
// Fetch remote data
|
|
109
|
+
this._remoteModule
|
|
110
|
+
.fetchData()
|
|
111
|
+
.then((items) => {
|
|
112
|
+
if (this._config.debug) console.log('Remote data fetched:', items);
|
|
113
|
+
|
|
114
|
+
// Remove placeholder/loading options before setting new items
|
|
115
|
+
this._clearExistingOptions();
|
|
116
|
+
|
|
117
|
+
// Update state with fetched items
|
|
118
|
+
this._state
|
|
119
|
+
.setItems(items)
|
|
120
|
+
.then(() => {
|
|
121
|
+
// Generate options from the fetched data
|
|
122
|
+
this._generateOptionsHtml(this._element);
|
|
123
|
+
|
|
124
|
+
if (this._config.debug)
|
|
125
|
+
console.log('Generating options HTML from remote data');
|
|
126
|
+
this._setupComponent();
|
|
127
|
+
|
|
128
|
+
// Add pagination "Load More" button if needed
|
|
129
|
+
if (this._config.pagination && this._remoteModule.hasMorePages()) {
|
|
130
|
+
this._addLoadMoreButton();
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.catch((error) => {
|
|
134
|
+
console.error('Error setting items:', error);
|
|
135
|
+
this._renderErrorState(error.message || 'Failed to load data');
|
|
136
|
+
});
|
|
137
|
+
})
|
|
138
|
+
.catch((error) => {
|
|
139
|
+
console.error('Error fetching remote data:', error);
|
|
140
|
+
this._renderErrorState(
|
|
141
|
+
this._remoteModule.getErrorMessage() || 'Failed to load data',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear existing options from the select element
|
|
148
|
+
*/
|
|
149
|
+
private _clearExistingOptions() {
|
|
150
|
+
// Keep only the empty/placeholder option and remove the rest
|
|
151
|
+
const options = Array.from(
|
|
152
|
+
this._element.querySelectorAll('option:not([value=""])'),
|
|
153
|
+
);
|
|
154
|
+
options.forEach((option) => option.remove());
|
|
155
|
+
|
|
156
|
+
// Ensure we have at least an empty option
|
|
157
|
+
if (this._element.querySelectorAll('option').length === 0) {
|
|
158
|
+
const emptyOption = defaultTemplates.emptyOption({
|
|
159
|
+
...this._config,
|
|
160
|
+
placeholder: this._config.placeholder,
|
|
161
|
+
});
|
|
162
|
+
this._element.appendChild(emptyOption);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Helper to show a dropdown message (error, loading, noResults)
|
|
168
|
+
*/
|
|
169
|
+
private _showDropdownMessage(
|
|
170
|
+
type: 'error' | 'loading' | 'noResults',
|
|
171
|
+
message?: string,
|
|
172
|
+
) {
|
|
173
|
+
if (!this._dropdownContentElement) return;
|
|
174
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
175
|
+
'[data-kt-select-options-container]',
|
|
176
|
+
);
|
|
177
|
+
if (!optionsContainer) return;
|
|
178
|
+
|
|
179
|
+
switch (type) {
|
|
180
|
+
case 'error':
|
|
181
|
+
optionsContainer.innerHTML = defaultTemplates.error({
|
|
182
|
+
...this._config,
|
|
183
|
+
errorMessage: message,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
case 'loading':
|
|
187
|
+
optionsContainer.innerHTML = defaultTemplates.loading(
|
|
188
|
+
this._config,
|
|
189
|
+
message || 'Loading...',
|
|
190
|
+
).outerHTML;
|
|
191
|
+
break;
|
|
192
|
+
case 'noResults':
|
|
193
|
+
optionsContainer.innerHTML = '';
|
|
194
|
+
optionsContainer.appendChild(defaultTemplates.noResults(this._config));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render loading state in dropdown
|
|
201
|
+
*/
|
|
202
|
+
private _renderLoadingState() {
|
|
203
|
+
if (this._element.querySelectorAll('option').length <= 1) {
|
|
204
|
+
const existingLoadingOptions = this._element.querySelectorAll(
|
|
205
|
+
'option[disabled][selected][value=""]',
|
|
206
|
+
);
|
|
207
|
+
existingLoadingOptions.forEach((option) => option.remove());
|
|
208
|
+
this._showDropdownMessage('loading', 'Loading options...');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Render error state
|
|
214
|
+
* @param message Error message
|
|
215
|
+
*/
|
|
216
|
+
private _renderErrorState(message: string) {
|
|
217
|
+
// Create error option if the select is empty
|
|
218
|
+
if (this._element.querySelectorAll('option').length <= 1) {
|
|
219
|
+
const loadingOptions = this._element.querySelectorAll(
|
|
220
|
+
'option[disabled]:not([value])',
|
|
221
|
+
);
|
|
222
|
+
loadingOptions.forEach((option) => option.remove());
|
|
223
|
+
|
|
224
|
+
// Use template function for error option instead of hardcoded element
|
|
225
|
+
const errorOption = defaultTemplates.errorOption({
|
|
226
|
+
...this._config,
|
|
227
|
+
errorMessage: message,
|
|
228
|
+
});
|
|
229
|
+
this._element.appendChild(errorOption);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If dropdown is already created, show error message there
|
|
233
|
+
this._showDropdownMessage('error', message);
|
|
234
|
+
|
|
235
|
+
if (!this._wrapperElement) {
|
|
236
|
+
if (this._config.debug) console.log('Setting up component after error');
|
|
237
|
+
this._setupComponent();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Add "Load More" button for pagination
|
|
243
|
+
*/
|
|
244
|
+
private _addLoadMoreButton() {
|
|
245
|
+
if (!this._dropdownContentElement || !this._config.pagination) return;
|
|
246
|
+
|
|
247
|
+
// Remove existing button if any
|
|
248
|
+
if (this._loadMoreIndicator) {
|
|
249
|
+
this._loadMoreIndicator.remove();
|
|
250
|
+
this._loadMoreIndicator = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Create load more button using template
|
|
254
|
+
this._loadMoreIndicator = defaultTemplates.loadMore(this._config);
|
|
255
|
+
|
|
256
|
+
// Add to dropdown
|
|
257
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
258
|
+
'[data-kt-select-options-container]',
|
|
259
|
+
);
|
|
260
|
+
if (optionsContainer) {
|
|
261
|
+
optionsContainer.appendChild(this._loadMoreIndicator);
|
|
262
|
+
} else {
|
|
263
|
+
this._dropdownContentElement.appendChild(this._loadMoreIndicator);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add event listener
|
|
267
|
+
this._loadMoreIndicator.addEventListener(
|
|
268
|
+
'click',
|
|
269
|
+
this._handleLoadMore.bind(this),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle load more button click
|
|
275
|
+
*/
|
|
276
|
+
private _handleLoadMore() {
|
|
277
|
+
if (!this._remoteModule || !this._config.pagination) return;
|
|
278
|
+
|
|
279
|
+
// Show loading state
|
|
280
|
+
if (this._loadMoreIndicator) {
|
|
281
|
+
this._loadMoreIndicator.textContent = 'Loading...';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Fetch next page
|
|
285
|
+
this._remoteModule
|
|
286
|
+
.loadNextPage()
|
|
287
|
+
.then((newItems) => {
|
|
288
|
+
// Get existing items
|
|
289
|
+
const existingItems = this._state.getItems();
|
|
290
|
+
|
|
291
|
+
// Combine new items with existing items
|
|
292
|
+
this._state
|
|
293
|
+
.setItems([...existingItems, ...newItems])
|
|
294
|
+
.then(() => {
|
|
295
|
+
// Update options in the dropdown
|
|
296
|
+
this._updateOptionsInDropdown(newItems);
|
|
297
|
+
|
|
298
|
+
// Check if there are more pages
|
|
299
|
+
if (this._remoteModule.hasMorePages()) {
|
|
300
|
+
// Reset load more button
|
|
301
|
+
if (this._loadMoreIndicator) {
|
|
302
|
+
this._loadMoreIndicator.textContent =
|
|
303
|
+
this._config.loadMoreText || 'Load more...';
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
// Remove load more button if no more pages
|
|
307
|
+
if (this._loadMoreIndicator) {
|
|
308
|
+
this._loadMoreIndicator.remove();
|
|
309
|
+
this._loadMoreIndicator = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
.catch((error) => {
|
|
314
|
+
console.error('Error updating items:', error);
|
|
315
|
+
|
|
316
|
+
// Reset load more button
|
|
317
|
+
if (this._loadMoreIndicator) {
|
|
318
|
+
this._loadMoreIndicator.textContent = 'Error loading more items';
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
})
|
|
322
|
+
.catch((error) => {
|
|
323
|
+
console.error('Error loading more items:', error);
|
|
324
|
+
|
|
325
|
+
// Reset load more button
|
|
326
|
+
if (this._loadMoreIndicator) {
|
|
327
|
+
this._loadMoreIndicator.textContent = 'Error loading more items';
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Update options in the dropdown
|
|
334
|
+
* @param newItems New items to add to the dropdown
|
|
335
|
+
*/
|
|
336
|
+
private _updateOptionsInDropdown(newItems: KTSelectOptionData[]) {
|
|
337
|
+
if (!this._dropdownContentElement || !newItems.length) return;
|
|
338
|
+
|
|
339
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
340
|
+
'[data-kt-select-options-container]',
|
|
341
|
+
);
|
|
342
|
+
if (!optionsContainer) return;
|
|
343
|
+
|
|
344
|
+
// Get the load more button
|
|
345
|
+
const loadMoreButton = optionsContainer.querySelector(
|
|
346
|
+
'[data-kt-select-load-more]',
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Process each new item
|
|
350
|
+
newItems.forEach((item) => {
|
|
351
|
+
// Create option for the original select
|
|
352
|
+
const selectOption = defaultTemplates.emptyOption({
|
|
353
|
+
...this._config,
|
|
354
|
+
placeholder: item.title || 'Unnamed option',
|
|
355
|
+
});
|
|
356
|
+
selectOption.value = item.id || '';
|
|
357
|
+
|
|
358
|
+
// Add description and icon attributes if available and valid
|
|
359
|
+
if (
|
|
360
|
+
item.description &&
|
|
361
|
+
item.description !== 'null' &&
|
|
362
|
+
item.description !== 'undefined'
|
|
363
|
+
) {
|
|
364
|
+
selectOption.setAttribute(
|
|
365
|
+
'data-kt-select-option-description',
|
|
366
|
+
item.description,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (item.icon && item.icon !== 'null' && item.icon !== 'undefined') {
|
|
370
|
+
selectOption.setAttribute('data-kt-select-option-icon', item.icon);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Add the option to the original select element
|
|
374
|
+
this._element.appendChild(selectOption);
|
|
375
|
+
|
|
376
|
+
// Create option element for the dropdown using the KTSelectOption class
|
|
377
|
+
// This ensures consistent option rendering
|
|
378
|
+
const ktOption = new KTSelectOption(selectOption, this._config);
|
|
379
|
+
const renderedOption = ktOption.render();
|
|
380
|
+
|
|
381
|
+
// Add to dropdown container
|
|
382
|
+
if (loadMoreButton) {
|
|
383
|
+
// Insert before the load more button
|
|
384
|
+
optionsContainer.insertBefore(renderedOption, loadMoreButton);
|
|
385
|
+
} else {
|
|
386
|
+
// Append to the end
|
|
387
|
+
optionsContainer.appendChild(renderedOption);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Update options NodeList to include the new options
|
|
392
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
393
|
+
`[data-kt-select-option]`,
|
|
394
|
+
) as NodeListOf<HTMLElement>;
|
|
395
|
+
|
|
396
|
+
if (this._config.debug)
|
|
397
|
+
console.log(`Added ${newItems.length} more options to dropdown`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* ========================================================================
|
|
402
|
+
* INITIALIZATION METHODS
|
|
403
|
+
* ========================================================================
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Set up the component after everything is initialized
|
|
408
|
+
*/
|
|
409
|
+
private _setupComponent() {
|
|
410
|
+
// Setup HTML structure
|
|
411
|
+
this._createHtmlStructure();
|
|
412
|
+
this._setupElementReferences();
|
|
413
|
+
this._initZIndex();
|
|
414
|
+
|
|
415
|
+
// Initialize options
|
|
416
|
+
this._initializeOptionsHtml();
|
|
417
|
+
this._preSelectOptions(this._element);
|
|
418
|
+
|
|
419
|
+
// Apply disabled state if needed
|
|
420
|
+
this._applyInitialDisabledState();
|
|
421
|
+
|
|
422
|
+
// Initialize search if enabled
|
|
423
|
+
if (this._config.enableSearch) {
|
|
424
|
+
this._initializeSearchModule();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Initialize combobox if enabled
|
|
428
|
+
if (this._config.mode === SelectMode.COMBOBOX) {
|
|
429
|
+
this._comboboxModule = new KTSelectCombobox(this);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Initialize tags if enabled
|
|
433
|
+
if (this._config.mode === SelectMode.TAGS) {
|
|
434
|
+
this._tagsModule = new KTSelectTags(this);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Initialize focus manager after dropdown element is created
|
|
438
|
+
this._focusManager = new FocusManager(
|
|
439
|
+
this._dropdownContentElement,
|
|
440
|
+
'[data-kt-select-option]',
|
|
441
|
+
this._config,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Initialize dropdown module after all elements are created
|
|
445
|
+
this._dropdownModule = new KTSelectDropdown(
|
|
446
|
+
this._wrapperElement,
|
|
447
|
+
this._displayElement,
|
|
448
|
+
this._dropdownContentElement,
|
|
449
|
+
this._config,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// Update display and set ARIA attributes
|
|
453
|
+
this._updateDisplayAndAriaAttributes();
|
|
454
|
+
this.updateSelectedOptionDisplay();
|
|
455
|
+
this._setAriaAttributes();
|
|
456
|
+
|
|
457
|
+
// Attach event listeners after all modules are initialized
|
|
458
|
+
this._attachEventListeners();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Initialize options HTML from data
|
|
463
|
+
*/
|
|
464
|
+
private _initializeOptionsHtml() {
|
|
465
|
+
this._generateOptionsHtml(this._element);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Creates the HTML structure for the select component
|
|
470
|
+
*/
|
|
471
|
+
private _createHtmlStructure() {
|
|
472
|
+
const options = Array.from(this._element.querySelectorAll('option'));
|
|
473
|
+
|
|
474
|
+
// Create wrapper and display elements
|
|
475
|
+
const wrapperElement = defaultTemplates.main(this._config);
|
|
476
|
+
const displayElement = defaultTemplates.display(this._config);
|
|
477
|
+
|
|
478
|
+
// Add the display element to the wrapper
|
|
479
|
+
wrapperElement.appendChild(displayElement);
|
|
480
|
+
|
|
481
|
+
// Create an empty dropdown first (without options) using template
|
|
482
|
+
const dropdownElement = defaultTemplates.dropdownContent({
|
|
483
|
+
...this._config,
|
|
484
|
+
zindex: this._config.dropdownZindex,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Add search input if needed
|
|
488
|
+
const isCombobox = this._config.mode === SelectMode.COMBOBOX;
|
|
489
|
+
const hasSearch = this._config.enableSearch && !isCombobox;
|
|
490
|
+
|
|
491
|
+
if (hasSearch) {
|
|
492
|
+
const searchElement = defaultTemplates.search(this._config);
|
|
493
|
+
dropdownElement.appendChild(searchElement);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Create options container using template
|
|
497
|
+
const optionsContainer = defaultTemplates.optionsContainer(this._config);
|
|
498
|
+
|
|
499
|
+
// Add each option directly to the container
|
|
500
|
+
options.forEach((optionElement) => {
|
|
501
|
+
// Skip empty placeholder options (only if BOTH value AND text are empty)
|
|
502
|
+
// This allows options with empty value but visible text to display in dropdown
|
|
503
|
+
if (
|
|
504
|
+
optionElement.value === '' &&
|
|
505
|
+
optionElement.textContent.trim() === ''
|
|
506
|
+
) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Create new KTSelectOption instance for each option
|
|
511
|
+
const selectOption = new KTSelectOption(optionElement, this._config);
|
|
512
|
+
const renderedOption = selectOption.render();
|
|
513
|
+
|
|
514
|
+
// Append directly to options container
|
|
515
|
+
optionsContainer.appendChild(renderedOption);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Add options container to dropdown
|
|
519
|
+
dropdownElement.appendChild(optionsContainer);
|
|
520
|
+
|
|
521
|
+
// Add dropdown to wrapper
|
|
522
|
+
wrapperElement.appendChild(dropdownElement);
|
|
523
|
+
|
|
524
|
+
// Insert after the original element
|
|
525
|
+
this._element.after(wrapperElement);
|
|
526
|
+
this._element.style.display = 'none';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Setup all element references after DOM is created
|
|
531
|
+
*/
|
|
532
|
+
private _setupElementReferences() {
|
|
533
|
+
this._wrapperElement = this._element.nextElementSibling as HTMLElement;
|
|
534
|
+
|
|
535
|
+
// Get display element
|
|
536
|
+
this._displayElement = this._wrapperElement.querySelector(
|
|
537
|
+
`[data-kt-select-display]`,
|
|
538
|
+
) as HTMLElement;
|
|
539
|
+
|
|
540
|
+
// Get dropdown content element - this is critical for dropdown functionality
|
|
541
|
+
this._dropdownContentElement = this._wrapperElement.querySelector(
|
|
542
|
+
`[data-kt-select-dropdown-content]`,
|
|
543
|
+
) as HTMLElement;
|
|
544
|
+
|
|
545
|
+
if (!this._dropdownContentElement) {
|
|
546
|
+
console.error('Dropdown content element not found', this._wrapperElement);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get search input element - this is used for the search functionality
|
|
550
|
+
// First check if it's in dropdown, then check if it's in display (for combobox)
|
|
551
|
+
this._searchInputElement = this._dropdownContentElement.querySelector(
|
|
552
|
+
`[data-kt-select-search]`,
|
|
553
|
+
) as HTMLInputElement;
|
|
554
|
+
|
|
555
|
+
// If not found in dropdown, check if it's the display element itself (for combobox)
|
|
556
|
+
if (
|
|
557
|
+
!this._searchInputElement &&
|
|
558
|
+
this._config.mode === SelectMode.COMBOBOX
|
|
559
|
+
) {
|
|
560
|
+
this._searchInputElement = this._displayElement as HTMLInputElement;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (this._config.debug)
|
|
564
|
+
console.log(
|
|
565
|
+
'Search input found:',
|
|
566
|
+
this._searchInputElement ? 'Yes' : 'No',
|
|
567
|
+
'Mode:',
|
|
568
|
+
this._config.mode,
|
|
569
|
+
'EnableSearch:',
|
|
570
|
+
this._config.enableSearch,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
this._valueDisplayElement = this._wrapperElement.querySelector(
|
|
574
|
+
`[data-kt-select-value]`,
|
|
575
|
+
) as HTMLElement;
|
|
576
|
+
|
|
577
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
578
|
+
`[data-kt-select-option]`,
|
|
579
|
+
) as NodeListOf<HTMLElement>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Attach all event listeners to elements
|
|
584
|
+
*/
|
|
585
|
+
private _attachEventListeners() {
|
|
586
|
+
// Document level event listeners
|
|
587
|
+
document.addEventListener('click', this._handleDocumentClick.bind(this));
|
|
588
|
+
document.addEventListener('keydown', this._handleEscKey.bind(this));
|
|
589
|
+
|
|
590
|
+
// Dropdown option click events
|
|
591
|
+
this._eventManager.addListener(
|
|
592
|
+
this._dropdownContentElement,
|
|
593
|
+
'click',
|
|
594
|
+
this._handleDropdownOptionClick.bind(this),
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Only attach click handler to display element
|
|
598
|
+
this._eventManager.addListener(
|
|
599
|
+
this._displayElement,
|
|
600
|
+
'click',
|
|
601
|
+
this._handleDropdownClick.bind(this),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
// Only attach keyboard navigation to display element if NOT in combobox mode
|
|
605
|
+
// This prevents conflicts with the combobox module's keyboard handler
|
|
606
|
+
if (this._config.mode !== SelectMode.COMBOBOX) {
|
|
607
|
+
if (this._config.debug)
|
|
608
|
+
console.log(
|
|
609
|
+
'Attaching keyboard navigation to display element (non-combobox mode)',
|
|
610
|
+
);
|
|
611
|
+
this._eventManager.addListener(
|
|
612
|
+
this._displayElement,
|
|
613
|
+
'keydown',
|
|
614
|
+
this._handleDropdownKeyDown.bind(this),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Initialize search module if search is enabled
|
|
621
|
+
*/
|
|
622
|
+
private _initializeSearchModule() {
|
|
623
|
+
if (this._config.enableSearch) {
|
|
624
|
+
this._searchModule = new KTSelectSearch(this);
|
|
625
|
+
this._searchModule.init();
|
|
626
|
+
|
|
627
|
+
// If remote search is enabled, add event listener for search input
|
|
628
|
+
if (
|
|
629
|
+
this._config.remote &&
|
|
630
|
+
this._config.searchParam &&
|
|
631
|
+
this._searchInputElement
|
|
632
|
+
) {
|
|
633
|
+
this._searchInputElement.addEventListener(
|
|
634
|
+
'input',
|
|
635
|
+
this._handleRemoteSearch.bind(this),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Apply ARIA attributes and update display
|
|
643
|
+
*/
|
|
644
|
+
private _updateDisplayAndAriaAttributes() {
|
|
645
|
+
this.updateSelectedOptionDisplay();
|
|
646
|
+
this._setAriaAttributes();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Apply initial disabled state if configured
|
|
651
|
+
*/
|
|
652
|
+
private _applyInitialDisabledState() {
|
|
653
|
+
if (this._config.disabled) {
|
|
654
|
+
this.getElement().classList.add('disabled');
|
|
655
|
+
this.getElement().setAttribute('disabled', 'disabled');
|
|
656
|
+
this._wrapperElement.classList.add('disabled');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Generate options HTML from data items
|
|
662
|
+
*/
|
|
663
|
+
private _generateOptionsHtml(element: HTMLElement) {
|
|
664
|
+
const items = this._state.getItems() || [];
|
|
665
|
+
|
|
666
|
+
if (this._config.debug)
|
|
667
|
+
console.log(`Generating options HTML from ${items.length} items`);
|
|
668
|
+
|
|
669
|
+
// Only modify options if we have items to replace them with
|
|
670
|
+
if (items && items.length > 0) {
|
|
671
|
+
// Clear existing options except the first empty one
|
|
672
|
+
const options = element.querySelectorAll('option:not(:first-child)');
|
|
673
|
+
options.forEach((option) => option.remove());
|
|
674
|
+
|
|
675
|
+
// Generate options from data
|
|
676
|
+
items.forEach((item) => {
|
|
677
|
+
const optionElement = document.createElement('option');
|
|
678
|
+
|
|
679
|
+
// Get value - use item.id directly if available, otherwise try dataValueField
|
|
680
|
+
let value = '';
|
|
681
|
+
if (item.id !== undefined) {
|
|
682
|
+
value = String(item.id);
|
|
683
|
+
} else if (this._config.dataValueField) {
|
|
684
|
+
const extractedValue = this._getValueByKey(
|
|
685
|
+
item,
|
|
686
|
+
this._config.dataValueField,
|
|
687
|
+
);
|
|
688
|
+
value = extractedValue !== null ? String(extractedValue) : '';
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Get label - use item.title directly if available, otherwise try dataFieldText
|
|
692
|
+
let label = '';
|
|
693
|
+
if (item.title !== undefined) {
|
|
694
|
+
label = String(item.title);
|
|
695
|
+
} else if (this._config.dataFieldText) {
|
|
696
|
+
const extractedLabel = this._getValueByKey(
|
|
697
|
+
item,
|
|
698
|
+
this._config.dataFieldText,
|
|
699
|
+
);
|
|
700
|
+
label =
|
|
701
|
+
extractedLabel !== null ? String(extractedLabel) : 'Unnamed option';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Get description - skip if null, undefined, or "null" string
|
|
705
|
+
let description = null;
|
|
706
|
+
if (
|
|
707
|
+
item.description !== undefined &&
|
|
708
|
+
item.description !== null &&
|
|
709
|
+
String(item.description) !== 'null' &&
|
|
710
|
+
String(item.description) !== 'undefined'
|
|
711
|
+
) {
|
|
712
|
+
description = String(item.description);
|
|
713
|
+
} else if (this._config.dataFieldDescription) {
|
|
714
|
+
const extractedDesc = this._getValueByKey(
|
|
715
|
+
item,
|
|
716
|
+
this._config.dataFieldDescription,
|
|
717
|
+
);
|
|
718
|
+
if (
|
|
719
|
+
extractedDesc !== null &&
|
|
720
|
+
extractedDesc !== undefined &&
|
|
721
|
+
String(extractedDesc) !== 'null' &&
|
|
722
|
+
String(extractedDesc) !== 'undefined'
|
|
723
|
+
) {
|
|
724
|
+
description = String(extractedDesc);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Get icon - skip if null, undefined, or "null" string
|
|
729
|
+
let icon = null;
|
|
730
|
+
if (
|
|
731
|
+
item.icon !== undefined &&
|
|
732
|
+
item.icon !== null &&
|
|
733
|
+
String(item.icon) !== 'null' &&
|
|
734
|
+
String(item.icon) !== 'undefined'
|
|
735
|
+
) {
|
|
736
|
+
icon = String(item.icon);
|
|
737
|
+
} else if (this._config.dataFieldIcon) {
|
|
738
|
+
const extractedIcon = this._getValueByKey(
|
|
739
|
+
item,
|
|
740
|
+
this._config.dataFieldIcon,
|
|
741
|
+
);
|
|
742
|
+
if (
|
|
743
|
+
extractedIcon !== null &&
|
|
744
|
+
extractedIcon !== undefined &&
|
|
745
|
+
String(extractedIcon) !== 'null' &&
|
|
746
|
+
String(extractedIcon) !== 'undefined'
|
|
747
|
+
) {
|
|
748
|
+
icon = String(extractedIcon);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Log the extracted values for debugging
|
|
753
|
+
if (this._config.debug)
|
|
754
|
+
console.log(
|
|
755
|
+
`Option: value=${value}, label=${label}, desc=${description ? description : 'none'}, icon=${icon ? icon : 'none'}`,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Set option attributes
|
|
759
|
+
optionElement.value = value;
|
|
760
|
+
optionElement.textContent = label || 'Unnamed option';
|
|
761
|
+
|
|
762
|
+
if (description) {
|
|
763
|
+
optionElement.setAttribute(
|
|
764
|
+
'data-kt-select-option-description',
|
|
765
|
+
description,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (icon) {
|
|
770
|
+
optionElement.setAttribute('data-kt-select-option-icon', icon);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (item.selected) {
|
|
774
|
+
optionElement.setAttribute('selected', 'selected');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
element.appendChild(optionElement);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (this._config.debug)
|
|
781
|
+
console.log(`Added ${items.length} options to select element`);
|
|
782
|
+
} else {
|
|
783
|
+
if (this._config.debug) console.log('No items to generate options from');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Extract nested property value from object using dot notation
|
|
789
|
+
*/
|
|
790
|
+
private _getValueByKey(obj: any, key: string): any {
|
|
791
|
+
if (!key || !obj) return null;
|
|
792
|
+
|
|
793
|
+
// Use reduce to walk through the object by splitting the key on dots
|
|
794
|
+
const result = key
|
|
795
|
+
.split('.')
|
|
796
|
+
.reduce((o, k) => (o && o[k] !== undefined ? o[k] : null), obj);
|
|
797
|
+
|
|
798
|
+
if (this._config.debug)
|
|
799
|
+
console.log(
|
|
800
|
+
`Extracting [${key}] from object => ${result !== null ? JSON.stringify(result) : 'null'}`,
|
|
801
|
+
);
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Pre-select options that have the selected attribute
|
|
807
|
+
*/
|
|
808
|
+
private _preSelectOptions(element: HTMLElement) {
|
|
809
|
+
// Handle options with selected attribute
|
|
810
|
+
Array.from(element.querySelectorAll('option[selected]')).forEach(
|
|
811
|
+
(option) => {
|
|
812
|
+
const value = (option as HTMLOptionElement).value;
|
|
813
|
+
this._selectOption(value);
|
|
814
|
+
},
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
// Handle data-kt-select-pre-selected attribute for React compatibility
|
|
818
|
+
const preSelectedValues = element.getAttribute(
|
|
819
|
+
'data-kt-select-pre-selected',
|
|
820
|
+
);
|
|
821
|
+
if (preSelectedValues) {
|
|
822
|
+
const values = preSelectedValues.split(',').map((v) => v.trim());
|
|
823
|
+
values.forEach((value) => {
|
|
824
|
+
if (value) {
|
|
825
|
+
this._selectOption(value);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Set appropriate z-index for dropdown
|
|
833
|
+
*/
|
|
834
|
+
private _initZIndex() {
|
|
835
|
+
let zindex: number = this._config.dropdownZindex as number;
|
|
836
|
+
if (
|
|
837
|
+
parseInt(KTDom.getCssProp(this._dropdownContentElement, 'z-index')) >
|
|
838
|
+
zindex
|
|
839
|
+
) {
|
|
840
|
+
zindex = parseInt(
|
|
841
|
+
KTDom.getCssProp(this._dropdownContentElement, 'z-index'),
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
if (KTDom.getHighestZindex(this._wrapperElement) > zindex) {
|
|
845
|
+
zindex = KTDom.getHighestZindex(this._wrapperElement) + 1;
|
|
846
|
+
}
|
|
847
|
+
this._dropdownContentElement.style.zIndex = String(zindex);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* ========================================================================
|
|
852
|
+
* DROPDOWN MANAGEMENT
|
|
853
|
+
* ========================================================================
|
|
854
|
+
*/
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Toggle dropdown visibility
|
|
858
|
+
*/
|
|
859
|
+
public toggleDropdown() {
|
|
860
|
+
if (this._config.debug) console.log('toggleDropdown called');
|
|
861
|
+
if (this._dropdownModule) {
|
|
862
|
+
// Always use the dropdown module's state to determine whether to open or close
|
|
863
|
+
if (this._dropdownModule.isOpen()) {
|
|
864
|
+
if (this._config.debug) console.log('Dropdown is open, closing...');
|
|
865
|
+
this.closeDropdown();
|
|
866
|
+
} else {
|
|
867
|
+
if (this._config.debug) console.log('Dropdown is closed, opening...');
|
|
868
|
+
this.openDropdown();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Open the dropdown
|
|
875
|
+
*/
|
|
876
|
+
public openDropdown() {
|
|
877
|
+
if (this._config.debug)
|
|
878
|
+
console.log(
|
|
879
|
+
'openDropdown called, dropdownModule exists:',
|
|
880
|
+
!!this._dropdownModule,
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
if (!this._dropdownModule) {
|
|
884
|
+
if (this._config.debug)
|
|
885
|
+
console.log('Early return from openDropdown - module missing');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Don't open dropdown if the select is disabled
|
|
890
|
+
if (this._config.disabled) {
|
|
891
|
+
if (this._config.debug)
|
|
892
|
+
console.log('Early return from openDropdown - select is disabled');
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (this._config.debug)
|
|
897
|
+
console.log('Opening dropdown via dropdownModule...');
|
|
898
|
+
|
|
899
|
+
// Set our internal flag to match what we're doing
|
|
900
|
+
this._dropdownIsOpen = true;
|
|
901
|
+
|
|
902
|
+
// Open the dropdown via the module
|
|
903
|
+
this._dropdownModule.open();
|
|
904
|
+
|
|
905
|
+
// Dispatch custom event
|
|
906
|
+
this._dispatchEvent('show');
|
|
907
|
+
this._fireEvent('show');
|
|
908
|
+
|
|
909
|
+
// Focus search input if configured and exists
|
|
910
|
+
if (
|
|
911
|
+
this._config.enableSearch &&
|
|
912
|
+
this._config.searchAutofocus &&
|
|
913
|
+
this._searchInputElement
|
|
914
|
+
) {
|
|
915
|
+
setTimeout(() => {
|
|
916
|
+
this._searchInputElement.focus();
|
|
917
|
+
}, 50);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Update ARIA states
|
|
921
|
+
this._setAriaAttributes();
|
|
922
|
+
|
|
923
|
+
// Focus the first selected option or first option if nothing selected
|
|
924
|
+
this._focusSelectedOption();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Close the dropdown
|
|
929
|
+
*/
|
|
930
|
+
public closeDropdown() {
|
|
931
|
+
if (this._config.debug)
|
|
932
|
+
console.log(
|
|
933
|
+
'closeDropdown called, dropdownModule exists:',
|
|
934
|
+
!!this._dropdownModule,
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
// Only check if dropdown module exists, not dropdownIsOpen flag
|
|
938
|
+
if (!this._dropdownModule) {
|
|
939
|
+
if (this._config.debug)
|
|
940
|
+
console.log('Early return from closeDropdown - module missing');
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Always close by delegating to the dropdown module, which is the source of truth
|
|
945
|
+
if (this._config.debug)
|
|
946
|
+
console.log('Closing dropdown via dropdownModule...');
|
|
947
|
+
|
|
948
|
+
// Clear search input and highlights if the dropdown is closing
|
|
949
|
+
if (this._searchModule && this._searchInputElement) {
|
|
950
|
+
// Clear search input if configured to do so
|
|
951
|
+
if (this._config.clearSearchOnClose) {
|
|
952
|
+
this._searchInputElement.value = '';
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Always clear the highlights when dropdown closes
|
|
956
|
+
this._searchModule.clearSearchHighlights();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Set our internal flag to match what we're doing
|
|
960
|
+
this._dropdownIsOpen = false;
|
|
961
|
+
|
|
962
|
+
// Call the dropdown module's close method
|
|
963
|
+
this._dropdownModule.close();
|
|
964
|
+
|
|
965
|
+
// Reset all focus states
|
|
966
|
+
if (this._focusManager) {
|
|
967
|
+
this._focusManager.resetFocus();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Dispatch custom events
|
|
971
|
+
this._dispatchEvent('close');
|
|
972
|
+
this._fireEvent('close');
|
|
973
|
+
|
|
974
|
+
// Update ARIA states
|
|
975
|
+
this._setAriaAttributes();
|
|
976
|
+
if (this._config.debug) console.log('closeDropdown complete');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Update dropdown position
|
|
981
|
+
*/
|
|
982
|
+
public updateDropdownPosition() {
|
|
983
|
+
if (this._dropdownModule) {
|
|
984
|
+
this._dropdownModule.updatePosition();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Focus on the first selected option if any exists in the dropdown
|
|
990
|
+
*/
|
|
991
|
+
private _focusSelectedOption() {
|
|
992
|
+
// Get selected options
|
|
993
|
+
const selectedOptions = this.getSelectedOptions();
|
|
994
|
+
if (selectedOptions.length === 0) return;
|
|
995
|
+
|
|
996
|
+
// Get the first selected option element
|
|
997
|
+
const firstSelectedValue = selectedOptions[0];
|
|
998
|
+
|
|
999
|
+
// Use the FocusManager to focus on the option
|
|
1000
|
+
this._focusManager.focusOptionByValue(firstSelectedValue);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* ========================================================================
|
|
1005
|
+
* SELECTION MANAGEMENT
|
|
1006
|
+
* ========================================================================
|
|
1007
|
+
*/
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Select an option by value
|
|
1011
|
+
*/
|
|
1012
|
+
private _selectOption(value: string) {
|
|
1013
|
+
// Get current selection state
|
|
1014
|
+
const isSelected = this._state.isSelected(value);
|
|
1015
|
+
|
|
1016
|
+
// Toggle selection in state
|
|
1017
|
+
if (this._config.multiple) {
|
|
1018
|
+
// Toggle in multiple mode
|
|
1019
|
+
this._state.toggleSelectedOptions(value);
|
|
1020
|
+
} else {
|
|
1021
|
+
// Set as only selection in single mode
|
|
1022
|
+
this._state.setSelectedOptions(value);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Update the original select element's option selected state
|
|
1026
|
+
const optionEl = Array.from(this._element.querySelectorAll('option')).find(
|
|
1027
|
+
(opt) => opt.value === value,
|
|
1028
|
+
) as HTMLOptionElement;
|
|
1029
|
+
|
|
1030
|
+
if (optionEl) {
|
|
1031
|
+
if (this._config.multiple) {
|
|
1032
|
+
// Toggle the selection for multiple select
|
|
1033
|
+
optionEl.selected = !isSelected;
|
|
1034
|
+
} else {
|
|
1035
|
+
// Set as only selection for single select
|
|
1036
|
+
Array.from(this._element.querySelectorAll('option')).forEach((opt) => {
|
|
1037
|
+
(opt as HTMLOptionElement).selected = opt.value === value;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Update the visual display of selected options
|
|
1043
|
+
this.updateSelectedOptionDisplay();
|
|
1044
|
+
|
|
1045
|
+
// Update option classes without re-rendering the dropdown content
|
|
1046
|
+
this._updateSelectedOptionClass();
|
|
1047
|
+
|
|
1048
|
+
// Dispatch standard and custom change events
|
|
1049
|
+
this._dispatchEvent('change', {
|
|
1050
|
+
value: value,
|
|
1051
|
+
selected: !isSelected,
|
|
1052
|
+
selectedOptions: this.getSelectedOptions(),
|
|
1053
|
+
});
|
|
1054
|
+
this._fireEvent('change', {
|
|
1055
|
+
value: value,
|
|
1056
|
+
selected: !isSelected,
|
|
1057
|
+
selectedOptions: this.getSelectedOptions(),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Update selected option display value
|
|
1063
|
+
*/
|
|
1064
|
+
public updateSelectedOptionDisplay() {
|
|
1065
|
+
const selectedOptions = this.getSelectedOptions();
|
|
1066
|
+
|
|
1067
|
+
if (this._config.renderSelected) {
|
|
1068
|
+
// Use the custom renderSelected function if provided
|
|
1069
|
+
this._updateValueDisplay(this._config.renderSelected(selectedOptions));
|
|
1070
|
+
} else {
|
|
1071
|
+
if (selectedOptions.length === 0) {
|
|
1072
|
+
if (this._config.mode !== SelectMode.COMBOBOX) {
|
|
1073
|
+
this._updateValueDisplay(this._config.placeholder); // Use innerHTML for placeholder
|
|
1074
|
+
}
|
|
1075
|
+
} else if (this._config.multiple) {
|
|
1076
|
+
if (this._config.mode === SelectMode.TAGS) {
|
|
1077
|
+
// Use the tags module to render selected options as tags
|
|
1078
|
+
if (this._tagsModule) {
|
|
1079
|
+
this._tagsModule.updateTagsDisplay(selectedOptions);
|
|
1080
|
+
} else {
|
|
1081
|
+
// Fallback if tags module not initialized for some reason
|
|
1082
|
+
this._updateValueDisplay(selectedOptions.join(', '));
|
|
1083
|
+
}
|
|
1084
|
+
} else {
|
|
1085
|
+
// Render as comma-separated values
|
|
1086
|
+
const displayText = selectedOptions
|
|
1087
|
+
.map((option) => this._getOptionInnerHtml(option) || '')
|
|
1088
|
+
.join(', ');
|
|
1089
|
+
this._updateValueDisplay(displayText);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
const selectedOption = selectedOptions[0];
|
|
1093
|
+
if (selectedOption) {
|
|
1094
|
+
const selectedText = this._getOptionInnerHtml(selectedOption);
|
|
1095
|
+
this._updateValueDisplay(selectedText);
|
|
1096
|
+
|
|
1097
|
+
// Update combobox input value if in combobox mode
|
|
1098
|
+
if (
|
|
1099
|
+
this._config.mode === SelectMode.COMBOBOX &&
|
|
1100
|
+
this._comboboxModule
|
|
1101
|
+
) {
|
|
1102
|
+
this._comboboxModule.updateSelectedValue(selectedText);
|
|
1103
|
+
}
|
|
1104
|
+
} else {
|
|
1105
|
+
this._updateValueDisplay(this._config.placeholder);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Update any debug display boxes if they exist
|
|
1111
|
+
this._updateDebugDisplays();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Update the value display element
|
|
1116
|
+
*/
|
|
1117
|
+
private _updateValueDisplay(value: string) {
|
|
1118
|
+
if (this._config.mode === SelectMode.COMBOBOX) {
|
|
1119
|
+
// For combobox, we only update the hidden value element, not the input
|
|
1120
|
+
// The combobox module will handle updating the input value
|
|
1121
|
+
if (!this._comboboxModule) {
|
|
1122
|
+
(this._valueDisplayElement as HTMLInputElement).value = value;
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
this._valueDisplayElement.innerHTML = value;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Update debug displays if present
|
|
1131
|
+
*/
|
|
1132
|
+
private _updateDebugDisplays() {
|
|
1133
|
+
// Check if we're in a test environment with debug boxes
|
|
1134
|
+
const selectId = this.getElement().id;
|
|
1135
|
+
if (selectId) {
|
|
1136
|
+
const debugElement = document.getElementById(`${selectId}-value`);
|
|
1137
|
+
if (debugElement) {
|
|
1138
|
+
const selectedOptions = this.getSelectedOptions();
|
|
1139
|
+
|
|
1140
|
+
// Format display based on selection mode
|
|
1141
|
+
if (this._config.multiple) {
|
|
1142
|
+
// For multiple selection, show comma-separated list
|
|
1143
|
+
debugElement.textContent =
|
|
1144
|
+
selectedOptions.length > 0 ? selectedOptions.join(', ') : 'None';
|
|
1145
|
+
} else {
|
|
1146
|
+
// For single selection, show just the one value
|
|
1147
|
+
debugElement.textContent =
|
|
1148
|
+
selectedOptions.length > 0 ? selectedOptions[0] : 'None';
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Get option inner HTML content by option value
|
|
1156
|
+
*/
|
|
1157
|
+
private _getOptionInnerHtml(optionValue: string) {
|
|
1158
|
+
const option = Array.from(this._options).find(
|
|
1159
|
+
(opt) => opt.dataset.value === optionValue,
|
|
1160
|
+
);
|
|
1161
|
+
if (this._config.mode == SelectMode.COMBOBOX) {
|
|
1162
|
+
return option.textContent;
|
|
1163
|
+
}
|
|
1164
|
+
return option.innerHTML; // Get the entire HTML content of the option
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Update CSS classes for selected options
|
|
1169
|
+
*/
|
|
1170
|
+
private _updateSelectedOptionClass(): void {
|
|
1171
|
+
const allOptions = this._wrapperElement.querySelectorAll(
|
|
1172
|
+
`[data-kt-select-option]`,
|
|
1173
|
+
);
|
|
1174
|
+
const selectedValues = this._state.getSelectedOptions();
|
|
1175
|
+
const maxReached =
|
|
1176
|
+
typeof this._config.maxSelections === 'number' &&
|
|
1177
|
+
selectedValues.length >= this._config.maxSelections;
|
|
1178
|
+
|
|
1179
|
+
if (this._config.debug)
|
|
1180
|
+
console.log(
|
|
1181
|
+
'Updating selected classes for options, selected values:',
|
|
1182
|
+
selectedValues,
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
allOptions.forEach((option) => {
|
|
1186
|
+
const optionValue = option.getAttribute('data-value');
|
|
1187
|
+
if (!optionValue) return;
|
|
1188
|
+
const isSelected = selectedValues.includes(optionValue);
|
|
1189
|
+
if (isSelected) {
|
|
1190
|
+
option.classList.add('selected');
|
|
1191
|
+
option.setAttribute('aria-selected', 'true');
|
|
1192
|
+
option.classList.remove('hidden');
|
|
1193
|
+
option.classList.remove('disabled');
|
|
1194
|
+
option.removeAttribute('aria-disabled');
|
|
1195
|
+
} else {
|
|
1196
|
+
option.classList.remove('selected');
|
|
1197
|
+
option.setAttribute('aria-selected', 'false');
|
|
1198
|
+
if (maxReached) {
|
|
1199
|
+
option.classList.add('disabled');
|
|
1200
|
+
option.setAttribute('aria-disabled', 'true');
|
|
1201
|
+
} else {
|
|
1202
|
+
option.classList.remove('disabled');
|
|
1203
|
+
option.removeAttribute('aria-disabled');
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Clear all selected options
|
|
1211
|
+
*/
|
|
1212
|
+
public clearSelection() {
|
|
1213
|
+
// Clear the current selection
|
|
1214
|
+
this._state.setSelectedOptions([]);
|
|
1215
|
+
this.updateSelectedOptionDisplay();
|
|
1216
|
+
this._updateSelectedOptionClass();
|
|
1217
|
+
|
|
1218
|
+
// For combobox, also clear the input value
|
|
1219
|
+
if (this._config.mode === SelectMode.COMBOBOX) {
|
|
1220
|
+
if (this._searchInputElement) {
|
|
1221
|
+
this._searchInputElement.value = '';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// If combobox has a clear button, hide it
|
|
1225
|
+
if (this._comboboxModule) {
|
|
1226
|
+
// The combobox module will handle hiding the clear button
|
|
1227
|
+
this._comboboxModule.resetInputValueToSelection();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Dispatch change event
|
|
1232
|
+
this._dispatchEvent('change');
|
|
1233
|
+
this._fireEvent('change');
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Set selected options programmatically
|
|
1238
|
+
*/
|
|
1239
|
+
public setSelectedOptions(options: HTMLOptionElement[]) {
|
|
1240
|
+
const values = Array.from(options).map((option) => option.value);
|
|
1241
|
+
this._state.setSelectedOptions(values);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* ========================================================================
|
|
1246
|
+
* KEYBOARD NAVIGATION
|
|
1247
|
+
* ========================================================================
|
|
1248
|
+
*/
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Handle dropdown key down events for keyboard navigation
|
|
1252
|
+
* Only used for standard (non-combobox) dropdowns
|
|
1253
|
+
*/
|
|
1254
|
+
private _handleDropdownKeyDown(event: KeyboardEvent) {
|
|
1255
|
+
// Log event for debugging
|
|
1256
|
+
if (this._config.debug)
|
|
1257
|
+
console.log('Standard dropdown keydown:', event.key);
|
|
1258
|
+
|
|
1259
|
+
// Use the shared handler
|
|
1260
|
+
handleDropdownKeyNavigation(event, this, {
|
|
1261
|
+
multiple: this._config.multiple,
|
|
1262
|
+
closeOnSelect: this._config.closeOnSelect,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Focus next option in dropdown
|
|
1268
|
+
*/
|
|
1269
|
+
private _focusNextOption(): Element | null {
|
|
1270
|
+
return this._focusManager.focusNext();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Focus previous option in dropdown
|
|
1275
|
+
*/
|
|
1276
|
+
private _focusPreviousOption(): Element | null {
|
|
1277
|
+
return this._focusManager.focusPrevious();
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Apply hover/focus state to focused option
|
|
1282
|
+
*/
|
|
1283
|
+
private _hoverFocusedOption(option: Element) {
|
|
1284
|
+
this._focusManager.applyFocus(option as HTMLElement);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Scroll option into view when navigating
|
|
1289
|
+
*/
|
|
1290
|
+
private _scrollOptionIntoView(option: Element) {
|
|
1291
|
+
this._focusManager.scrollIntoView(option as HTMLElement);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Select the currently focused option
|
|
1296
|
+
*/
|
|
1297
|
+
public selectFocusedOption() {
|
|
1298
|
+
const focusedOption = this._focusManager.getFocusedOption();
|
|
1299
|
+
|
|
1300
|
+
if (focusedOption) {
|
|
1301
|
+
const selectedValue = focusedOption.dataset.value;
|
|
1302
|
+
|
|
1303
|
+
// Extract just the title text, not including description
|
|
1304
|
+
let selectedText = '';
|
|
1305
|
+
const titleElement = focusedOption.querySelector(
|
|
1306
|
+
'[data-kt-option-title]',
|
|
1307
|
+
);
|
|
1308
|
+
if (titleElement) {
|
|
1309
|
+
// If it has a structured content with title element
|
|
1310
|
+
selectedText = titleElement.textContent?.trim() || '';
|
|
1311
|
+
} else {
|
|
1312
|
+
// Fallback to the whole text content
|
|
1313
|
+
selectedText = focusedOption.textContent?.trim() || '';
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// First trigger the selection to ensure state is updated properly
|
|
1317
|
+
if (selectedValue) {
|
|
1318
|
+
this._selectOption(selectedValue);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// For combobox mode, update input value AFTER selection to ensure consistency
|
|
1322
|
+
if (this._config.mode === SelectMode.COMBOBOX && this._comboboxModule) {
|
|
1323
|
+
this._comboboxModule.updateSelectedValue(selectedText);
|
|
1324
|
+
// Also directly update the input value for immediate visual feedback
|
|
1325
|
+
if (this._searchInputElement) {
|
|
1326
|
+
this._searchInputElement.value = selectedText;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* ========================================================================
|
|
1334
|
+
* COMBOBOX SPECIFIC METHODS
|
|
1335
|
+
* ========================================================================
|
|
1336
|
+
*/
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Handle combobox input events
|
|
1340
|
+
*/
|
|
1341
|
+
private _handleComboboxInput(event: Event) {
|
|
1342
|
+
if (this._comboboxModule) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const inputElement = event.target as HTMLInputElement;
|
|
1347
|
+
const query = inputElement.value.toLowerCase();
|
|
1348
|
+
|
|
1349
|
+
// If dropdown isn't open, open it when user starts typing
|
|
1350
|
+
if (!this._dropdownIsOpen) {
|
|
1351
|
+
this.openDropdown();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Filter options based on input
|
|
1355
|
+
this._filterOptionsForCombobox(query);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Filter options for combobox based on input query
|
|
1360
|
+
* Uses the shared filterOptions function
|
|
1361
|
+
*/
|
|
1362
|
+
private _filterOptionsForCombobox(query: string) {
|
|
1363
|
+
const options = Array.from(
|
|
1364
|
+
this._dropdownContentElement.querySelectorAll('[data-kt-select-option]'),
|
|
1365
|
+
) as HTMLElement[];
|
|
1366
|
+
|
|
1367
|
+
filterOptions(options, query, this._config, this._dropdownContentElement);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* ========================================================================
|
|
1372
|
+
* EVENT HANDLERS
|
|
1373
|
+
* ========================================================================
|
|
1374
|
+
*/
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Handle display element click
|
|
1378
|
+
*/
|
|
1379
|
+
private _handleDropdownClick(event: Event) {
|
|
1380
|
+
if (this._config.debug)
|
|
1381
|
+
console.log('Display element clicked', event.target);
|
|
1382
|
+
event.preventDefault();
|
|
1383
|
+
event.stopPropagation(); // Prevent event bubbling
|
|
1384
|
+
this.toggleDropdown();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Handle click within the dropdown
|
|
1389
|
+
*/
|
|
1390
|
+
private _handleDropdownOptionClick(event: Event) {
|
|
1391
|
+
const optionElement = (event.target as HTMLElement).closest(
|
|
1392
|
+
`[data-kt-select-option]`,
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
// If an option is clicked, handle the option click
|
|
1396
|
+
if (optionElement) {
|
|
1397
|
+
this._handleOptionClick(event);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Handle clicking on an option in the dropdown
|
|
1403
|
+
*/
|
|
1404
|
+
private _handleOptionClick(event: Event) {
|
|
1405
|
+
if (this._config.debug)
|
|
1406
|
+
console.log('_handleOptionClick called', event.target);
|
|
1407
|
+
event.preventDefault();
|
|
1408
|
+
event.stopPropagation();
|
|
1409
|
+
|
|
1410
|
+
// Find the clicked option element
|
|
1411
|
+
const clickedOption = (event.target as HTMLElement).closest(
|
|
1412
|
+
`[data-kt-select-option]`,
|
|
1413
|
+
) as HTMLElement;
|
|
1414
|
+
|
|
1415
|
+
if (!clickedOption) {
|
|
1416
|
+
if (this._config.debug) console.log('No clicked option found');
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Check if the option is disabled
|
|
1421
|
+
if (clickedOption.getAttribute('aria-disabled') === 'true') {
|
|
1422
|
+
if (this._config.debug) console.log('Option is disabled, ignoring click');
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Use dataset.value to get the option value
|
|
1427
|
+
const optionValue = clickedOption.dataset.value;
|
|
1428
|
+
if (optionValue === undefined) {
|
|
1429
|
+
if (this._config.debug) console.log('Option value is undefined');
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (this._config.debug) console.log('Option clicked:', optionValue);
|
|
1434
|
+
|
|
1435
|
+
// Use toggleSelection instead of _selectOption to prevent re-rendering
|
|
1436
|
+
this.toggleSelection(optionValue);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Handle document click for closing dropdown
|
|
1441
|
+
*/
|
|
1442
|
+
private _handleDocumentClick(event: MouseEvent) {
|
|
1443
|
+
const targetElement = event.target as HTMLElement;
|
|
1444
|
+
// Check if the click is outside the dropdown and the display element
|
|
1445
|
+
if (!this._wrapperElement.contains(targetElement)) {
|
|
1446
|
+
this.closeDropdown();
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Handle escape key press
|
|
1452
|
+
*/
|
|
1453
|
+
private _handleEscKey(event: KeyboardEvent) {
|
|
1454
|
+
if (event.key === 'Escape' && this._dropdownIsOpen) {
|
|
1455
|
+
this.closeDropdown();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* ========================================================================
|
|
1461
|
+
* ACCESSIBILITY METHODS
|
|
1462
|
+
* ========================================================================
|
|
1463
|
+
*/
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Set ARIA attributes for accessibility
|
|
1467
|
+
*/
|
|
1468
|
+
private _setAriaAttributes() {
|
|
1469
|
+
this._displayElement.setAttribute(
|
|
1470
|
+
'aria-expanded',
|
|
1471
|
+
this._dropdownIsOpen.toString(),
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Handle focus events
|
|
1477
|
+
*/
|
|
1478
|
+
private _handleFocus() {
|
|
1479
|
+
// Implementation pending
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Handle blur events
|
|
1484
|
+
*/
|
|
1485
|
+
private _handleBlur() {
|
|
1486
|
+
// Implementation pending
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* ========================================================================
|
|
1491
|
+
* PUBLIC API
|
|
1492
|
+
* ========================================================================
|
|
1493
|
+
*/
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Get the search input element
|
|
1497
|
+
*/
|
|
1498
|
+
public getSearchInput(): HTMLInputElement {
|
|
1499
|
+
return this._searchInputElement;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Get selected options
|
|
1504
|
+
*/
|
|
1505
|
+
public getSelectedOptions() {
|
|
1506
|
+
return this._state.getSelectedOptions();
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Get configuration
|
|
1511
|
+
*/
|
|
1512
|
+
public getConfig(): KTSelectConfigInterface {
|
|
1513
|
+
return this._config;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Get option elements
|
|
1518
|
+
*/
|
|
1519
|
+
public getOptionsElement(): NodeListOf<HTMLElement> {
|
|
1520
|
+
return this._options;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Get dropdown element
|
|
1525
|
+
*/
|
|
1526
|
+
public getDropdownElement() {
|
|
1527
|
+
return this._dropdownContentElement;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Get value display element
|
|
1532
|
+
*/
|
|
1533
|
+
public getValueDisplayElement() {
|
|
1534
|
+
return this._valueDisplayElement;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Show all options in the dropdown
|
|
1539
|
+
*/
|
|
1540
|
+
public showAllOptions() {
|
|
1541
|
+
// Get all options in the dropdown
|
|
1542
|
+
const options = Array.from(
|
|
1543
|
+
this._wrapperElement.querySelectorAll(`[data-kt-select-option]`),
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
// Show all options by removing the hidden class and any inline styles
|
|
1547
|
+
options.forEach((option) => {
|
|
1548
|
+
// Remove hidden class
|
|
1549
|
+
option.classList.remove('hidden');
|
|
1550
|
+
|
|
1551
|
+
// Clean up any existing inline styles for backward compatibility
|
|
1552
|
+
if (option.hasAttribute('style')) {
|
|
1553
|
+
const styleAttr = option.getAttribute('style');
|
|
1554
|
+
|
|
1555
|
+
if (styleAttr.includes('display:')) {
|
|
1556
|
+
// If style only contains display property, remove the entire attribute
|
|
1557
|
+
if (
|
|
1558
|
+
styleAttr.trim() === 'display: none;' ||
|
|
1559
|
+
styleAttr.trim() === 'display: block;'
|
|
1560
|
+
) {
|
|
1561
|
+
option.removeAttribute('style');
|
|
1562
|
+
} else {
|
|
1563
|
+
// Otherwise, remove just the display property
|
|
1564
|
+
option.setAttribute(
|
|
1565
|
+
'style',
|
|
1566
|
+
styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// If search input exists, clear it
|
|
1574
|
+
if (this._searchInputElement && this._config.mode !== SelectMode.COMBOBOX) {
|
|
1575
|
+
this._searchInputElement.value = '';
|
|
1576
|
+
// If we have a search module, clear any search filtering
|
|
1577
|
+
if (this._searchModule) {
|
|
1578
|
+
this._searchModule.clearSearchHighlights();
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Toggle multi-select functionality
|
|
1585
|
+
*/
|
|
1586
|
+
public enableMultiSelect() {
|
|
1587
|
+
this._state.modifyConfig({ multiple: true });
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Disable multi-select functionality
|
|
1592
|
+
*/
|
|
1593
|
+
public disableMultiSelect() {
|
|
1594
|
+
this._state.modifyConfig({ multiple: false });
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Toggle the selection of an option
|
|
1599
|
+
*/
|
|
1600
|
+
public toggleSelection(value: string): void {
|
|
1601
|
+
// Get current selection state
|
|
1602
|
+
const isSelected = this._state.isSelected(value);
|
|
1603
|
+
if (this._config.debug)
|
|
1604
|
+
console.log(
|
|
1605
|
+
`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}, closeOnSelect: ${this._config.closeOnSelect}`,
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
// If already selected in single select mode, do nothing (can't deselect in single select)
|
|
1609
|
+
if (isSelected && !this._config.multiple) {
|
|
1610
|
+
if (this._config.debug)
|
|
1611
|
+
console.log(
|
|
1612
|
+
'Early return from toggleSelection - already selected in single select mode',
|
|
1613
|
+
);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (this._config.debug)
|
|
1618
|
+
console.log(
|
|
1619
|
+
`Toggling selection for option: ${value}, currently selected: ${isSelected}`,
|
|
1620
|
+
);
|
|
1621
|
+
|
|
1622
|
+
// Ensure any search highlights are cleared when selection changes
|
|
1623
|
+
if (this._searchModule) {
|
|
1624
|
+
this._searchModule.clearSearchHighlights();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Toggle the selection in the state
|
|
1628
|
+
this._state.toggleSelectedOptions(value);
|
|
1629
|
+
|
|
1630
|
+
// Update the original select element's option selected state
|
|
1631
|
+
const optionEl = Array.from(this._element.querySelectorAll('option')).find(
|
|
1632
|
+
(opt) => opt.value === value,
|
|
1633
|
+
) as HTMLOptionElement;
|
|
1634
|
+
|
|
1635
|
+
if (optionEl) {
|
|
1636
|
+
// For multiple select, toggle the 'selected' attribute
|
|
1637
|
+
if (this._config.multiple) {
|
|
1638
|
+
optionEl.selected = !isSelected;
|
|
1639
|
+
} else {
|
|
1640
|
+
// For single select, deselect all other options and select this one
|
|
1641
|
+
Array.from(this._element.querySelectorAll('option')).forEach((opt) => {
|
|
1642
|
+
(opt as HTMLOptionElement).selected = opt.value === value;
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Update the display element value
|
|
1648
|
+
this.updateSelectedOptionDisplay();
|
|
1649
|
+
|
|
1650
|
+
// Update option classes without re-rendering the dropdown content
|
|
1651
|
+
this._updateSelectedOptionClass();
|
|
1652
|
+
|
|
1653
|
+
// For single select mode, always close the dropdown after selection
|
|
1654
|
+
// For multiple select mode, only close if closeOnSelect is true
|
|
1655
|
+
if (!this._config.multiple) {
|
|
1656
|
+
if (this._config.debug)
|
|
1657
|
+
console.log(
|
|
1658
|
+
'About to call closeDropdown() for single select mode - always close after selection',
|
|
1659
|
+
);
|
|
1660
|
+
this.closeDropdown();
|
|
1661
|
+
} else if (this._config.closeOnSelect) {
|
|
1662
|
+
if (this._config.debug)
|
|
1663
|
+
console.log(
|
|
1664
|
+
'About to call closeDropdown() for multiple select with closeOnSelect:true',
|
|
1665
|
+
);
|
|
1666
|
+
this.closeDropdown();
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Dispatch custom change event with additional data
|
|
1670
|
+
this._dispatchEvent('change', {
|
|
1671
|
+
value: value,
|
|
1672
|
+
selected: !isSelected,
|
|
1673
|
+
selectedOptions: this.getSelectedOptions(),
|
|
1674
|
+
});
|
|
1675
|
+
this._fireEvent('change', {
|
|
1676
|
+
value: value,
|
|
1677
|
+
selected: !isSelected,
|
|
1678
|
+
selectedOptions: this.getSelectedOptions(),
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Clean up all resources when the component is destroyed
|
|
1684
|
+
* This overrides the parent dispose method
|
|
1685
|
+
*/
|
|
1686
|
+
public override dispose(): void {
|
|
1687
|
+
// Clean up event listeners
|
|
1688
|
+
this._eventManager.removeAllListeners(null);
|
|
1689
|
+
|
|
1690
|
+
// Dispose modules
|
|
1691
|
+
if (this._dropdownModule) {
|
|
1692
|
+
this._dropdownModule.dispose();
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (this._comboboxModule) {
|
|
1696
|
+
if (typeof this._comboboxModule.destroy === 'function') {
|
|
1697
|
+
this._comboboxModule.destroy();
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (this._tagsModule) {
|
|
1702
|
+
if (typeof this._tagsModule.destroy === 'function') {
|
|
1703
|
+
this._tagsModule.destroy();
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (this._searchModule) {
|
|
1708
|
+
if (typeof this._searchModule.destroy === 'function') {
|
|
1709
|
+
this._searchModule.destroy();
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Remove DOM elements
|
|
1714
|
+
if (this._wrapperElement && this._wrapperElement.parentNode) {
|
|
1715
|
+
this._wrapperElement.parentNode.removeChild(this._wrapperElement);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Call parent dispose to clean up data
|
|
1719
|
+
super.dispose();
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* ========================================================================
|
|
1724
|
+
* STATIC METHODS
|
|
1725
|
+
* ========================================================================
|
|
1726
|
+
*/
|
|
1727
|
+
|
|
1728
|
+
private static readonly _instances = new Map<HTMLElement, KTSelect>();
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Create instances of KTSelect for all matching elements
|
|
1732
|
+
*/
|
|
1733
|
+
public static createInstances(): void {
|
|
1734
|
+
const elements = document.querySelectorAll<HTMLElement>('[data-kt-select]');
|
|
1735
|
+
|
|
1736
|
+
elements.forEach((element) => {
|
|
1737
|
+
if (
|
|
1738
|
+
element.hasAttribute('data-kt-select') &&
|
|
1739
|
+
!element.classList.contains('data-kt-select-initialized')
|
|
1740
|
+
) {
|
|
1741
|
+
const instance = new KTSelect(element);
|
|
1742
|
+
this._instances.set(element, instance);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Initialize all KTSelect instances
|
|
1749
|
+
*/
|
|
1750
|
+
public static init(): void {
|
|
1751
|
+
KTSelect.createInstances();
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Handle remote search
|
|
1756
|
+
* @param event Input event
|
|
1757
|
+
*/
|
|
1758
|
+
private _handleRemoteSearch(event: Event) {
|
|
1759
|
+
if (
|
|
1760
|
+
!this._remoteModule ||
|
|
1761
|
+
!this._config.remote ||
|
|
1762
|
+
!this._config.searchParam
|
|
1763
|
+
)
|
|
1764
|
+
return;
|
|
1765
|
+
|
|
1766
|
+
const query = (event.target as HTMLInputElement).value;
|
|
1767
|
+
|
|
1768
|
+
// Check if the query is long enough
|
|
1769
|
+
if (query.length < (this._config.searchMinLength || 0)) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Debounce the search
|
|
1774
|
+
if (this._searchDebounceTimeout) {
|
|
1775
|
+
clearTimeout(this._searchDebounceTimeout);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
this._searchDebounceTimeout = window.setTimeout(() => {
|
|
1779
|
+
// Show loading state
|
|
1780
|
+
this._renderSearchLoadingState();
|
|
1781
|
+
|
|
1782
|
+
// Fetch remote data with search query
|
|
1783
|
+
this._remoteModule
|
|
1784
|
+
.fetchData(query)
|
|
1785
|
+
.then((items) => {
|
|
1786
|
+
// Update state with fetched items
|
|
1787
|
+
this._state
|
|
1788
|
+
.setItems(items)
|
|
1789
|
+
.then(() => {
|
|
1790
|
+
// Update options in the dropdown
|
|
1791
|
+
this._updateSearchResults(items);
|
|
1792
|
+
|
|
1793
|
+
// Refresh the search module's option cache if search is enabled
|
|
1794
|
+
if (this._searchModule && this._config.enableSearch) {
|
|
1795
|
+
this._searchModule.refreshOptionCache();
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1798
|
+
.catch((error) => {
|
|
1799
|
+
console.error('Error updating search results:', error);
|
|
1800
|
+
this._renderSearchErrorState(
|
|
1801
|
+
error.message || 'Failed to load search results',
|
|
1802
|
+
);
|
|
1803
|
+
});
|
|
1804
|
+
})
|
|
1805
|
+
.catch((error) => {
|
|
1806
|
+
console.error('Error fetching search results:', error);
|
|
1807
|
+
this._renderSearchErrorState(
|
|
1808
|
+
this._remoteModule.getErrorMessage() ||
|
|
1809
|
+
'Failed to load search results',
|
|
1810
|
+
);
|
|
1811
|
+
});
|
|
1812
|
+
}, this._config.searchDebounce || 300);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Search debounce timeout
|
|
1816
|
+
private _searchDebounceTimeout: number | null = null;
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Render loading state for search
|
|
1820
|
+
*/
|
|
1821
|
+
private _renderSearchLoadingState() {
|
|
1822
|
+
if (!this._originalOptionsHtml && this._dropdownContentElement) {
|
|
1823
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
1824
|
+
'[data-kt-select-options-container]',
|
|
1825
|
+
);
|
|
1826
|
+
if (optionsContainer) {
|
|
1827
|
+
this._originalOptionsHtml = optionsContainer.innerHTML;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
this._showDropdownMessage('loading', 'Searching...');
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Store original options HTML for restoring after search
|
|
1834
|
+
private _originalOptionsHtml: string | null = null;
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Render error state for search
|
|
1838
|
+
* @param message Error message
|
|
1839
|
+
*/
|
|
1840
|
+
private _renderSearchErrorState(message: string) {
|
|
1841
|
+
this._showDropdownMessage('error', message);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* Update search results in the dropdown
|
|
1846
|
+
* @param items Search result items
|
|
1847
|
+
*/
|
|
1848
|
+
private _updateSearchResults(items: KTSelectOptionData[]) {
|
|
1849
|
+
if (!this._dropdownContentElement) return;
|
|
1850
|
+
|
|
1851
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
1852
|
+
'[data-kt-select-options-container]',
|
|
1853
|
+
);
|
|
1854
|
+
if (!optionsContainer) return;
|
|
1855
|
+
|
|
1856
|
+
// Clear current options
|
|
1857
|
+
optionsContainer.innerHTML = '';
|
|
1858
|
+
|
|
1859
|
+
if (items.length === 0) {
|
|
1860
|
+
// Show no results message using template for consistency and customization
|
|
1861
|
+
const noResultsElement = defaultTemplates.noResults(this._config);
|
|
1862
|
+
optionsContainer.appendChild(noResultsElement);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Process each item individually to create options
|
|
1867
|
+
items.forEach((item) => {
|
|
1868
|
+
// Create option for the original select
|
|
1869
|
+
const selectOption = defaultTemplates.emptyOption({
|
|
1870
|
+
...this._config,
|
|
1871
|
+
placeholder: item.title,
|
|
1872
|
+
});
|
|
1873
|
+
selectOption.value = item.id;
|
|
1874
|
+
if (item.description) {
|
|
1875
|
+
selectOption.setAttribute(
|
|
1876
|
+
'data-kt-select-option-description',
|
|
1877
|
+
item.description,
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (item.icon) {
|
|
1881
|
+
selectOption.setAttribute('data-kt-select-option-icon', item.icon);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// Create option element for the dropdown
|
|
1885
|
+
const ktOption = new KTSelectOption(selectOption, this._config);
|
|
1886
|
+
const renderedOption = ktOption.render();
|
|
1887
|
+
|
|
1888
|
+
// Add to dropdown container
|
|
1889
|
+
optionsContainer.appendChild(renderedOption);
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
// Add pagination "Load More" button if needed
|
|
1893
|
+
if (this._config.pagination && this._remoteModule.hasMorePages()) {
|
|
1894
|
+
this._addLoadMoreButton();
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Update options NodeList
|
|
1898
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
1899
|
+
`[data-kt-select-option]`,
|
|
1900
|
+
) as NodeListOf<HTMLElement>;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Filter options by query
|
|
1905
|
+
*/
|
|
1906
|
+
public filterOptions(query: string): void {
|
|
1907
|
+
this._filterOptionsForCombobox(query);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Check if dropdown is open
|
|
1912
|
+
*/
|
|
1913
|
+
public isDropdownOpen(): boolean {
|
|
1914
|
+
return this._dropdownIsOpen;
|
|
1915
|
+
}
|
|
1916
|
+
}
|