@keenthemes/ktui 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/ktui.js +738 -700
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +5824 -0
  5. package/examples/select/avatar.html +47 -0
  6. package/examples/select/basic-usage.html +10 -14
  7. package/examples/select/{test.html → combobox-icons_.html} +13 -48
  8. package/examples/select/country.html +43 -0
  9. package/examples/select/description.html +25 -41
  10. package/examples/select/disable-option.html +10 -16
  11. package/examples/select/disable-select.html +7 -6
  12. package/examples/select/icon-multiple.html +23 -31
  13. package/examples/select/icon.html +20 -30
  14. package/examples/select/max-selection.html +8 -9
  15. package/examples/select/modal.html +16 -17
  16. package/examples/select/multiple.html +11 -13
  17. package/examples/select/placeholder.html +9 -12
  18. package/examples/select/search.html +30 -22
  19. package/examples/select/sizes.html +94 -0
  20. package/examples/select/template-customization.html +0 -3
  21. package/lib/cjs/components/component.js +1 -1
  22. package/lib/cjs/components/component.js.map +1 -1
  23. package/lib/cjs/components/datatable/datatable.js +14 -11
  24. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  25. package/lib/cjs/components/select/combobox.js +96 -61
  26. package/lib/cjs/components/select/combobox.js.map +1 -1
  27. package/lib/cjs/components/select/config.js +13 -8
  28. package/lib/cjs/components/select/config.js.map +1 -1
  29. package/lib/cjs/components/select/dropdown.js +32 -96
  30. package/lib/cjs/components/select/dropdown.js.map +1 -1
  31. package/lib/cjs/components/select/option.js +53 -20
  32. package/lib/cjs/components/select/option.js.map +1 -1
  33. package/lib/cjs/components/select/search.js +146 -97
  34. package/lib/cjs/components/select/search.js.map +1 -1
  35. package/lib/cjs/components/select/select.js +219 -118
  36. package/lib/cjs/components/select/select.js.map +1 -1
  37. package/lib/cjs/components/select/tags.js +0 -26
  38. package/lib/cjs/components/select/tags.js.map +1 -1
  39. package/lib/cjs/components/select/templates.js +130 -105
  40. package/lib/cjs/components/select/templates.js.map +1 -1
  41. package/lib/cjs/components/select/utils.js +33 -132
  42. package/lib/cjs/components/select/utils.js.map +1 -1
  43. package/lib/cjs/helpers/dom.js +0 -24
  44. package/lib/cjs/helpers/dom.js.map +1 -1
  45. package/lib/esm/components/component.js +1 -1
  46. package/lib/esm/components/component.js.map +1 -1
  47. package/lib/esm/components/datatable/datatable.js +14 -11
  48. package/lib/esm/components/datatable/datatable.js.map +1 -1
  49. package/lib/esm/components/select/combobox.js +96 -61
  50. package/lib/esm/components/select/combobox.js.map +1 -1
  51. package/lib/esm/components/select/config.js +13 -8
  52. package/lib/esm/components/select/config.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js +32 -96
  54. package/lib/esm/components/select/dropdown.js.map +1 -1
  55. package/lib/esm/components/select/option.js +53 -20
  56. package/lib/esm/components/select/option.js.map +1 -1
  57. package/lib/esm/components/select/search.js +146 -97
  58. package/lib/esm/components/select/search.js.map +1 -1
  59. package/lib/esm/components/select/select.js +219 -118
  60. package/lib/esm/components/select/select.js.map +1 -1
  61. package/lib/esm/components/select/tags.js +0 -26
  62. package/lib/esm/components/select/tags.js.map +1 -1
  63. package/lib/esm/components/select/templates.js +130 -105
  64. package/lib/esm/components/select/templates.js.map +1 -1
  65. package/lib/esm/components/select/utils.js +32 -130
  66. package/lib/esm/components/select/utils.js.map +1 -1
  67. package/lib/esm/helpers/dom.js +0 -24
  68. package/lib/esm/helpers/dom.js.map +1 -1
  69. package/package.json +9 -6
  70. package/src/components/component.ts +0 -4
  71. package/src/components/datatable/datatable.ts +14 -11
  72. package/src/components/input/input.css +1 -1
  73. package/src/components/scrollable/scrollable.css +9 -5
  74. package/src/components/select/combobox.ts +98 -87
  75. package/src/components/select/config.ts +16 -13
  76. package/src/components/select/dropdown.ts +43 -108
  77. package/src/components/select/option.ts +44 -25
  78. package/src/components/select/search.ts +158 -117
  79. package/src/components/select/select.css +99 -27
  80. package/src/components/select/select.ts +236 -128
  81. package/src/components/select/tags.ts +1 -27
  82. package/src/components/select/templates.ts +191 -132
  83. package/src/components/select/utils.ts +30 -166
  84. package/src/components/toast/toast.css +1 -1
  85. package/src/helpers/dom.ts +0 -30
  86. package/webpack.config.js +6 -1
  87. package/examples/select/combobox-icons.html +0 -58
  88. package/examples/select/icon-description.html +0 -56
  89. /package/examples/select/{combobox.html → combobox_.html} +0 -0
  90. /package/examples/select/{remote-data.html → remote-data_.html} +0 -0
  91. /package/examples/select/{tags-icons.html → tags-icons_.html} +0 -0
  92. /package/examples/select/{tags-selected.html → tags-selected_.html} +0 -0
  93. /package/examples/select/{tags.html → tags_.html} +0 -0
@@ -37,7 +37,6 @@ export class KTSelect extends KTComponent {
37
37
  private _displayElement: HTMLElement;
38
38
  private _dropdownContentElement: HTMLElement;
39
39
  private _searchInputElement: HTMLInputElement | null;
40
- private _valueDisplayElement: HTMLElement;
41
40
  private _options: NodeListOf<HTMLElement>;
42
41
 
43
42
  // State
@@ -52,6 +51,7 @@ export class KTSelect extends KTComponent {
52
51
  private _focusManager: FocusManager;
53
52
  private _eventManager: EventManager;
54
53
  private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
54
+ private _mutationObserver: MutationObserver | null = null;
55
55
 
56
56
  /**
57
57
  * Constructor: Initializes the select component
@@ -167,21 +167,23 @@ export class KTSelect extends KTComponent {
167
167
  );
168
168
  if (!optionsContainer) return;
169
169
 
170
+ // Clear previous messages
171
+ optionsContainer.innerHTML = '';
172
+
170
173
  switch (type) {
171
174
  case 'error':
172
- optionsContainer.innerHTML = defaultTemplates.error({
175
+ optionsContainer.appendChild(defaultTemplates.error({
173
176
  ...this._config,
174
177
  errorMessage: message,
175
- });
178
+ }));
176
179
  break;
177
180
  case 'loading':
178
- optionsContainer.innerHTML = defaultTemplates.loading(
181
+ optionsContainer.appendChild(defaultTemplates.loading(
179
182
  this._config,
180
183
  message || 'Loading...',
181
- ).outerHTML;
184
+ ));
182
185
  break;
183
186
  case 'empty':
184
- optionsContainer.innerHTML = '';
185
187
  optionsContainer.appendChild(defaultTemplates.empty(this._config));
186
188
  break;
187
189
  }
@@ -397,6 +399,7 @@ export class KTSelect extends KTComponent {
397
399
  this._displayElement,
398
400
  this._dropdownContentElement,
399
401
  this._config,
402
+ this, // Pass the KTSelect instance to KTSelectDropdown
400
403
  );
401
404
 
402
405
  // Update display and set ARIA attributes
@@ -406,14 +409,9 @@ export class KTSelect extends KTComponent {
406
409
 
407
410
  // Attach event listeners after all modules are initialized
408
411
  this._attachEventListeners();
409
- }
410
412
 
411
- /**
412
- * Initialize options HTML from data
413
- */
414
- // private _initializeOptionsHtml() {
415
- // this._generateOptionsHtml(this._element);
416
- // }
413
+ this._observeNativeSelect();
414
+ }
417
415
 
418
416
  /**
419
417
  * Creates the HTML structure for the select component
@@ -431,7 +429,17 @@ export class KTSelect extends KTComponent {
431
429
 
432
430
  // Move classes from original select to display element
433
431
  if (this._element.classList.length > 0) {
434
- displayElement.classList.add(...Array.from(this._element.classList));
432
+ // Exclude kt-select class from being added to the wrapper element
433
+ const classes = Array.from(this._element.classList).filter(
434
+ (className) => className !== 'kt-select',
435
+ );
436
+ wrapperElement.classList.add(...classes);
437
+
438
+ // If element has class kt-select, move it to display element
439
+ if (this._element.classList.contains('kt-select')) {
440
+ displayElement.classList.add('kt-select');
441
+ }
442
+
435
443
  this._element.className = '';
436
444
  }
437
445
 
@@ -480,7 +488,7 @@ export class KTSelect extends KTComponent {
480
488
 
481
489
  // Insert after the original element
482
490
  this._element.after(wrapperElement);
483
- this._element.style.display = 'none';
491
+ this._element.classList.add('hidden');
484
492
  }
485
493
 
486
494
  /**
@@ -514,10 +522,6 @@ export class KTSelect extends KTComponent {
514
522
  this._searchInputElement = this._displayElement as HTMLInputElement;
515
523
  }
516
524
 
517
- this._valueDisplayElement = this._wrapperElement.querySelector(
518
- `[data-kt-select-value]`,
519
- ) as HTMLElement;
520
-
521
525
  this._options = this._wrapperElement.querySelectorAll(
522
526
  `[data-kt-select-option]`,
523
527
  ) as NodeListOf<HTMLElement>;
@@ -537,17 +541,10 @@ export class KTSelect extends KTComponent {
537
541
  this._handleDropdownOptionClick.bind(this),
538
542
  );
539
543
 
540
- // Only attach click handler to display element
541
- // this._eventManager.addListener(
542
- // this._wrapperElement,
543
- // 'click',
544
- // this._handleDropdownClick.bind(this),
545
- // );
546
-
547
- // Attach centralized keyboard handler
548
- const keyboardTarget = this._searchInputElement || this._wrapperElement;
549
- if (keyboardTarget) {
550
- keyboardTarget.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
544
+ // Attach centralized keyboard handler to the wrapper element.
545
+ // Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
546
+ if (this._wrapperElement) {
547
+ this._wrapperElement.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
551
548
  }
552
549
  }
553
550
 
@@ -729,28 +726,6 @@ export class KTSelect extends KTComponent {
729
726
  * ========================================================================
730
727
  */
731
728
 
732
- /**
733
- * Toggle dropdown visibility
734
- * @deprecated
735
- */
736
- public toggleDropdown() {
737
- if (this._config.disabled) {
738
- if (this._config.debug) console.log('toggleDropdown: select is disabled, not opening');
739
- return;
740
- }
741
- if (this._config.debug) console.log('toggleDropdown called');
742
- if (this._dropdownModule) {
743
- // Always use the dropdown module's state to determine whether to open or close
744
- if (this._dropdownModule.isOpen()) {
745
- if (this._config.debug) console.log('Dropdown is open, closing...');
746
- this.closeDropdown();
747
- } else {
748
- if (this._config.debug) console.log('Dropdown is closed, opening...');
749
- this.openDropdown();
750
- }
751
- }
752
- }
753
-
754
729
  /**
755
730
  * Open the dropdown
756
731
  */
@@ -791,17 +766,6 @@ export class KTSelect extends KTComponent {
791
766
  this._dispatchEvent('show');
792
767
  this._fireEvent('show');
793
768
 
794
- // Focus search input if configured and exists
795
- if (
796
- this._config.enableSearch &&
797
- this._config.searchAutofocus &&
798
- this._searchInputElement
799
- ) {
800
- setTimeout(() => {
801
- this._searchInputElement.focus();
802
- }, 50);
803
- }
804
-
805
769
  // Update ARIA states
806
770
  this._setAriaAttributes();
807
771
 
@@ -830,15 +794,15 @@ export class KTSelect extends KTComponent {
830
794
  if (this._config.debug)
831
795
  console.log('Closing dropdown via dropdownModule...');
832
796
 
833
- // Clear search input and highlights if the dropdown is closing
797
+ // Clear search input if the dropdown is closing
834
798
  if (this._searchModule && this._searchInputElement) {
835
799
  // Clear search input if configured to do so
836
800
  if (this._config.clearSearchOnClose) {
837
801
  this._searchInputElement.value = '';
838
802
  }
839
803
 
840
- // Always clear the highlights when dropdown closes
841
- this._searchModule.clearSearchHighlights();
804
+ // Clear search input when dropdown closes
805
+ this._searchModule.clearSearch();
842
806
  }
843
807
 
844
808
  // Set our internal flag to match what we're doing
@@ -878,11 +842,12 @@ export class KTSelect extends KTComponent {
878
842
  const selectedOptions = this.getSelectedOptions();
879
843
  if (selectedOptions.length === 0) return;
880
844
 
881
- // Get the first selected option element
882
- const firstSelectedValue = selectedOptions[0];
883
-
884
- // Use the FocusManager to focus on the option
885
- this._focusManager.focusOptionByValue(firstSelectedValue);
845
+ // Iterate through selected options and focus the first one that is visible
846
+ for (const value of selectedOptions) {
847
+ if (this._focusManager && this._focusManager.focusOptionByValue(value)) {
848
+ break; // Stop after focusing the first found selected and visible option
849
+ }
850
+ }
886
851
  }
887
852
 
888
853
  /**
@@ -954,38 +919,60 @@ export class KTSelect extends KTComponent {
954
919
  */
955
920
  public updateSelectedOptionDisplay() {
956
921
  const selectedOptions = this.getSelectedOptions();
922
+ const tagsEnabled = this._config.tags && this._tagsModule;
923
+ const valueDisplayEl = this.getValueDisplayElement();
957
924
 
958
- // Tag mode: render tags if enabled
959
- if (this._config.tags && this._tagsModule) {
925
+ if (tagsEnabled) {
926
+ // Tags module will render tags if selectedOptions > 0, or clear them if selectedOptions === 0.
960
927
  this._tagsModule.updateTagsDisplay(selectedOptions);
961
- return;
928
+ }
929
+
930
+ // Guard against valueDisplayEl being null due to template modifications
931
+ if (!valueDisplayEl) {
932
+ if (this._config.debug) {
933
+ console.warn('KTSelect: Value display element is null. Cannot update display or placeholder. Check template for [data-kt-select-value].');
934
+ }
935
+ return; // Nothing to display on if the element is missing
962
936
  }
963
937
 
964
938
  if (typeof this._config.renderSelected === 'function') {
965
- // Use the custom renderSelected function if provided
966
- this._valueDisplayElement.innerHTML = this._config.renderSelected(selectedOptions);
939
+ valueDisplayEl.innerHTML = this._config.renderSelected(selectedOptions);
967
940
  } else {
968
-
969
941
  if (selectedOptions.length === 0) {
970
- const placeholder = defaultTemplates.placeholder(this._config);
971
- this._valueDisplayElement.replaceChildren(placeholder);
972
-
942
+ // No options selected: display placeholder.
943
+ // This runs if tags are off, OR if tags are on but no items are selected (tags module would have cleared tags).
944
+ const placeholderEl = defaultTemplates.placeholder(this._config);
945
+ valueDisplayEl.replaceChildren(placeholderEl);
973
946
  } else {
974
- let content = '';
975
-
976
- if (this._config.displayTemplate) {
977
- const selectedValues = this.getSelectedOptions();
978
- content = this.renderDisplayTemplateForSelected(selectedValues);
947
+ // Options are selected.
948
+ if (tagsEnabled) {
949
+ // Tags are enabled AND options are selected: tags module has rendered them.
950
+ // Clear valueDisplayEl as tags are the primary display.
951
+ valueDisplayEl.innerHTML = '';
979
952
  } else {
980
- // If no displayTemplate is provided, use the default comma-separated list of selected options
981
- content = this.getSelectedOptionsText();
953
+ // Tags are not enabled AND options are selected: render normal text display.
954
+ let content = '';
955
+ if (this._config.displayTemplate) {
956
+ content = this.renderDisplayTemplateForSelected(this.getSelectedOptions());
957
+ } else {
958
+ content = this.getSelectedOptionsText();
959
+ }
960
+ valueDisplayEl.innerHTML = content;
982
961
  }
983
-
984
- this._valueDisplayElement.innerHTML = content;
985
962
  }
986
963
  }
987
964
  }
988
965
 
966
+ /**
967
+ * Check if an option was originally disabled in the HTML
968
+ */
969
+ private _isOptionOriginallyDisabled(value: string): boolean {
970
+ const originalOption = Array.from(this._element.querySelectorAll('option')).find(
971
+ (opt) => opt.value === value
972
+ ) as HTMLOptionElement;
973
+ return originalOption ? originalOption.disabled : false;
974
+ }
975
+
989
976
  /**
990
977
  * Update CSS classes for selected options
991
978
  */
@@ -1007,17 +994,23 @@ export class KTSelect extends KTComponent {
1007
994
  allOptions.forEach((option) => {
1008
995
  const optionValue = option.getAttribute('data-value');
1009
996
  if (!optionValue) return;
997
+
1010
998
  const isSelected = selectedValues.includes(optionValue);
999
+ const isOriginallyDisabled = this._isOptionOriginallyDisabled(optionValue);
1000
+
1011
1001
  if (isSelected) {
1012
1002
  option.classList.add('selected');
1013
1003
  option.setAttribute('aria-selected', 'true');
1004
+ // Selected options should not be visually hidden or disabled by maxSelections logic
1014
1005
  option.classList.remove('hidden');
1015
1006
  option.classList.remove('disabled');
1016
1007
  option.removeAttribute('aria-disabled');
1017
1008
  } else {
1018
1009
  option.classList.remove('selected');
1019
1010
  option.setAttribute('aria-selected', 'false');
1020
- if (maxReached) {
1011
+
1012
+ // An option should be disabled if it was originally disabled OR if maxSelections is reached
1013
+ if (isOriginallyDisabled || maxReached) {
1021
1014
  option.classList.add('disabled');
1022
1015
  option.setAttribute('aria-disabled', 'true');
1023
1016
  } else {
@@ -1085,18 +1078,6 @@ export class KTSelect extends KTComponent {
1085
1078
  * ========================================================================
1086
1079
  */
1087
1080
 
1088
- /**
1089
- * Handle display element click
1090
- * @deprecated
1091
- */
1092
- private _handleDropdownClick(event: Event) {
1093
- if (this._config.debug)
1094
- console.log('Display element clicked', event.target);
1095
- event.preventDefault();
1096
- event.stopPropagation(); // Prevent event bubbling
1097
- this.toggleDropdown();
1098
- }
1099
-
1100
1081
  /**
1101
1082
  * Handle click within the dropdown
1102
1083
  */
@@ -1145,6 +1126,13 @@ export class KTSelect extends KTComponent {
1145
1126
 
1146
1127
  if (this._config.debug) console.log('Option clicked:', optionValue);
1147
1128
 
1129
+ // If in single-select mode and the clicked option is already selected, just close the dropdown.
1130
+ if (!this._config.multiple && this._state.isSelected(optionValue)) {
1131
+ if (this._config.debug) console.log('Single select mode: clicked already selected option. Closing dropdown.');
1132
+ this.closeDropdown();
1133
+ return;
1134
+ }
1135
+
1148
1136
  // Use toggleSelection instead of _selectOption to prevent re-rendering
1149
1137
  this.toggleSelection(optionValue);
1150
1138
  }
@@ -1221,7 +1209,14 @@ export class KTSelect extends KTComponent {
1221
1209
  * Get value display element
1222
1210
  */
1223
1211
  public getValueDisplayElement() {
1224
- return this._valueDisplayElement;
1212
+ return this._displayElement;
1213
+ }
1214
+
1215
+ /**
1216
+ * Get wrapper element
1217
+ */
1218
+ public getWrapperElement(): HTMLElement {
1219
+ return this._wrapperElement;
1225
1220
  }
1226
1221
 
1227
1222
  /**
@@ -1253,7 +1248,7 @@ export class KTSelect extends KTComponent {
1253
1248
  // Otherwise, remove just the display property
1254
1249
  option.setAttribute(
1255
1250
  'style',
1256
- styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
1251
+ styleAttr?.replace(/display:\s*[^;]+;?/gi, '')?.trim(),
1257
1252
  );
1258
1253
  }
1259
1254
  }
@@ -1265,7 +1260,7 @@ export class KTSelect extends KTComponent {
1265
1260
  this._searchInputElement.value = '';
1266
1261
  // If we have a search module, clear any search filtering
1267
1262
  if (this._searchModule) {
1268
- this._searchModule.clearSearchHighlights();
1263
+ this._searchModule.clearSearch();
1269
1264
  }
1270
1265
  }
1271
1266
  }
@@ -1297,7 +1292,7 @@ export class KTSelect extends KTComponent {
1297
1292
  // Get current selection state
1298
1293
  const isSelected = this._state.isSelected(value);
1299
1294
  if (this._config.debug)
1300
- console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}, closeOnSelect: ${this._config.closeOnSelect}`);
1295
+ console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}`);
1301
1296
 
1302
1297
  // If already selected in single select mode, do nothing (can't deselect in single select)
1303
1298
  if (isSelected && !this._config.multiple) {
@@ -1309,9 +1304,9 @@ export class KTSelect extends KTComponent {
1309
1304
  if (this._config.debug)
1310
1305
  console.log(`Toggling selection for option: ${value}, currently selected: ${isSelected}`);
1311
1306
 
1312
- // Ensure any search highlights are cleared when selection changes
1307
+ // Ensure any search input is cleared when selection changes
1313
1308
  if (this._searchModule) {
1314
- this._searchModule.clearSearchHighlights();
1309
+ this._searchModule.clearSearch();
1315
1310
  }
1316
1311
 
1317
1312
  // Toggle the selection in the state
@@ -1341,14 +1336,13 @@ export class KTSelect extends KTComponent {
1341
1336
  this._updateSelectedOptionClass();
1342
1337
 
1343
1338
  // For single select mode, always close the dropdown after selection
1344
- // For multiple select mode, only close if closeOnSelect is true
1345
1339
  if (!this._config.multiple) {
1346
1340
  if (this._config.debug)
1347
1341
  console.log('About to call closeDropdown() for single select mode - always close after selection');
1348
1342
  this.closeDropdown();
1349
- } else if (this._config.closeOnSelect) {
1343
+ } else {
1350
1344
  if (this._config.debug)
1351
- console.log('About to call closeDropdown() for multiple select with closeOnSelect:true');
1345
+ console.log('About to call closeDropdown() for multiple select');
1352
1346
  this.closeDropdown();
1353
1347
  }
1354
1348
 
@@ -1476,9 +1470,9 @@ export class KTSelect extends KTComponent {
1476
1470
  // Update options in the dropdown
1477
1471
  this._updateSearchResults(items);
1478
1472
 
1479
- // Refresh the search module's option cache if search is enabled
1480
- if (this._searchModule && this._config.enableSearch) {
1481
- this._searchModule.refreshOptionCache();
1473
+ // Refresh the search module to update focus and cache
1474
+ if (this._searchModule) {
1475
+ this._searchModule.refreshAfterSearch();
1482
1476
  }
1483
1477
  })
1484
1478
  .catch((error) => {
@@ -1606,20 +1600,49 @@ export class KTSelect extends KTComponent {
1606
1600
  * Centralized keyboard event handler for all select modes
1607
1601
  */
1608
1602
  private _handleKeyboardEvent(event: KeyboardEvent) {
1603
+ // If the event target is the search input and the event was already handled (defaultPrevented),
1604
+ // then return early to avoid duplicate processing by this broader handler.
1605
+ if (event.target === this._searchInputElement && event.defaultPrevented) {
1606
+ return;
1607
+ }
1608
+
1609
1609
  const isOpen = this._dropdownIsOpen;
1610
1610
  const config = this._config;
1611
1611
  const focusManager = this._focusManager;
1612
1612
  const buffer = this._typeToSearchBuffer;
1613
1613
 
1614
- // Ignore modifier keys
1614
+ // If the event target is the search input, let it handle most typing keys naturally.
1615
+ if (event.target === this._searchInputElement) {
1616
+ // Allow navigation keys like ArrowDown, ArrowUp, Escape, Enter (for search/selection) to be handled by the logic below.
1617
+ // For other keys (characters, space, backspace, delete), let the input field process them.
1618
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' &&
1619
+ event.key !== 'Escape' && event.key !== 'Enter' && event.key !== 'Tab' &&
1620
+ event.key !== 'Home' && event.key !== 'End') {
1621
+ // If it's a character key and we are NOT type-to-searching (because search has focus)
1622
+ // then let the input field handle it for its own value.
1623
+ // The search module's 'input' event will handle filtering based on the input's value.
1624
+ buffer.clear(); // Clear type-to-search buffer when typing in search field
1625
+ return;
1626
+ }
1627
+ // For Enter specifically in search input, we might want to select the focused option or submit search.
1628
+ // This is handled later in the switch.
1629
+ }
1630
+
1631
+ // Ignore modifier keys (except for specific combinations if added later)
1615
1632
  if (event.altKey || event.ctrlKey || event.metaKey) return;
1616
1633
 
1617
- // Type-to-search: only for single char keys
1618
- if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/)) {
1634
+ // Type-to-search: only for single char keys, when search input does not have focus
1635
+ if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/) && document.activeElement !== this._searchInputElement) {
1619
1636
  buffer.push(event.key);
1620
1637
  const str = buffer.getBuffer();
1638
+ if (isOpen) {
1621
1639
  focusManager.focusByString(str);
1622
- return;
1640
+ } else {
1641
+ // If closed, type-to-search could potentially open and select.
1642
+ // For now, let's assume it only works when open or opens it first.
1643
+ // Or, we could find the matching option and set it directly without opening.
1644
+ }
1645
+ return; // Type-to-search handles the event
1623
1646
  }
1624
1647
 
1625
1648
  switch (event.key) {
@@ -1650,18 +1673,29 @@ export class KTSelect extends KTComponent {
1650
1673
  case 'Enter':
1651
1674
  case ' ': // Space
1652
1675
  if (isOpen) {
1653
- const focused = focusManager.getFocusedOption();
1654
- if (focused) {
1655
- const value = focused.dataset.value;
1656
- if (value) {
1657
- this.toggleSelection(value);
1658
- if (!config.multiple && config.closeOnSelect) {
1659
- this.closeDropdown();
1660
- }
1676
+ const focusedOptionEl = this._focusManager.getFocusedOption();
1677
+ if (focusedOptionEl) {
1678
+ const val = focusedOptionEl.dataset.value;
1679
+ // If single select, and the item is already selected, just close.
1680
+ if (val !== undefined && !this._config.multiple && this._state.isSelected(val)) {
1681
+ if (this._config.debug) console.log('Enter on already selected item in single-select mode. Closing.');
1682
+ this.closeDropdown();
1683
+ event.preventDefault();
1684
+ break;
1661
1685
  }
1662
1686
  }
1663
- // Prevent form submit
1664
- event.preventDefault();
1687
+
1688
+ // Proceed with selection if not handled above
1689
+ this.selectFocusedOption();
1690
+
1691
+ // Close dropdown if configured to do so (for new selections)
1692
+ if (!this._config.multiple) {
1693
+ // This will also be true for the case handled above, but closeDropdown is idempotent.
1694
+ // However, the break above prevents this from being reached for that specific case.
1695
+ this.closeDropdown();
1696
+ }
1697
+ event.preventDefault(); // Prevent form submission or other default actions
1698
+ break;
1665
1699
  } else {
1666
1700
  this.openDropdown();
1667
1701
  }
@@ -1707,4 +1741,78 @@ export class KTSelect extends KTComponent {
1707
1741
  ));
1708
1742
  return contentArray.join(displaySeparator);
1709
1743
  }
1744
+
1745
+ public getDisplayElement(): HTMLElement {
1746
+ return this._displayElement;
1747
+ }
1748
+
1749
+ private _observeNativeSelect() {
1750
+ if (this._mutationObserver) return; // Prevent double observers
1751
+ this._mutationObserver = new MutationObserver((mutations) => {
1752
+ let needsRebuild = false;
1753
+ let needsSelectionSync = false;
1754
+
1755
+ for (const mutation of mutations) {
1756
+ if (mutation.type === 'childList') {
1757
+ // Option(s) added or removed
1758
+ needsRebuild = true;
1759
+ } else if (mutation.type === 'attributes' && mutation.target instanceof HTMLOptionElement) {
1760
+ if (mutation.attributeName === 'selected') {
1761
+ needsSelectionSync = true;
1762
+ }
1763
+ }
1764
+ }
1765
+
1766
+ if (needsRebuild) {
1767
+ // Rebuild the custom dropdown options
1768
+ this._rebuildOptionsFromNative();
1769
+ }
1770
+ if (needsSelectionSync) {
1771
+ this._syncSelectionFromNative();
1772
+ }
1773
+ });
1774
+
1775
+ this._mutationObserver.observe(this._element, {
1776
+ childList: true,
1777
+ attributes: true,
1778
+ subtree: true,
1779
+ attributeFilter: ['selected'],
1780
+ });
1781
+ }
1782
+
1783
+ private _rebuildOptionsFromNative() {
1784
+ // Remove and rebuild the custom dropdown options from the native select
1785
+ if (this._dropdownContentElement) {
1786
+ const optionsContainer = this._dropdownContentElement.querySelector('[data-kt-select-options]');
1787
+ if (optionsContainer) {
1788
+ optionsContainer.innerHTML = '';
1789
+ const options = Array.from(this._element.querySelectorAll('option'));
1790
+ options.forEach((optionElement) => {
1791
+ if (
1792
+ optionElement.value === '' &&
1793
+ optionElement.textContent.trim() === ''
1794
+ ) {
1795
+ return;
1796
+ }
1797
+ const selectOption = new KTSelectOption(optionElement, this._config);
1798
+ const renderedOption = selectOption.render();
1799
+ optionsContainer.appendChild(renderedOption);
1800
+ });
1801
+ // Update internal references
1802
+ this._options = this._wrapperElement.querySelectorAll('[data-kt-select-option]') as NodeListOf<HTMLElement>;
1803
+ }
1804
+ }
1805
+ // Sync selection after rebuilding
1806
+ this._syncSelectionFromNative();
1807
+ this.updateSelectedOptionDisplay();
1808
+ this._updateSelectedOptionClass();
1809
+ }
1810
+
1811
+ private _syncSelectionFromNative() {
1812
+ // Sync internal state from the native select's selected options
1813
+ const selected = Array.from(this._element.querySelectorAll('option:checked')).map(opt => (opt as HTMLOptionElement).value);
1814
+ this._state.setSelectedOptions(this._config.multiple ? selected : selected[0] || '');
1815
+ this.updateSelectedOptionDisplay();
1816
+ this._updateSelectedOptionClass();
1817
+ }
1710
1818
  }
@@ -6,7 +6,7 @@
6
6
  import { KTSelectConfigInterface } from './config';
7
7
  import { KTSelect } from './select';
8
8
  import { defaultTemplates } from './templates';
9
- import { EventManager, renderTemplateString } from './utils';
9
+ import { EventManager } from './utils';
10
10
 
11
11
  /**
12
12
  * KTSelectTags - Handles tags-specific functionality for KTSelect
@@ -83,32 +83,6 @@ export class KTSelectTags {
83
83
  });
84
84
  }
85
85
 
86
- /**
87
- * Get the label/text for an option by its value
88
- */
89
- private _getOptionLabel(optionValue: string): string {
90
- // First look for an option element in the dropdown with matching value
91
- const optionElements = this._select.getOptionsElement();
92
- for (const option of Array.from(optionElements)) {
93
- if ((option as HTMLElement).dataset.value === optionValue) {
94
- return (option as HTMLElement).textContent?.trim() || optionValue;
95
- }
96
- }
97
-
98
- // If not found in dropdown, look in original select element
99
- const originalOptions = this._select
100
- .getElement()
101
- .querySelectorAll('option');
102
- for (const option of Array.from(originalOptions)) {
103
- if ((option as HTMLOptionElement).value === optionValue) {
104
- return (option as HTMLOptionElement).textContent?.trim() || optionValue;
105
- }
106
- }
107
-
108
- // If still not found, return the value itself
109
- return optionValue;
110
- }
111
-
112
86
  /**
113
87
  * Remove a tag and its selection
114
88
  */