@keenthemes/ktui 1.0.19 → 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 (111) hide show
  1. package/dist/ktui.js +690 -166
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +165 -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 +59 -5
  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 +79 -12
  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/config.js +1 -0
  26. package/lib/cjs/components/select/config.js.map +1 -1
  27. package/lib/cjs/components/select/dropdown.js +4 -2
  28. package/lib/cjs/components/select/dropdown.js.map +1 -1
  29. package/lib/cjs/components/select/index.js.map +1 -1
  30. package/lib/cjs/components/select/option.js +2 -1
  31. package/lib/cjs/components/select/option.js.map +1 -1
  32. package/lib/cjs/components/select/remote.js +50 -50
  33. package/lib/cjs/components/select/remote.js.map +1 -1
  34. package/lib/cjs/components/select/search.js +15 -5
  35. package/lib/cjs/components/select/search.js.map +1 -1
  36. package/lib/cjs/components/select/select.js +273 -32
  37. package/lib/cjs/components/select/select.js.map +1 -1
  38. package/lib/cjs/components/select/tags.js +3 -1
  39. package/lib/cjs/components/select/tags.js.map +1 -1
  40. package/lib/cjs/components/select/templates.js +6 -0
  41. package/lib/cjs/components/select/templates.js.map +1 -1
  42. package/lib/cjs/components/select/utils.js +23 -10
  43. package/lib/cjs/components/select/utils.js.map +1 -1
  44. package/lib/cjs/components/stepper/stepper.js +59 -12
  45. package/lib/cjs/components/stepper/stepper.js.map +1 -1
  46. package/lib/cjs/components/sticky/sticky.js +52 -14
  47. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  48. package/lib/esm/components/component.js +59 -5
  49. package/lib/esm/components/component.js.map +1 -1
  50. package/lib/esm/components/datatable/datatable-sort.js +4 -0
  51. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  52. package/lib/esm/components/datatable/datatable.js +78 -12
  53. package/lib/esm/components/datatable/datatable.js.map +1 -1
  54. package/lib/esm/components/image-input/image-input.js +10 -2
  55. package/lib/esm/components/image-input/image-input.js.map +1 -1
  56. package/lib/esm/components/select/combobox.js +50 -20
  57. package/lib/esm/components/select/combobox.js.map +1 -1
  58. package/lib/esm/components/select/config.js +1 -0
  59. package/lib/esm/components/select/config.js.map +1 -1
  60. package/lib/esm/components/select/dropdown.js +4 -2
  61. package/lib/esm/components/select/dropdown.js.map +1 -1
  62. package/lib/esm/components/select/index.js +1 -1
  63. package/lib/esm/components/select/index.js.map +1 -1
  64. package/lib/esm/components/select/option.js +2 -1
  65. package/lib/esm/components/select/option.js.map +1 -1
  66. package/lib/esm/components/select/remote.js +50 -50
  67. package/lib/esm/components/select/remote.js.map +1 -1
  68. package/lib/esm/components/select/search.js +16 -6
  69. package/lib/esm/components/select/search.js.map +1 -1
  70. package/lib/esm/components/select/select.js +273 -32
  71. package/lib/esm/components/select/select.js.map +1 -1
  72. package/lib/esm/components/select/tags.js +3 -1
  73. package/lib/esm/components/select/tags.js.map +1 -1
  74. package/lib/esm/components/select/templates.js +6 -0
  75. package/lib/esm/components/select/templates.js.map +1 -1
  76. package/lib/esm/components/select/utils.js +23 -10
  77. package/lib/esm/components/select/utils.js.map +1 -1
  78. package/lib/esm/components/stepper/stepper.js +59 -12
  79. package/lib/esm/components/stepper/stepper.js.map +1 -1
  80. package/lib/esm/components/sticky/sticky.js +52 -14
  81. package/lib/esm/components/sticky/sticky.js.map +1 -1
  82. package/package.json +2 -2
  83. package/src/components/component.ts +19 -4
  84. package/src/components/datatable/datatable-sort.ts +6 -0
  85. package/src/components/datatable/datatable.ts +98 -15
  86. package/src/components/datatable/types.ts +5 -1
  87. package/src/components/image-input/image-input.ts +11 -2
  88. package/src/components/image-input/types.ts +1 -0
  89. package/src/components/input/input-group.css +1 -1
  90. package/src/components/input/input.css +1 -1
  91. package/src/components/scrollable/scrollable.css +3 -3
  92. package/src/components/select/combobox.ts +84 -34
  93. package/src/components/select/config.ts +2 -0
  94. package/src/components/select/dropdown.ts +20 -11
  95. package/src/components/select/index.ts +6 -1
  96. package/src/components/select/option.ts +7 -6
  97. package/src/components/select/remote.ts +51 -52
  98. package/src/components/select/search.ts +59 -44
  99. package/src/components/select/select.css +26 -17
  100. package/src/components/select/select.ts +472 -101
  101. package/src/components/select/tags.ts +9 -3
  102. package/src/components/select/templates.ts +10 -0
  103. package/src/components/select/utils.ts +55 -20
  104. package/src/components/select/variants.css +0 -1
  105. package/src/components/stepper/stepper.ts +2 -2
  106. package/src/components/sticky/sticky.ts +47 -16
  107. package/src/components/sticky/types.ts +3 -0
  108. package/src/components/table/table.css +1 -1
  109. package/src/components/textarea/textarea.css +1 -1
  110. package/src/components/toast/toast.css +84 -47
  111. 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;
@@ -48,6 +51,8 @@ export class KTSelect extends KTComponent {
48
51
  private _tagsModule: KTSelectTags | null = null;
49
52
  private _dropdownModule: KTSelectDropdown | null = null;
50
53
  private _loadMoreIndicator: HTMLElement | null = null;
54
+ private _selectAllButton: HTMLElement | null = null;
55
+ private _selectAllButtonToggle: HTMLButtonElement | null = null;
51
56
  private _focusManager: FocusManager;
52
57
  private _eventManager: EventManager;
53
58
  private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
@@ -102,6 +107,11 @@ export class KTSelect extends KTComponent {
102
107
  if (this._config.debug)
103
108
  console.log('Initializing remote data with URL:', this._config.dataUrl);
104
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
+
105
115
  // Show loading state
106
116
  this._renderLoadingState();
107
117
 
@@ -123,7 +133,12 @@ export class KTSelect extends KTComponent {
123
133
 
124
134
  if (this._config.debug)
125
135
  console.log('Generating options HTML from remote data');
126
- 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();
127
142
 
128
143
  // Add pagination "Load More" button if needed
129
144
  if (this._config.pagination && this._remoteModule.hasMorePages()) {
@@ -154,6 +169,145 @@ export class KTSelect extends KTComponent {
154
169
  options.forEach((option) => option.remove());
155
170
  }
156
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
+
157
311
  /**
158
312
  * Helper to show a dropdown message (error, loading, noResults)
159
313
  */
@@ -172,19 +326,22 @@ export class KTSelect extends KTComponent {
172
326
 
173
327
  switch (type) {
174
328
  case 'error':
175
- optionsContainer.appendChild(defaultTemplates.error({
176
- ...this._config,
177
- errorMessage: message,
178
- }));
329
+ optionsContainer.appendChild(
330
+ defaultTemplates.error({
331
+ ...this._config,
332
+ errorMessage: message,
333
+ }),
334
+ );
179
335
  break;
180
336
  case 'loading':
181
- optionsContainer.appendChild(defaultTemplates.loading(
182
- this._config,
183
- message || 'Loading...',
184
- ));
337
+ optionsContainer.appendChild(
338
+ defaultTemplates.loading(this._config, message || 'Loading...'),
339
+ );
185
340
  break;
186
341
  case 'empty':
187
- optionsContainer.appendChild(defaultTemplates.searchEmpty(this._config));
342
+ optionsContainer.appendChild(
343
+ defaultTemplates.searchEmpty(this._config),
344
+ );
188
345
  break;
189
346
  }
190
347
  }
@@ -405,6 +562,12 @@ export class KTSelect extends KTComponent {
405
562
  this.updateSelectedOptionDisplay();
406
563
  this._setAriaAttributes();
407
564
 
565
+ // Update select all button state
566
+ this.updateSelectAllButtonState();
567
+
568
+ // Focus the first selected option or first option if nothing selected
569
+ this._focusSelectedOption();
570
+
408
571
  // Attach event listeners after all modules are initialized
409
572
  this._attachEventListeners();
410
573
 
@@ -428,18 +591,22 @@ export class KTSelect extends KTComponent {
428
591
  // Move classes from original select to wrapper and display elements
429
592
  if (this._element.classList.length > 0) {
430
593
  const originalClasses = Array.from(this._element.classList);
431
- 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
+ ];
432
599
 
433
600
  const classesForWrapper = originalClasses.filter(
434
- (className) => !displaySpecificClasses.includes(className)
601
+ (className) => !displaySpecificClasses.includes(className),
435
602
  );
436
603
  if (classesForWrapper.length > 0) {
437
604
  wrapperElement.classList.add(...classesForWrapper);
438
605
  }
439
606
 
440
607
  // Move display-specific classes to display element
441
- const classesForDisplay = originalClasses.filter(
442
- (className) => displaySpecificClasses.includes(className)
608
+ const classesForDisplay = originalClasses.filter((className) =>
609
+ displaySpecificClasses.includes(className),
443
610
  );
444
611
  if (classesForDisplay.length > 0) {
445
612
  displayElement.classList.add(...classesForDisplay);
@@ -460,10 +627,16 @@ export class KTSelect extends KTComponent {
460
627
  dropdownElement.appendChild(searchElement);
461
628
  }
462
629
 
630
+ // Add select all button if needed
631
+ if (this._config.multiple && this._config.enableSelectAll) {
632
+ const selectAllElement = defaultTemplates.selectAll(this._config);
633
+ dropdownElement.appendChild(selectAllElement);
634
+ }
635
+
463
636
  // Create options container using template
464
637
  const optionsContainer = defaultTemplates.options(this._config);
465
638
 
466
- // Add each option directly to the container
639
+ // Add each option directly to the container (only if options exist)
467
640
  options.forEach((optionElement) => {
468
641
  // Skip empty placeholder options (only if BOTH value AND text are empty)
469
642
  // This allows options with empty value but visible text to display in dropdown
@@ -499,6 +672,14 @@ export class KTSelect extends KTComponent {
499
672
  private _setupElementReferences() {
500
673
  this._wrapperElement = this._element.nextElementSibling as HTMLElement;
501
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
+
502
683
  // Get display element
503
684
  this._displayElement = this._wrapperElement.querySelector(
504
685
  `[data-kt-select-display]`,
@@ -510,8 +691,11 @@ export class KTSelect extends KTComponent {
510
691
  ) as HTMLElement;
511
692
 
512
693
  if (!this._dropdownContentElement) {
513
- console.log(this._element)
514
- 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;
515
699
  }
516
700
 
517
701
  // Get search input element - this is used for the search functionality
@@ -524,6 +708,15 @@ export class KTSelect extends KTComponent {
524
708
  this._searchInputElement = this._displayElement as HTMLInputElement;
525
709
  }
526
710
 
711
+ this._selectAllButton = this._wrapperElement.querySelector(
712
+ '[data-kt-select-select-all]',
713
+ ) as HTMLElement;
714
+
715
+ // Cache the options container for performance
716
+ this._optionsContainer = this._dropdownContentElement.querySelector(
717
+ '[data-kt-select-options]',
718
+ ) as HTMLElement;
719
+
527
720
  this._options = this._wrapperElement.querySelectorAll(
528
721
  `[data-kt-select-option]`,
529
722
  ) as NodeListOf<HTMLElement>;
@@ -543,10 +736,25 @@ export class KTSelect extends KTComponent {
543
736
  this._handleDropdownOptionClick.bind(this),
544
737
  );
545
738
 
739
+ if (this._selectAllButton) {
740
+ this._selectAllButtonToggle =
741
+ this._selectAllButton.querySelector('button');
742
+ if (this._selectAllButtonToggle) {
743
+ this._eventManager.addListener(
744
+ this._selectAllButtonToggle,
745
+ 'click',
746
+ this._handleSelectAllClick.bind(this),
747
+ );
748
+ }
749
+ }
750
+
546
751
  // Attach centralized keyboard handler to the wrapper element.
547
752
  // Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
548
753
  if (this._wrapperElement) {
549
- this._wrapperElement.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
754
+ this._wrapperElement.addEventListener(
755
+ 'keydown',
756
+ this._handleKeyboardEvent.bind(this),
757
+ );
550
758
  }
551
759
  }
552
760
 
@@ -637,9 +845,7 @@ export class KTSelect extends KTComponent {
637
845
 
638
846
  // Log the extracted values for debugging
639
847
  if (this._config.debug)
640
- console.log(
641
- `Option: value=${value}, label=${label}`,
642
- );
848
+ console.log(`Option: value=${value}, label=${label}`);
643
849
 
644
850
  // Set option attributes
645
851
  optionElement.value = value;
@@ -714,7 +920,8 @@ export class KTSelect extends KTComponent {
714
920
  */
715
921
  public openDropdown() {
716
922
  if (this._config.disabled) {
717
- 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');
718
925
  return;
719
926
  }
720
927
  if (this._config.debug)
@@ -752,6 +959,9 @@ export class KTSelect extends KTComponent {
752
959
  // Update ARIA states
753
960
  this._setAriaAttributes();
754
961
 
962
+ // Update select all button state
963
+ this.updateSelectAllButtonState();
964
+
755
965
  // Focus the first selected option or first option if nothing selected
756
966
  this._focusSelectedOption();
757
967
  }
@@ -845,7 +1055,8 @@ export class KTSelect extends KTComponent {
845
1055
  private _selectOption(value: string) {
846
1056
  // Prevent selection if the option is disabled (in dropdown or original select)
847
1057
  if (this._isOptionDisabled(value)) {
848
- 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');
849
1060
  return;
850
1061
  }
851
1062
 
@@ -913,7 +1124,9 @@ export class KTSelect extends KTComponent {
913
1124
  // Guard against valueDisplayEl being null due to template modifications
914
1125
  if (!valueDisplayEl) {
915
1126
  if (this._config.debug) {
916
- 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
+ );
917
1130
  }
918
1131
  return; // Nothing to display on if the element is missing
919
1132
  }
@@ -936,7 +1149,9 @@ export class KTSelect extends KTComponent {
936
1149
  // Tags are not enabled AND options are selected: render normal text display.
937
1150
  let content = '';
938
1151
  if (this._config.displayTemplate) {
939
- content = this.renderDisplayTemplateForSelected(this.getSelectedOptions());
1152
+ content = this.renderDisplayTemplateForSelected(
1153
+ this.getSelectedOptions(),
1154
+ );
940
1155
  } else {
941
1156
  content = this.getSelectedOptionsText();
942
1157
  }
@@ -950,9 +1165,9 @@ export class KTSelect extends KTComponent {
950
1165
  * Check if an option was originally disabled in the HTML
951
1166
  */
952
1167
  private _isOptionOriginallyDisabled(value: string): boolean {
953
- const originalOption = Array.from(this._element.querySelectorAll('option')).find(
954
- (opt) => opt.value === value
955
- ) as HTMLOptionElement;
1168
+ const originalOption = Array.from(
1169
+ this._element.querySelectorAll('option'),
1170
+ ).find((opt) => opt.value === value) as HTMLOptionElement;
956
1171
  return originalOption ? originalOption.disabled : false;
957
1172
  }
958
1173
 
@@ -979,7 +1194,8 @@ export class KTSelect extends KTComponent {
979
1194
  if (!optionValue) return;
980
1195
 
981
1196
  const isSelected = selectedValues.includes(optionValue);
982
- const isOriginallyDisabled = this._isOptionOriginallyDisabled(optionValue);
1197
+ const isOriginallyDisabled =
1198
+ this._isOptionOriginallyDisabled(optionValue);
983
1199
 
984
1200
  if (isSelected) {
985
1201
  option.classList.add('selected');
@@ -1013,6 +1229,9 @@ export class KTSelect extends KTComponent {
1013
1229
  this.updateSelectedOptionDisplay();
1014
1230
  this._updateSelectedOptionClass();
1015
1231
 
1232
+ // Update select all button state
1233
+ this.updateSelectAllButtonState();
1234
+
1016
1235
  // Dispatch change event
1017
1236
  this._dispatchEvent('change');
1018
1237
  this._fireEvent('change');
@@ -1111,7 +1330,10 @@ export class KTSelect extends KTComponent {
1111
1330
 
1112
1331
  // If in single-select mode and the clicked option is already selected, just close the dropdown.
1113
1332
  if (!this._config.multiple && this._state.isSelected(optionValue)) {
1114
- 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
+ );
1115
1337
  this.closeDropdown();
1116
1338
  return;
1117
1339
  }
@@ -1268,24 +1490,31 @@ export class KTSelect extends KTComponent {
1268
1490
  public toggleSelection(value: string): void {
1269
1491
  // Prevent selection if the option is disabled (in dropdown or original select)
1270
1492
  if (this._isOptionDisabled(value)) {
1271
- 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');
1272
1495
  return;
1273
1496
  }
1274
1497
 
1275
1498
  // Get current selection state
1276
1499
  const isSelected = this._state.isSelected(value);
1277
1500
  if (this._config.debug)
1278
- 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
+ );
1279
1504
 
1280
1505
  // If already selected in single select mode, do nothing (can't deselect in single select)
1281
1506
  if (isSelected && !this._config.multiple) {
1282
1507
  if (this._config.debug)
1283
- 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
+ );
1284
1511
  return;
1285
1512
  }
1286
1513
 
1287
1514
  if (this._config.debug)
1288
- console.log(`Toggling selection for option: ${value}, currently selected: ${isSelected}`);
1515
+ console.log(
1516
+ `Toggling selection for option: ${value}, currently selected: ${isSelected}`,
1517
+ );
1289
1518
 
1290
1519
  // Ensure any search input is cleared when selection changes
1291
1520
  if (this._searchModule) {
@@ -1322,12 +1551,17 @@ export class KTSelect extends KTComponent {
1322
1551
  // For multiple select mode, keep the dropdown open to allow multiple selections
1323
1552
  if (!this._config.multiple) {
1324
1553
  if (this._config.debug)
1325
- 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
+ );
1326
1557
  this.closeDropdown();
1327
1558
  } else {
1328
1559
  if (this._config.debug)
1329
- 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
+ );
1330
1563
  // Don't close dropdown in multiple select mode to allow multiple selections
1564
+ this.updateSelectAllButtonState();
1331
1565
  }
1332
1566
 
1333
1567
  // Dispatch custom change event with additional data
@@ -1431,6 +1665,8 @@ export class KTSelect extends KTComponent {
1431
1665
 
1432
1666
  // Check if the query is long enough
1433
1667
  if (query.length < (this._config.searchMinLength || 0)) {
1668
+ // Restore original options if query is too short
1669
+ this._restoreOriginalOptions();
1434
1670
  return;
1435
1671
  }
1436
1672
 
@@ -1458,6 +1694,7 @@ export class KTSelect extends KTComponent {
1458
1694
  if (this._searchModule) {
1459
1695
  this._searchModule.refreshAfterSearch();
1460
1696
  }
1697
+ this.updateSelectAllButtonState();
1461
1698
  })
1462
1699
  .catch((error) => {
1463
1700
  console.error('Error updating search results:', error);
@@ -1470,7 +1707,7 @@ export class KTSelect extends KTComponent {
1470
1707
  console.error('Error fetching search results:', error);
1471
1708
  this._renderSearchErrorState(
1472
1709
  this._remoteModule.getErrorMessage() ||
1473
- 'Failed to load search results',
1710
+ 'Failed to load search results',
1474
1711
  );
1475
1712
  });
1476
1713
  }, this._config.searchDebounce || 300);
@@ -1503,6 +1740,42 @@ export class KTSelect extends KTComponent {
1503
1740
  */
1504
1741
  private _renderSearchErrorState(message: string) {
1505
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
+ }
1506
1779
  }
1507
1780
 
1508
1781
  /**
@@ -1512,40 +1785,27 @@ export class KTSelect extends KTComponent {
1512
1785
  private _updateSearchResults(items: KTSelectOptionData[]) {
1513
1786
  if (!this._dropdownContentElement) return;
1514
1787
 
1515
- const optionsContainer = this._dropdownContentElement.querySelector(
1516
- '[data-kt-select-options]',
1517
- );
1788
+ // Use cached options container for better performance
1789
+ const optionsContainer =
1790
+ this._optionsContainer ||
1791
+ this._dropdownContentElement.querySelector('[data-kt-select-options]');
1518
1792
  if (!optionsContainer) return;
1519
1793
 
1520
- // Clear current options
1521
- optionsContainer.innerHTML = '';
1522
-
1794
+ // Handle empty results
1523
1795
  if (items.length === 0) {
1524
- // Show no results message using template for consistency and customization
1796
+ optionsContainer.innerHTML = '';
1525
1797
  const noResultsElement = defaultTemplates.searchEmpty(this._config);
1526
1798
  optionsContainer.appendChild(noResultsElement);
1527
1799
  return;
1528
1800
  }
1529
1801
 
1530
- // Process each item individually to create options
1531
- items.forEach((item) => {
1532
- // Create option for the original select
1533
- const selectOption = document.createElement('option');
1534
- selectOption.value = item.id;
1535
-
1536
- // Add to dropdown container
1537
- optionsContainer.appendChild(selectOption);
1538
- });
1802
+ // Use unified renderer for search results
1803
+ this._renderOptionsInDropdown(items, true);
1539
1804
 
1540
1805
  // Add pagination "Load More" button if needed
1541
1806
  if (this._config.pagination && this._remoteModule.hasMorePages()) {
1542
1807
  this._addLoadMoreButton();
1543
1808
  }
1544
-
1545
- // Update options NodeList
1546
- this._options = this._wrapperElement.querySelectorAll(
1547
- `[data-kt-select-option]`,
1548
- ) as NodeListOf<HTMLElement>;
1549
1809
  }
1550
1810
 
1551
1811
  /**
@@ -1558,10 +1818,14 @@ export class KTSelect extends KTComponent {
1558
1818
  public getSelectedOptionsText(): string {
1559
1819
  const selectedValues = this.getSelectedOptions();
1560
1820
  const displaySeparator = this._config.displaySeparator || ', ';
1561
- const texts = selectedValues.map(value => {
1562
- const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1563
- return option?.getAttribute('data-text') || '';
1564
- }).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);
1565
1829
  return texts.join(displaySeparator);
1566
1830
  }
1567
1831
 
@@ -1570,12 +1834,15 @@ export class KTSelect extends KTComponent {
1570
1834
  */
1571
1835
  private _isOptionDisabled(value: string): boolean {
1572
1836
  const dropdownOption = Array.from(this._options).find(
1573
- (opt) => opt.getAttribute('data-value') === value
1837
+ (opt) => opt.getAttribute('data-value') === value,
1574
1838
  );
1575
- const isDropdownDisabled = dropdownOption && (dropdownOption.classList.contains('disabled') || dropdownOption.getAttribute('aria-disabled') === 'true');
1576
- const selectOption = Array.from(this._element.querySelectorAll('option')).find(
1577
- (opt) => opt.value === value
1578
- ) 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;
1579
1846
  const isNativeDisabled = selectOption && selectOption.disabled;
1580
1847
  return Boolean(isDropdownDisabled || isNativeDisabled);
1581
1848
  }
@@ -1599,9 +1866,16 @@ export class KTSelect extends KTComponent {
1599
1866
  if (event.target === this._searchInputElement) {
1600
1867
  // Allow navigation keys like ArrowDown, ArrowUp, Escape, Enter (for search/selection) to be handled by the logic below.
1601
1868
  // For other keys (characters, space, backspace, delete), let the input field process them.
1602
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' &&
1603
- event.key !== 'Escape' && event.key !== 'Enter' && event.key !== 'Tab' &&
1604
- 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
+ ) {
1605
1879
  // If it's a character key and we are NOT type-to-searching (because search has focus)
1606
1880
  // then let the input field handle it for its own value.
1607
1881
  // The search module's 'input' event will handle filtering based on the input's value.
@@ -1616,11 +1890,16 @@ export class KTSelect extends KTComponent {
1616
1890
  if (event.altKey || event.ctrlKey || event.metaKey) return;
1617
1891
 
1618
1892
  // Type-to-search: only for single char keys, when search input does not have focus
1619
- 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
+ ) {
1620
1899
  buffer.push(event.key);
1621
1900
  const str = buffer.getBuffer();
1622
1901
  if (isOpen) {
1623
- focusManager.focusByString(str);
1902
+ focusManager.focusByString(str);
1624
1903
  } else {
1625
1904
  // If closed, type-to-search could potentially open and select.
1626
1905
  // For now, let's assume it only works when open or opens it first.
@@ -1661,8 +1940,15 @@ export class KTSelect extends KTComponent {
1661
1940
  if (focusedOptionEl) {
1662
1941
  const val = focusedOptionEl.dataset.value;
1663
1942
  // If single select, and the item is already selected, just close.
1664
- if (val !== undefined && !this._config.multiple && this._state.isSelected(val)) {
1665
- 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
+ );
1666
1952
  this.closeDropdown();
1667
1953
  event.preventDefault();
1668
1954
  break;
@@ -1700,30 +1986,39 @@ export class KTSelect extends KTComponent {
1700
1986
  }
1701
1987
 
1702
1988
  public renderDisplayTemplateForSelected(selectedValues: string[]): string {
1703
- const optionsConfig = this._config.optionsConfig as any || {};
1989
+ const optionsConfig = (this._config.optionsConfig as any) || {};
1704
1990
  const displaySeparator = this._config.displaySeparator || ', ';
1705
- const contentArray = Array.from(new Set(
1706
- selectedValues.map(value => {
1707
- const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1708
- if (!option) return '';
1709
-
1710
- let displayTemplate = this._config.displayTemplate;
1711
- const text = option.getAttribute('data-text') || '';
1712
-
1713
- // Replace all {{varname}} in option.innerHTML with values from _config
1714
- Object.entries(optionsConfig[value] || {}).forEach(([key, val]) => {
1715
- if (["string", "number", "boolean"].includes(typeof val)) {
1716
- displayTemplate = displayTemplate.replace(new RegExp(`{{${key}}}`, 'g'), String(val));
1717
- }
1718
- });
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
+ });
1719
2012
 
1720
- return renderTemplateString(displayTemplate, {
1721
- selectedCount: selectedValues.length || 0,
1722
- selectedTexts: this.getSelectedOptionsText() || '',
1723
- text,
1724
- });
1725
- }).filter(Boolean)
1726
- ));
2013
+ return renderTemplateString(displayTemplate, {
2014
+ selectedCount: selectedValues.length || 0,
2015
+ selectedTexts: this.getSelectedOptionsText() || '',
2016
+ text,
2017
+ });
2018
+ })
2019
+ .filter(Boolean),
2020
+ ),
2021
+ );
1727
2022
  return contentArray.join(displaySeparator);
1728
2023
  }
1729
2024
 
@@ -1741,7 +2036,10 @@ export class KTSelect extends KTComponent {
1741
2036
  if (mutation.type === 'childList') {
1742
2037
  // Option(s) added or removed
1743
2038
  needsRebuild = true;
1744
- } else if (mutation.type === 'attributes' && mutation.target instanceof HTMLOptionElement) {
2039
+ } else if (
2040
+ mutation.type === 'attributes' &&
2041
+ mutation.target instanceof HTMLOptionElement
2042
+ ) {
1745
2043
  if (mutation.attributeName === 'selected') {
1746
2044
  needsSelectionSync = true;
1747
2045
  }
@@ -1768,7 +2066,9 @@ export class KTSelect extends KTComponent {
1768
2066
  private _rebuildOptionsFromNative() {
1769
2067
  // Remove and rebuild the custom dropdown options from the native select
1770
2068
  if (this._dropdownContentElement) {
1771
- const optionsContainer = this._dropdownContentElement.querySelector('[data-kt-select-options]');
2069
+ const optionsContainer = this._dropdownContentElement.querySelector(
2070
+ '[data-kt-select-options]',
2071
+ );
1772
2072
  if (optionsContainer) {
1773
2073
  optionsContainer.innerHTML = '';
1774
2074
  const options = Array.from(this._element.querySelectorAll('option'));
@@ -1784,7 +2084,9 @@ export class KTSelect extends KTComponent {
1784
2084
  optionsContainer.appendChild(renderedOption);
1785
2085
  });
1786
2086
  // Update internal references
1787
- 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>;
1788
2090
  }
1789
2091
  }
1790
2092
  // Sync selection after rebuilding
@@ -1795,9 +2097,78 @@ export class KTSelect extends KTComponent {
1795
2097
 
1796
2098
  private _syncSelectionFromNative() {
1797
2099
  // Sync internal state from the native select's selected options
1798
- const selected = Array.from(this._element.querySelectorAll('option:checked')).map(opt => (opt as HTMLOptionElement).value);
1799
- 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
+ );
2106
+ this.updateSelectedOptionDisplay();
2107
+ this._updateSelectedOptionClass();
2108
+ this.updateSelectAllButtonState();
2109
+ }
2110
+
2111
+ private _handleSelectAllClick(event: Event): void {
2112
+ event.preventDefault();
2113
+ event.stopPropagation();
2114
+
2115
+ const visibleOptions = this._focusManager
2116
+ .getVisibleOptions()
2117
+ .filter((opt) => opt.getAttribute('aria-disabled') !== 'true');
2118
+ if (visibleOptions.length === 0) return;
2119
+
2120
+ const visibleValues = visibleOptions.map(
2121
+ (opt) => opt.dataset.value as string,
2122
+ );
2123
+ const selectedValues = new Set(this.getSelectedOptions());
2124
+ const isAllSelected = visibleOptions.every((opt) =>
2125
+ selectedValues.has(opt.dataset.value as string),
2126
+ );
2127
+
2128
+ if (isAllSelected) {
2129
+ // Deselect all visible
2130
+ visibleValues.forEach((value) => selectedValues.delete(value));
2131
+ } else {
2132
+ // Select all visible
2133
+ visibleValues.forEach((value) => selectedValues.add(value));
2134
+ }
2135
+
2136
+ this._state.setSelectedOptions(Array.from(selectedValues));
1800
2137
  this.updateSelectedOptionDisplay();
1801
2138
  this._updateSelectedOptionClass();
2139
+ this.updateSelectAllButtonState();
2140
+
2141
+ this._dispatchEvent('change');
2142
+ this._fireEvent('change');
2143
+ }
2144
+
2145
+ public updateSelectAllButtonState(): void {
2146
+ if (
2147
+ !this._config.multiple ||
2148
+ !this._config.enableSelectAll ||
2149
+ !this._selectAllButtonToggle
2150
+ ) {
2151
+ return;
2152
+ }
2153
+
2154
+ const visibleOptions = this._focusManager
2155
+ .getVisibleOptions()
2156
+ .filter((opt) => opt.getAttribute('aria-disabled') !== 'true');
2157
+
2158
+ if (visibleOptions.length === 0) {
2159
+ this._selectAllButton.style.display = 'none';
2160
+ return;
2161
+ }
2162
+
2163
+ this._selectAllButton.style.display = '';
2164
+
2165
+ const selectedValues = new Set(this.getSelectedOptions());
2166
+ const isAllSelected = visibleOptions.every((opt) =>
2167
+ selectedValues.has(opt.dataset.value as string),
2168
+ );
2169
+
2170
+ this._selectAllButtonToggle.textContent = isAllSelected
2171
+ ? this._config.clearAllText
2172
+ : this._config.selectAllText;
1802
2173
  }
1803
2174
  }