@keenthemes/ktui 1.0.20 → 1.0.21

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 (98) hide show
  1. package/dist/ktui.js +418 -144
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +139 -31
  5. package/examples/image-input/file-upload-example.html +189 -0
  6. package/examples/select/remote-data_.html +5 -0
  7. package/examples/select/test-optimizations.html +227 -0
  8. package/examples/select/test-remote-search.html +151 -0
  9. package/examples/sticky/README.md +158 -0
  10. package/examples/sticky/debug-sticky.html +144 -0
  11. package/examples/sticky/test-runner.html +175 -0
  12. package/examples/sticky/test-sticky-logic.js +369 -0
  13. package/examples/sticky/test-sticky-positioning.html +386 -0
  14. package/examples/toast/example.html +52 -0
  15. package/lib/cjs/components/component.js +5 -3
  16. package/lib/cjs/components/component.js.map +1 -1
  17. package/lib/cjs/components/datatable/datatable-sort.js +4 -0
  18. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  19. package/lib/cjs/components/datatable/datatable.js +9 -3
  20. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  21. package/lib/cjs/components/image-input/image-input.js +10 -2
  22. package/lib/cjs/components/image-input/image-input.js.map +1 -1
  23. package/lib/cjs/components/select/combobox.js +50 -20
  24. package/lib/cjs/components/select/combobox.js.map +1 -1
  25. package/lib/cjs/components/select/dropdown.js +4 -2
  26. package/lib/cjs/components/select/dropdown.js.map +1 -1
  27. package/lib/cjs/components/select/index.js.map +1 -1
  28. package/lib/cjs/components/select/option.js +2 -1
  29. package/lib/cjs/components/select/option.js.map +1 -1
  30. package/lib/cjs/components/select/remote.js +50 -50
  31. package/lib/cjs/components/select/remote.js.map +1 -1
  32. package/lib/cjs/components/select/search.js +7 -5
  33. package/lib/cjs/components/select/search.js.map +1 -1
  34. package/lib/cjs/components/select/select.js +199 -33
  35. package/lib/cjs/components/select/select.js.map +1 -1
  36. package/lib/cjs/components/select/tags.js +3 -1
  37. package/lib/cjs/components/select/tags.js.map +1 -1
  38. package/lib/cjs/components/select/templates.js.map +1 -1
  39. package/lib/cjs/components/select/utils.js +23 -10
  40. package/lib/cjs/components/select/utils.js.map +1 -1
  41. package/lib/cjs/components/sticky/sticky.js +52 -14
  42. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  43. package/lib/esm/components/component.js +5 -3
  44. package/lib/esm/components/component.js.map +1 -1
  45. package/lib/esm/components/datatable/datatable-sort.js +4 -0
  46. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  47. package/lib/esm/components/datatable/datatable.js +9 -3
  48. package/lib/esm/components/datatable/datatable.js.map +1 -1
  49. package/lib/esm/components/image-input/image-input.js +10 -2
  50. package/lib/esm/components/image-input/image-input.js.map +1 -1
  51. package/lib/esm/components/select/combobox.js +50 -20
  52. package/lib/esm/components/select/combobox.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js +4 -2
  54. package/lib/esm/components/select/dropdown.js.map +1 -1
  55. package/lib/esm/components/select/index.js +1 -1
  56. package/lib/esm/components/select/index.js.map +1 -1
  57. package/lib/esm/components/select/option.js +2 -1
  58. package/lib/esm/components/select/option.js.map +1 -1
  59. package/lib/esm/components/select/remote.js +50 -50
  60. package/lib/esm/components/select/remote.js.map +1 -1
  61. package/lib/esm/components/select/search.js +8 -6
  62. package/lib/esm/components/select/search.js.map +1 -1
  63. package/lib/esm/components/select/select.js +199 -33
  64. package/lib/esm/components/select/select.js.map +1 -1
  65. package/lib/esm/components/select/tags.js +3 -1
  66. package/lib/esm/components/select/tags.js.map +1 -1
  67. package/lib/esm/components/select/templates.js.map +1 -1
  68. package/lib/esm/components/select/utils.js +23 -10
  69. package/lib/esm/components/select/utils.js.map +1 -1
  70. package/lib/esm/components/sticky/sticky.js +52 -14
  71. package/lib/esm/components/sticky/sticky.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/components/component.ts +12 -11
  74. package/src/components/datatable/datatable-sort.ts +6 -0
  75. package/src/components/datatable/datatable.ts +90 -81
  76. package/src/components/image-input/image-input.ts +11 -2
  77. package/src/components/image-input/types.ts +1 -0
  78. package/src/components/input/input-group.css +1 -1
  79. package/src/components/input/input.css +1 -1
  80. package/src/components/scrollable/scrollable.css +3 -3
  81. package/src/components/select/combobox.ts +84 -34
  82. package/src/components/select/dropdown.ts +20 -11
  83. package/src/components/select/index.ts +6 -1
  84. package/src/components/select/option.ts +7 -6
  85. package/src/components/select/remote.ts +51 -52
  86. package/src/components/select/search.ts +51 -45
  87. package/src/components/select/select.css +12 -11
  88. package/src/components/select/select.ts +371 -102
  89. package/src/components/select/tags.ts +9 -3
  90. package/src/components/select/templates.ts +1 -4
  91. package/src/components/select/utils.ts +55 -20
  92. package/src/components/select/variants.css +0 -1
  93. package/src/components/sticky/sticky.ts +47 -16
  94. package/src/components/sticky/types.ts +3 -0
  95. package/src/components/table/table.css +1 -1
  96. package/src/components/textarea/textarea.css +1 -1
  97. package/src/components/toast/toast.css +84 -47
  98. package/src/components/toast/types.ts +3 -0
@@ -39,6 +39,9 @@ export class KTSelect extends KTComponent {
39
39
  private _searchInputElement: HTMLInputElement | null;
40
40
  private _options: NodeListOf<HTMLElement>;
41
41
 
42
+ // Cached DOM references for performance
43
+ private _optionsContainer: HTMLElement | null = null;
44
+
42
45
  // State
43
46
  private _dropdownIsOpen: boolean = false;
44
47
  private _state: KTSelectState;
@@ -104,6 +107,11 @@ export class KTSelect extends KTComponent {
104
107
  if (this._config.debug)
105
108
  console.log('Initializing remote data with URL:', this._config.dataUrl);
106
109
 
110
+ // For remote data, we need to create the HTML structure first
111
+ // so that the component can be properly initialized
112
+ this._createHtmlStructure();
113
+ this._setupElementReferences();
114
+
107
115
  // Show loading state
108
116
  this._renderLoadingState();
109
117
 
@@ -125,7 +133,12 @@ export class KTSelect extends KTComponent {
125
133
 
126
134
  if (this._config.debug)
127
135
  console.log('Generating options HTML from remote data');
128
- this._setupComponent();
136
+
137
+ // Update the dropdown to show the new options
138
+ this._updateDropdownWithNewOptions();
139
+
140
+ // Complete the component setup with the fetched data
141
+ this._completeRemoteSetup();
129
142
 
130
143
  // Add pagination "Load More" button if needed
131
144
  if (this._config.pagination && this._remoteModule.hasMorePages()) {
@@ -156,6 +169,145 @@ export class KTSelect extends KTComponent {
156
169
  options.forEach((option) => option.remove());
157
170
  }
158
171
 
172
+ /**
173
+ * Unified method to render options in dropdown - eliminates code duplication
174
+ */
175
+ private _renderOptionsInDropdown(
176
+ optionsData: KTSelectOptionData[] | HTMLOptionElement[],
177
+ clearContainer: boolean = true,
178
+ ): void {
179
+ if (!this._dropdownContentElement) return;
180
+
181
+ // Use cached options container for better performance
182
+ const optionsContainer =
183
+ this._optionsContainer ||
184
+ this._dropdownContentElement.querySelector('[data-kt-select-options]');
185
+ if (!optionsContainer) return;
186
+
187
+ // Clear container if requested
188
+ if (clearContainer) {
189
+ optionsContainer.innerHTML = '';
190
+ }
191
+
192
+ // Use DocumentFragment for efficient DOM manipulation
193
+ const fragment = document.createDocumentFragment();
194
+
195
+ // Process options data
196
+ optionsData.forEach((optionData) => {
197
+ let optionElement: HTMLOptionElement;
198
+
199
+ // Handle different input types
200
+ if (optionData instanceof HTMLOptionElement) {
201
+ // Skip empty placeholder options
202
+ if (optionData.value === '' && optionData.textContent.trim() === '') {
203
+ return;
204
+ }
205
+ optionElement = optionData;
206
+ } else {
207
+ // Handle KTSelectOptionData objects - cast to ensure type safety
208
+ const dataItem = optionData as KTSelectOptionData;
209
+ optionElement = document.createElement('option');
210
+ optionElement.value = dataItem.id || '';
211
+ optionElement.textContent = dataItem.title || '';
212
+
213
+ if (dataItem.selected) {
214
+ optionElement.setAttribute('selected', 'selected');
215
+ }
216
+ if (dataItem.disabled) {
217
+ optionElement.setAttribute('disabled', 'disabled');
218
+ }
219
+ }
220
+
221
+ // Create KTSelectOption instance for proper rendering
222
+ const selectOption = new KTSelectOption(optionElement, this._config);
223
+ const renderedOption = selectOption.render();
224
+
225
+ // Add to fragment for batch DOM operation
226
+ fragment.appendChild(renderedOption);
227
+ });
228
+
229
+ // Batch append all options at once
230
+ optionsContainer.appendChild(fragment);
231
+
232
+ // Update options NodeList
233
+ this._options = this._wrapperElement.querySelectorAll(
234
+ '[data-kt-select-option]',
235
+ ) as NodeListOf<HTMLElement>;
236
+
237
+ if (this._config.debug) {
238
+ console.log(`Rendered ${optionsData.length} options in dropdown`);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Update dropdown with new options from the original select element
244
+ */
245
+ private _updateDropdownWithNewOptions() {
246
+ // Get all options from the original select element
247
+ const options = Array.from(this._element.querySelectorAll('option'));
248
+
249
+ // Use unified renderer
250
+ this._renderOptionsInDropdown(options, true);
251
+ }
252
+
253
+ /**
254
+ * Complete the setup for remote data after HTML structure is created
255
+ */
256
+ private _completeRemoteSetup() {
257
+ // Initialize options
258
+ this._preSelectOptions(this._element);
259
+
260
+ // Apply disabled state if needed
261
+ this._applyInitialDisabledState();
262
+
263
+ // Initialize search if enabled
264
+ if (this._config.enableSearch) {
265
+ this._initializeSearchModule();
266
+ }
267
+
268
+ // Initialize combobox if enabled
269
+ if (this._config.combobox) {
270
+ this._comboboxModule = new KTSelectCombobox(this);
271
+ }
272
+
273
+ // Initialize tags if enabled
274
+ if (this._config.tags) {
275
+ this._tagsModule = new KTSelectTags(this);
276
+ }
277
+
278
+ // Initialize focus manager after dropdown element is created
279
+ this._focusManager = new FocusManager(
280
+ this._dropdownContentElement,
281
+ '[data-kt-select-option]',
282
+ this._config,
283
+ );
284
+
285
+ // Initialize dropdown module after all elements are created
286
+ this._dropdownModule = new KTSelectDropdown(
287
+ this._wrapperElement,
288
+ this._displayElement,
289
+ this._dropdownContentElement,
290
+ this._config,
291
+ this, // Pass the KTSelect instance to KTSelectDropdown
292
+ );
293
+
294
+ // Update display and set ARIA attributes
295
+ this._updateDisplayAndAriaAttributes();
296
+ this.updateSelectedOptionDisplay();
297
+ this._setAriaAttributes();
298
+
299
+ // Update select all button state
300
+ this.updateSelectAllButtonState();
301
+
302
+ // Focus the first selected option or first option if nothing selected
303
+ this._focusSelectedOption();
304
+
305
+ // Attach event listeners after all modules are initialized
306
+ this._attachEventListeners();
307
+
308
+ this._observeNativeSelect();
309
+ }
310
+
159
311
  /**
160
312
  * Helper to show a dropdown message (error, loading, noResults)
161
313
  */
@@ -174,19 +326,22 @@ export class KTSelect extends KTComponent {
174
326
 
175
327
  switch (type) {
176
328
  case 'error':
177
- optionsContainer.appendChild(defaultTemplates.error({
178
- ...this._config,
179
- errorMessage: message,
180
- }));
329
+ optionsContainer.appendChild(
330
+ defaultTemplates.error({
331
+ ...this._config,
332
+ errorMessage: message,
333
+ }),
334
+ );
181
335
  break;
182
336
  case 'loading':
183
- optionsContainer.appendChild(defaultTemplates.loading(
184
- this._config,
185
- message || 'Loading...',
186
- ));
337
+ optionsContainer.appendChild(
338
+ defaultTemplates.loading(this._config, message || 'Loading...'),
339
+ );
187
340
  break;
188
341
  case 'empty':
189
- optionsContainer.appendChild(defaultTemplates.searchEmpty(this._config));
342
+ optionsContainer.appendChild(
343
+ defaultTemplates.searchEmpty(this._config),
344
+ );
190
345
  break;
191
346
  }
192
347
  }
@@ -436,18 +591,22 @@ export class KTSelect extends KTComponent {
436
591
  // Move classes from original select to wrapper and display elements
437
592
  if (this._element.classList.length > 0) {
438
593
  const originalClasses = Array.from(this._element.classList);
439
- const displaySpecificClasses = ['kt-select', 'kt-select-sm', 'kt-select-lg'];
594
+ const displaySpecificClasses = [
595
+ 'kt-select',
596
+ 'kt-select-sm',
597
+ 'kt-select-lg',
598
+ ];
440
599
 
441
600
  const classesForWrapper = originalClasses.filter(
442
- (className) => !displaySpecificClasses.includes(className)
601
+ (className) => !displaySpecificClasses.includes(className),
443
602
  );
444
603
  if (classesForWrapper.length > 0) {
445
604
  wrapperElement.classList.add(...classesForWrapper);
446
605
  }
447
606
 
448
607
  // Move display-specific classes to display element
449
- const classesForDisplay = originalClasses.filter(
450
- (className) => displaySpecificClasses.includes(className)
608
+ const classesForDisplay = originalClasses.filter((className) =>
609
+ displaySpecificClasses.includes(className),
451
610
  );
452
611
  if (classesForDisplay.length > 0) {
453
612
  displayElement.classList.add(...classesForDisplay);
@@ -477,7 +636,7 @@ export class KTSelect extends KTComponent {
477
636
  // Create options container using template
478
637
  const optionsContainer = defaultTemplates.options(this._config);
479
638
 
480
- // Add each option directly to the container
639
+ // Add each option directly to the container (only if options exist)
481
640
  options.forEach((optionElement) => {
482
641
  // Skip empty placeholder options (only if BOTH value AND text are empty)
483
642
  // This allows options with empty value but visible text to display in dropdown
@@ -513,6 +672,14 @@ export class KTSelect extends KTComponent {
513
672
  private _setupElementReferences() {
514
673
  this._wrapperElement = this._element.nextElementSibling as HTMLElement;
515
674
 
675
+ // Safety check - ensure wrapper element exists
676
+ if (!this._wrapperElement) {
677
+ console.error(
678
+ 'KTSelect: Wrapper element not found. HTML structure may not be created properly.',
679
+ );
680
+ return;
681
+ }
682
+
516
683
  // Get display element
517
684
  this._displayElement = this._wrapperElement.querySelector(
518
685
  `[data-kt-select-display]`,
@@ -524,8 +691,11 @@ export class KTSelect extends KTComponent {
524
691
  ) as HTMLElement;
525
692
 
526
693
  if (!this._dropdownContentElement) {
527
- console.log(this._element)
528
- console.error('Dropdown content element not found', this._wrapperElement);
694
+ console.error(
695
+ 'KTSelect: Dropdown content element not found',
696
+ this._wrapperElement,
697
+ );
698
+ return;
529
699
  }
530
700
 
531
701
  // Get search input element - this is used for the search functionality
@@ -542,6 +712,11 @@ export class KTSelect extends KTComponent {
542
712
  '[data-kt-select-select-all]',
543
713
  ) as HTMLElement;
544
714
 
715
+ // Cache the options container for performance
716
+ this._optionsContainer = this._dropdownContentElement.querySelector(
717
+ '[data-kt-select-options]',
718
+ ) as HTMLElement;
719
+
545
720
  this._options = this._wrapperElement.querySelectorAll(
546
721
  `[data-kt-select-option]`,
547
722
  ) as NodeListOf<HTMLElement>;
@@ -562,7 +737,8 @@ export class KTSelect extends KTComponent {
562
737
  );
563
738
 
564
739
  if (this._selectAllButton) {
565
- this._selectAllButtonToggle = this._selectAllButton.querySelector('button');
740
+ this._selectAllButtonToggle =
741
+ this._selectAllButton.querySelector('button');
566
742
  if (this._selectAllButtonToggle) {
567
743
  this._eventManager.addListener(
568
744
  this._selectAllButtonToggle,
@@ -575,7 +751,10 @@ export class KTSelect extends KTComponent {
575
751
  // Attach centralized keyboard handler to the wrapper element.
576
752
  // Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
577
753
  if (this._wrapperElement) {
578
- this._wrapperElement.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
754
+ this._wrapperElement.addEventListener(
755
+ 'keydown',
756
+ this._handleKeyboardEvent.bind(this),
757
+ );
579
758
  }
580
759
  }
581
760
 
@@ -666,9 +845,7 @@ export class KTSelect extends KTComponent {
666
845
 
667
846
  // Log the extracted values for debugging
668
847
  if (this._config.debug)
669
- console.log(
670
- `Option: value=${value}, label=${label}`,
671
- );
848
+ console.log(`Option: value=${value}, label=${label}`);
672
849
 
673
850
  // Set option attributes
674
851
  optionElement.value = value;
@@ -743,7 +920,8 @@ export class KTSelect extends KTComponent {
743
920
  */
744
921
  public openDropdown() {
745
922
  if (this._config.disabled) {
746
- if (this._config.debug) console.log('openDropdown: select is disabled, not opening');
923
+ if (this._config.debug)
924
+ console.log('openDropdown: select is disabled, not opening');
747
925
  return;
748
926
  }
749
927
  if (this._config.debug)
@@ -877,7 +1055,8 @@ export class KTSelect extends KTComponent {
877
1055
  private _selectOption(value: string) {
878
1056
  // Prevent selection if the option is disabled (in dropdown or original select)
879
1057
  if (this._isOptionDisabled(value)) {
880
- if (this._config.debug) console.log('_selectOption: Option is disabled, ignoring selection');
1058
+ if (this._config.debug)
1059
+ console.log('_selectOption: Option is disabled, ignoring selection');
881
1060
  return;
882
1061
  }
883
1062
 
@@ -945,7 +1124,9 @@ export class KTSelect extends KTComponent {
945
1124
  // Guard against valueDisplayEl being null due to template modifications
946
1125
  if (!valueDisplayEl) {
947
1126
  if (this._config.debug) {
948
- console.warn('KTSelect: Value display element is null. Cannot update display or placeholder. Check template for [data-kt-select-value].');
1127
+ console.warn(
1128
+ 'KTSelect: Value display element is null. Cannot update display or placeholder. Check template for [data-kt-select-value].',
1129
+ );
949
1130
  }
950
1131
  return; // Nothing to display on if the element is missing
951
1132
  }
@@ -968,7 +1149,9 @@ export class KTSelect extends KTComponent {
968
1149
  // Tags are not enabled AND options are selected: render normal text display.
969
1150
  let content = '';
970
1151
  if (this._config.displayTemplate) {
971
- content = this.renderDisplayTemplateForSelected(this.getSelectedOptions());
1152
+ content = this.renderDisplayTemplateForSelected(
1153
+ this.getSelectedOptions(),
1154
+ );
972
1155
  } else {
973
1156
  content = this.getSelectedOptionsText();
974
1157
  }
@@ -982,9 +1165,9 @@ export class KTSelect extends KTComponent {
982
1165
  * Check if an option was originally disabled in the HTML
983
1166
  */
984
1167
  private _isOptionOriginallyDisabled(value: string): boolean {
985
- const originalOption = Array.from(this._element.querySelectorAll('option')).find(
986
- (opt) => opt.value === value
987
- ) as HTMLOptionElement;
1168
+ const originalOption = Array.from(
1169
+ this._element.querySelectorAll('option'),
1170
+ ).find((opt) => opt.value === value) as HTMLOptionElement;
988
1171
  return originalOption ? originalOption.disabled : false;
989
1172
  }
990
1173
 
@@ -1011,7 +1194,8 @@ export class KTSelect extends KTComponent {
1011
1194
  if (!optionValue) return;
1012
1195
 
1013
1196
  const isSelected = selectedValues.includes(optionValue);
1014
- const isOriginallyDisabled = this._isOptionOriginallyDisabled(optionValue);
1197
+ const isOriginallyDisabled =
1198
+ this._isOptionOriginallyDisabled(optionValue);
1015
1199
 
1016
1200
  if (isSelected) {
1017
1201
  option.classList.add('selected');
@@ -1146,7 +1330,10 @@ export class KTSelect extends KTComponent {
1146
1330
 
1147
1331
  // If in single-select mode and the clicked option is already selected, just close the dropdown.
1148
1332
  if (!this._config.multiple && this._state.isSelected(optionValue)) {
1149
- if (this._config.debug) console.log('Single select mode: clicked already selected option. Closing dropdown.');
1333
+ if (this._config.debug)
1334
+ console.log(
1335
+ 'Single select mode: clicked already selected option. Closing dropdown.',
1336
+ );
1150
1337
  this.closeDropdown();
1151
1338
  return;
1152
1339
  }
@@ -1303,24 +1490,31 @@ export class KTSelect extends KTComponent {
1303
1490
  public toggleSelection(value: string): void {
1304
1491
  // Prevent selection if the option is disabled (in dropdown or original select)
1305
1492
  if (this._isOptionDisabled(value)) {
1306
- if (this._config.debug) console.log('toggleSelection: Option is disabled, ignoring selection');
1493
+ if (this._config.debug)
1494
+ console.log('toggleSelection: Option is disabled, ignoring selection');
1307
1495
  return;
1308
1496
  }
1309
1497
 
1310
1498
  // Get current selection state
1311
1499
  const isSelected = this._state.isSelected(value);
1312
1500
  if (this._config.debug)
1313
- console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}`);
1501
+ console.log(
1502
+ `toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}`,
1503
+ );
1314
1504
 
1315
1505
  // If already selected in single select mode, do nothing (can't deselect in single select)
1316
1506
  if (isSelected && !this._config.multiple) {
1317
1507
  if (this._config.debug)
1318
- console.log('Early return from toggleSelection - already selected in single select mode');
1508
+ console.log(
1509
+ 'Early return from toggleSelection - already selected in single select mode',
1510
+ );
1319
1511
  return;
1320
1512
  }
1321
1513
 
1322
1514
  if (this._config.debug)
1323
- console.log(`Toggling selection for option: ${value}, currently selected: ${isSelected}`);
1515
+ console.log(
1516
+ `Toggling selection for option: ${value}, currently selected: ${isSelected}`,
1517
+ );
1324
1518
 
1325
1519
  // Ensure any search input is cleared when selection changes
1326
1520
  if (this._searchModule) {
@@ -1357,11 +1551,15 @@ export class KTSelect extends KTComponent {
1357
1551
  // For multiple select mode, keep the dropdown open to allow multiple selections
1358
1552
  if (!this._config.multiple) {
1359
1553
  if (this._config.debug)
1360
- console.log('About to call closeDropdown() for single select mode - always close after selection');
1554
+ console.log(
1555
+ 'About to call closeDropdown() for single select mode - always close after selection',
1556
+ );
1361
1557
  this.closeDropdown();
1362
1558
  } else {
1363
1559
  if (this._config.debug)
1364
- console.log('Multiple select mode - keeping dropdown open for additional selections');
1560
+ console.log(
1561
+ 'Multiple select mode - keeping dropdown open for additional selections',
1562
+ );
1365
1563
  // Don't close dropdown in multiple select mode to allow multiple selections
1366
1564
  this.updateSelectAllButtonState();
1367
1565
  }
@@ -1467,6 +1665,8 @@ export class KTSelect extends KTComponent {
1467
1665
 
1468
1666
  // Check if the query is long enough
1469
1667
  if (query.length < (this._config.searchMinLength || 0)) {
1668
+ // Restore original options if query is too short
1669
+ this._restoreOriginalOptions();
1470
1670
  return;
1471
1671
  }
1472
1672
 
@@ -1507,7 +1707,7 @@ export class KTSelect extends KTComponent {
1507
1707
  console.error('Error fetching search results:', error);
1508
1708
  this._renderSearchErrorState(
1509
1709
  this._remoteModule.getErrorMessage() ||
1510
- 'Failed to load search results',
1710
+ 'Failed to load search results',
1511
1711
  );
1512
1712
  });
1513
1713
  }, this._config.searchDebounce || 300);
@@ -1540,6 +1740,42 @@ export class KTSelect extends KTComponent {
1540
1740
  */
1541
1741
  private _renderSearchErrorState(message: string) {
1542
1742
  this._showDropdownMessage('error', message);
1743
+
1744
+ // Restore original options after error with a delay
1745
+ setTimeout(() => {
1746
+ this._restoreOriginalOptions();
1747
+ }, 2000);
1748
+ }
1749
+
1750
+ /**
1751
+ * Restore original options when search is cleared
1752
+ */
1753
+ private _restoreOriginalOptions() {
1754
+ if (!this._dropdownContentElement || !this._originalOptionsHtml) return;
1755
+
1756
+ // Use cached options container for better performance
1757
+ const optionsContainer =
1758
+ this._optionsContainer ||
1759
+ this._dropdownContentElement.querySelector('[data-kt-select-options]');
1760
+ if (!optionsContainer) return;
1761
+
1762
+ // Restore original options
1763
+ optionsContainer.innerHTML = this._originalOptionsHtml;
1764
+
1765
+ // Update options NodeList
1766
+ this._options = this._wrapperElement.querySelectorAll(
1767
+ '[data-kt-select-option]',
1768
+ ) as NodeListOf<HTMLElement>;
1769
+
1770
+ // Refresh search module
1771
+ if (this._searchModule) {
1772
+ this._searchModule.refreshAfterSearch();
1773
+ }
1774
+ this.updateSelectAllButtonState();
1775
+
1776
+ if (this._config.debug) {
1777
+ console.log('Restored original options after search clear');
1778
+ }
1543
1779
  }
1544
1780
 
1545
1781
  /**
@@ -1549,40 +1785,27 @@ export class KTSelect extends KTComponent {
1549
1785
  private _updateSearchResults(items: KTSelectOptionData[]) {
1550
1786
  if (!this._dropdownContentElement) return;
1551
1787
 
1552
- const optionsContainer = this._dropdownContentElement.querySelector(
1553
- '[data-kt-select-options]',
1554
- );
1788
+ // Use cached options container for better performance
1789
+ const optionsContainer =
1790
+ this._optionsContainer ||
1791
+ this._dropdownContentElement.querySelector('[data-kt-select-options]');
1555
1792
  if (!optionsContainer) return;
1556
1793
 
1557
- // Clear current options
1558
- optionsContainer.innerHTML = '';
1559
-
1794
+ // Handle empty results
1560
1795
  if (items.length === 0) {
1561
- // Show no results message using template for consistency and customization
1796
+ optionsContainer.innerHTML = '';
1562
1797
  const noResultsElement = defaultTemplates.searchEmpty(this._config);
1563
1798
  optionsContainer.appendChild(noResultsElement);
1564
1799
  return;
1565
1800
  }
1566
1801
 
1567
- // Process each item individually to create options
1568
- items.forEach((item) => {
1569
- // Create option for the original select
1570
- const selectOption = document.createElement('option');
1571
- selectOption.value = item.id;
1572
-
1573
- // Add to dropdown container
1574
- optionsContainer.appendChild(selectOption);
1575
- });
1802
+ // Use unified renderer for search results
1803
+ this._renderOptionsInDropdown(items, true);
1576
1804
 
1577
1805
  // Add pagination "Load More" button if needed
1578
1806
  if (this._config.pagination && this._remoteModule.hasMorePages()) {
1579
1807
  this._addLoadMoreButton();
1580
1808
  }
1581
-
1582
- // Update options NodeList
1583
- this._options = this._wrapperElement.querySelectorAll(
1584
- `[data-kt-select-option]`,
1585
- ) as NodeListOf<HTMLElement>;
1586
1809
  }
1587
1810
 
1588
1811
  /**
@@ -1595,10 +1818,14 @@ export class KTSelect extends KTComponent {
1595
1818
  public getSelectedOptionsText(): string {
1596
1819
  const selectedValues = this.getSelectedOptions();
1597
1820
  const displaySeparator = this._config.displaySeparator || ', ';
1598
- const texts = selectedValues.map(value => {
1599
- const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1600
- return option?.getAttribute('data-text') || '';
1601
- }).filter(Boolean);
1821
+ const texts = selectedValues
1822
+ .map((value) => {
1823
+ const option = Array.from(this._options).find(
1824
+ (opt) => opt.getAttribute('data-value') === value,
1825
+ );
1826
+ return option?.getAttribute('data-text') || '';
1827
+ })
1828
+ .filter(Boolean);
1602
1829
  return texts.join(displaySeparator);
1603
1830
  }
1604
1831
 
@@ -1607,12 +1834,15 @@ export class KTSelect extends KTComponent {
1607
1834
  */
1608
1835
  private _isOptionDisabled(value: string): boolean {
1609
1836
  const dropdownOption = Array.from(this._options).find(
1610
- (opt) => opt.getAttribute('data-value') === value
1837
+ (opt) => opt.getAttribute('data-value') === value,
1611
1838
  );
1612
- const isDropdownDisabled = dropdownOption && (dropdownOption.classList.contains('disabled') || dropdownOption.getAttribute('aria-disabled') === 'true');
1613
- const selectOption = Array.from(this._element.querySelectorAll('option')).find(
1614
- (opt) => opt.value === value
1615
- ) as HTMLOptionElement;
1839
+ const isDropdownDisabled =
1840
+ dropdownOption &&
1841
+ (dropdownOption.classList.contains('disabled') ||
1842
+ dropdownOption.getAttribute('aria-disabled') === 'true');
1843
+ const selectOption = Array.from(
1844
+ this._element.querySelectorAll('option'),
1845
+ ).find((opt) => opt.value === value) as HTMLOptionElement;
1616
1846
  const isNativeDisabled = selectOption && selectOption.disabled;
1617
1847
  return Boolean(isDropdownDisabled || isNativeDisabled);
1618
1848
  }
@@ -1636,9 +1866,16 @@ export class KTSelect extends KTComponent {
1636
1866
  if (event.target === this._searchInputElement) {
1637
1867
  // Allow navigation keys like ArrowDown, ArrowUp, Escape, Enter (for search/selection) to be handled by the logic below.
1638
1868
  // For other keys (characters, space, backspace, delete), let the input field process them.
1639
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' &&
1640
- event.key !== 'Escape' && event.key !== 'Enter' && event.key !== 'Tab' &&
1641
- event.key !== 'Home' && event.key !== 'End') {
1869
+ if (
1870
+ event.key !== 'ArrowDown' &&
1871
+ event.key !== 'ArrowUp' &&
1872
+ event.key !== 'Escape' &&
1873
+ event.key !== 'Enter' &&
1874
+ event.key !== 'Tab' &&
1875
+ event.key !== 'Home' &&
1876
+ event.key !== 'End' &&
1877
+ event.key !== ' '
1878
+ ) {
1642
1879
  // If it's a character key and we are NOT type-to-searching (because search has focus)
1643
1880
  // then let the input field handle it for its own value.
1644
1881
  // The search module's 'input' event will handle filtering based on the input's value.
@@ -1653,11 +1890,16 @@ export class KTSelect extends KTComponent {
1653
1890
  if (event.altKey || event.ctrlKey || event.metaKey) return;
1654
1891
 
1655
1892
  // Type-to-search: only for single char keys, when search input does not have focus
1656
- if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/) && document.activeElement !== this._searchInputElement) {
1893
+ if (
1894
+ event.key.length === 1 &&
1895
+ !event.repeat &&
1896
+ !event.key.match(/\s/) &&
1897
+ document.activeElement !== this._searchInputElement
1898
+ ) {
1657
1899
  buffer.push(event.key);
1658
1900
  const str = buffer.getBuffer();
1659
1901
  if (isOpen) {
1660
- focusManager.focusByString(str);
1902
+ focusManager.focusByString(str);
1661
1903
  } else {
1662
1904
  // If closed, type-to-search could potentially open and select.
1663
1905
  // For now, let's assume it only works when open or opens it first.
@@ -1698,8 +1940,15 @@ export class KTSelect extends KTComponent {
1698
1940
  if (focusedOptionEl) {
1699
1941
  const val = focusedOptionEl.dataset.value;
1700
1942
  // If single select, and the item is already selected, just close.
1701
- if (val !== undefined && !this._config.multiple && this._state.isSelected(val)) {
1702
- if (this._config.debug) console.log('Enter on already selected item in single-select mode. Closing.');
1943
+ if (
1944
+ val !== undefined &&
1945
+ !this._config.multiple &&
1946
+ this._state.isSelected(val)
1947
+ ) {
1948
+ if (this._config.debug)
1949
+ console.log(
1950
+ 'Enter on already selected item in single-select mode. Closing.',
1951
+ );
1703
1952
  this.closeDropdown();
1704
1953
  event.preventDefault();
1705
1954
  break;
@@ -1737,30 +1986,39 @@ export class KTSelect extends KTComponent {
1737
1986
  }
1738
1987
 
1739
1988
  public renderDisplayTemplateForSelected(selectedValues: string[]): string {
1740
- const optionsConfig = this._config.optionsConfig as any || {};
1989
+ const optionsConfig = (this._config.optionsConfig as any) || {};
1741
1990
  const displaySeparator = this._config.displaySeparator || ', ';
1742
- const contentArray = Array.from(new Set(
1743
- selectedValues.map(value => {
1744
- const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1745
- if (!option) return '';
1746
-
1747
- let displayTemplate = this._config.displayTemplate;
1748
- const text = option.getAttribute('data-text') || '';
1749
-
1750
- // Replace all {{varname}} in option.innerHTML with values from _config
1751
- Object.entries(optionsConfig[value] || {}).forEach(([key, val]) => {
1752
- if (["string", "number", "boolean"].includes(typeof val)) {
1753
- displayTemplate = displayTemplate.replace(new RegExp(`{{${key}}}`, 'g'), String(val));
1754
- }
1755
- });
1991
+ const contentArray = Array.from(
1992
+ new Set(
1993
+ selectedValues
1994
+ .map((value) => {
1995
+ const option = Array.from(this._options).find(
1996
+ (opt) => opt.getAttribute('data-value') === value,
1997
+ );
1998
+ if (!option) return '';
1999
+
2000
+ let displayTemplate = this._config.displayTemplate;
2001
+ const text = option.getAttribute('data-text') || '';
2002
+
2003
+ // Replace all {{varname}} in option.innerHTML with values from _config
2004
+ Object.entries(optionsConfig[value] || {}).forEach(([key, val]) => {
2005
+ if (['string', 'number', 'boolean'].includes(typeof val)) {
2006
+ displayTemplate = displayTemplate.replace(
2007
+ new RegExp(`{{${key}}}`, 'g'),
2008
+ String(val),
2009
+ );
2010
+ }
2011
+ });
1756
2012
 
1757
- return renderTemplateString(displayTemplate, {
1758
- selectedCount: selectedValues.length || 0,
1759
- selectedTexts: this.getSelectedOptionsText() || '',
1760
- text,
1761
- });
1762
- }).filter(Boolean)
1763
- ));
2013
+ return renderTemplateString(displayTemplate, {
2014
+ selectedCount: selectedValues.length || 0,
2015
+ selectedTexts: this.getSelectedOptionsText() || '',
2016
+ text,
2017
+ });
2018
+ })
2019
+ .filter(Boolean),
2020
+ ),
2021
+ );
1764
2022
  return contentArray.join(displaySeparator);
1765
2023
  }
1766
2024
 
@@ -1778,7 +2036,10 @@ export class KTSelect extends KTComponent {
1778
2036
  if (mutation.type === 'childList') {
1779
2037
  // Option(s) added or removed
1780
2038
  needsRebuild = true;
1781
- } else if (mutation.type === 'attributes' && mutation.target instanceof HTMLOptionElement) {
2039
+ } else if (
2040
+ mutation.type === 'attributes' &&
2041
+ mutation.target instanceof HTMLOptionElement
2042
+ ) {
1782
2043
  if (mutation.attributeName === 'selected') {
1783
2044
  needsSelectionSync = true;
1784
2045
  }
@@ -1805,7 +2066,9 @@ export class KTSelect extends KTComponent {
1805
2066
  private _rebuildOptionsFromNative() {
1806
2067
  // Remove and rebuild the custom dropdown options from the native select
1807
2068
  if (this._dropdownContentElement) {
1808
- const optionsContainer = this._dropdownContentElement.querySelector('[data-kt-select-options]');
2069
+ const optionsContainer = this._dropdownContentElement.querySelector(
2070
+ '[data-kt-select-options]',
2071
+ );
1809
2072
  if (optionsContainer) {
1810
2073
  optionsContainer.innerHTML = '';
1811
2074
  const options = Array.from(this._element.querySelectorAll('option'));
@@ -1821,7 +2084,9 @@ export class KTSelect extends KTComponent {
1821
2084
  optionsContainer.appendChild(renderedOption);
1822
2085
  });
1823
2086
  // Update internal references
1824
- this._options = this._wrapperElement.querySelectorAll('[data-kt-select-option]') as NodeListOf<HTMLElement>;
2087
+ this._options = this._wrapperElement.querySelectorAll(
2088
+ '[data-kt-select-option]',
2089
+ ) as NodeListOf<HTMLElement>;
1825
2090
  }
1826
2091
  }
1827
2092
  // Sync selection after rebuilding
@@ -1832,8 +2097,12 @@ export class KTSelect extends KTComponent {
1832
2097
 
1833
2098
  private _syncSelectionFromNative() {
1834
2099
  // Sync internal state from the native select's selected options
1835
- const selected = Array.from(this._element.querySelectorAll('option:checked')).map(opt => (opt as HTMLOptionElement).value);
1836
- this._state.setSelectedOptions(this._config.multiple ? selected : selected[0] || '');
2100
+ const selected = Array.from(
2101
+ this._element.querySelectorAll('option:checked'),
2102
+ ).map((opt) => (opt as HTMLOptionElement).value);
2103
+ this._state.setSelectedOptions(
2104
+ this._config.multiple ? selected : selected[0] || '',
2105
+ );
1837
2106
  this.updateSelectedOptionDisplay();
1838
2107
  this._updateSelectedOptionClass();
1839
2108
  this.updateSelectAllButtonState();