@keenthemes/ktui 1.0.11 → 1.0.12

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 (129) hide show
  1. package/dist/ktui.js +1283 -1096
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/examples/select/basic-usage.html +43 -0
  5. package/examples/select/combobox-icons.html +58 -0
  6. package/examples/select/combobox.html +46 -0
  7. package/examples/select/description.html +69 -0
  8. package/examples/select/disable-option.html +43 -0
  9. package/examples/select/disable-select.html +34 -0
  10. package/examples/select/icon-description.html +56 -0
  11. package/examples/select/icon-multiple.html +58 -0
  12. package/examples/select/icon.html +58 -0
  13. package/examples/select/max-selection.html +39 -0
  14. package/examples/select/modal.html +70 -0
  15. package/examples/select/multiple.html +42 -0
  16. package/examples/select/placeholder.html +43 -0
  17. package/examples/select/remote-data.html +32 -0
  18. package/examples/select/search.html +49 -0
  19. package/examples/select/tags-icons.html +58 -0
  20. package/examples/select/tags-selected.html +59 -0
  21. package/examples/select/tags.html +58 -0
  22. package/examples/select/template-customization.html +65 -0
  23. package/examples/select/test.html +94 -0
  24. package/examples/toast/example.html +427 -0
  25. package/lib/cjs/components/component.js +1 -1
  26. package/lib/cjs/components/component.js.map +1 -1
  27. package/lib/cjs/components/datatable/datatable.js +22 -6
  28. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  29. package/lib/cjs/components/select/combobox.js +38 -120
  30. package/lib/cjs/components/select/combobox.js.map +1 -1
  31. package/lib/cjs/components/select/config.js +4 -16
  32. package/lib/cjs/components/select/config.js.map +1 -1
  33. package/lib/cjs/components/select/dropdown.js +10 -49
  34. package/lib/cjs/components/select/dropdown.js.map +1 -1
  35. package/lib/cjs/components/select/index.js +2 -1
  36. package/lib/cjs/components/select/index.js.map +1 -1
  37. package/lib/cjs/components/select/option.js +21 -4
  38. package/lib/cjs/components/select/option.js.map +1 -1
  39. package/lib/cjs/components/select/remote.js +1 -37
  40. package/lib/cjs/components/select/remote.js.map +1 -1
  41. package/lib/cjs/components/select/search.js +11 -41
  42. package/lib/cjs/components/select/search.js.map +1 -1
  43. package/lib/cjs/components/select/select.js +213 -326
  44. package/lib/cjs/components/select/select.js.map +1 -1
  45. package/lib/cjs/components/select/tags.js +39 -31
  46. package/lib/cjs/components/select/tags.js.map +1 -1
  47. package/lib/cjs/components/select/templates.js +120 -179
  48. package/lib/cjs/components/select/templates.js.map +1 -1
  49. package/lib/cjs/components/select/types.js +0 -12
  50. package/lib/cjs/components/select/types.js.map +1 -1
  51. package/lib/cjs/components/select/utils.js +204 -257
  52. package/lib/cjs/components/select/utils.js.map +1 -1
  53. package/lib/cjs/components/toast/index.js +10 -0
  54. package/lib/cjs/components/toast/index.js.map +1 -0
  55. package/lib/cjs/components/toast/toast.js +543 -0
  56. package/lib/cjs/components/toast/toast.js.map +1 -0
  57. package/lib/cjs/components/toast/types.js +7 -0
  58. package/lib/cjs/components/toast/types.js.map +1 -0
  59. package/lib/cjs/helpers/dom.js +24 -0
  60. package/lib/cjs/helpers/dom.js.map +1 -1
  61. package/lib/cjs/index.js +5 -1
  62. package/lib/cjs/index.js.map +1 -1
  63. package/lib/esm/components/component.js +1 -1
  64. package/lib/esm/components/component.js.map +1 -1
  65. package/lib/esm/components/datatable/datatable.js +22 -6
  66. package/lib/esm/components/datatable/datatable.js.map +1 -1
  67. package/lib/esm/components/select/combobox.js +39 -121
  68. package/lib/esm/components/select/combobox.js.map +1 -1
  69. package/lib/esm/components/select/config.js +3 -15
  70. package/lib/esm/components/select/config.js.map +1 -1
  71. package/lib/esm/components/select/dropdown.js +10 -49
  72. package/lib/esm/components/select/dropdown.js.map +1 -1
  73. package/lib/esm/components/select/index.js +1 -1
  74. package/lib/esm/components/select/index.js.map +1 -1
  75. package/lib/esm/components/select/option.js +21 -4
  76. package/lib/esm/components/select/option.js.map +1 -1
  77. package/lib/esm/components/select/remote.js +1 -37
  78. package/lib/esm/components/select/remote.js.map +1 -1
  79. package/lib/esm/components/select/search.js +12 -42
  80. package/lib/esm/components/select/search.js.map +1 -1
  81. package/lib/esm/components/select/select.js +214 -327
  82. package/lib/esm/components/select/select.js.map +1 -1
  83. package/lib/esm/components/select/tags.js +39 -31
  84. package/lib/esm/components/select/tags.js.map +1 -1
  85. package/lib/esm/components/select/templates.js +119 -178
  86. package/lib/esm/components/select/templates.js.map +1 -1
  87. package/lib/esm/components/select/types.js +1 -11
  88. package/lib/esm/components/select/types.js.map +1 -1
  89. package/lib/esm/components/select/utils.js +201 -255
  90. package/lib/esm/components/select/utils.js.map +1 -1
  91. package/lib/esm/components/toast/index.js +6 -0
  92. package/lib/esm/components/toast/index.js.map +1 -0
  93. package/lib/esm/components/toast/toast.js +540 -0
  94. package/lib/esm/components/toast/toast.js.map +1 -0
  95. package/lib/esm/components/toast/types.js +6 -0
  96. package/lib/esm/components/toast/types.js.map +1 -0
  97. package/lib/esm/helpers/dom.js +24 -0
  98. package/lib/esm/helpers/dom.js.map +1 -1
  99. package/lib/esm/index.js +3 -0
  100. package/lib/esm/index.js.map +1 -1
  101. package/package.json +8 -6
  102. package/src/components/alert/alert.css +15 -2
  103. package/src/components/component.ts +4 -0
  104. package/src/components/datatable/datatable.ts +24 -16
  105. package/src/components/input/input.css +3 -1
  106. package/src/components/link/link.css +2 -2
  107. package/src/components/select/combobox.ts +42 -149
  108. package/src/components/select/config.ts +38 -33
  109. package/src/components/select/dropdown.ts +8 -55
  110. package/src/components/select/index.ts +1 -1
  111. package/src/components/select/option.ts +28 -7
  112. package/src/components/select/remote.ts +2 -42
  113. package/src/components/select/search.ts +14 -54
  114. package/src/components/select/select.css +49 -0
  115. package/src/components/select/select.ts +231 -437
  116. package/src/components/select/tags.ts +40 -37
  117. package/src/components/select/templates.ts +166 -303
  118. package/src/components/select/types.ts +0 -10
  119. package/src/components/select/utils.ts +214 -304
  120. package/src/components/textarea/textarea.css +2 -1
  121. package/src/components/toast/index.ts +7 -0
  122. package/src/components/toast/toast.css +60 -0
  123. package/src/components/toast/toast.ts +605 -0
  124. package/src/components/toast/types.ts +169 -0
  125. package/src/helpers/dom.ts +30 -0
  126. package/src/index.ts +4 -0
  127. package/styles/main.css +3 -0
  128. package/styles/vars.css +138 -0
  129. package/styles.css +1 -0
@@ -18,13 +18,12 @@ import { defaultTemplates } from './templates';
18
18
  import { KTSelectCombobox } from './combobox';
19
19
  import { KTSelectDropdown } from './dropdown';
20
20
  import {
21
- handleDropdownKeyNavigation,
22
- filterOptions,
23
21
  FocusManager,
24
22
  EventManager,
23
+ renderTemplateString,
24
+ TypeToSearchBuffer,
25
25
  } from './utils';
26
26
  import { KTSelectTags } from './tags';
27
- import { SelectMode } from './types';
28
27
 
29
28
  export class KTSelect extends KTComponent {
30
29
  // Core properties
@@ -52,6 +51,7 @@ export class KTSelect extends KTComponent {
52
51
  private _loadMoreIndicator: HTMLElement | null = null;
53
52
  private _focusManager: FocusManager;
54
53
  private _eventManager: EventManager;
54
+ private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
55
55
 
56
56
  /**
57
57
  * Constructor: Initializes the select component
@@ -152,27 +152,18 @@ export class KTSelect extends KTComponent {
152
152
  this._element.querySelectorAll('option:not([value=""])'),
153
153
  );
154
154
  options.forEach((option) => option.remove());
155
-
156
- // Ensure we have at least an empty option
157
- if (this._element.querySelectorAll('option').length === 0) {
158
- const emptyOption = defaultTemplates.emptyOption({
159
- ...this._config,
160
- placeholder: this._config.placeholder,
161
- });
162
- this._element.appendChild(emptyOption);
163
- }
164
155
  }
165
156
 
166
157
  /**
167
158
  * Helper to show a dropdown message (error, loading, noResults)
168
159
  */
169
160
  private _showDropdownMessage(
170
- type: 'error' | 'loading' | 'noResults',
161
+ type: 'error' | 'loading' | 'empty',
171
162
  message?: string,
172
163
  ) {
173
164
  if (!this._dropdownContentElement) return;
174
165
  const optionsContainer = this._dropdownContentElement.querySelector(
175
- '[data-kt-select-options-container]',
166
+ '[data-kt-select-options]',
176
167
  );
177
168
  if (!optionsContainer) return;
178
169
 
@@ -189,9 +180,9 @@ export class KTSelect extends KTComponent {
189
180
  message || 'Loading...',
190
181
  ).outerHTML;
191
182
  break;
192
- case 'noResults':
183
+ case 'empty':
193
184
  optionsContainer.innerHTML = '';
194
- optionsContainer.appendChild(defaultTemplates.noResults(this._config));
185
+ optionsContainer.appendChild(defaultTemplates.empty(this._config));
195
186
  break;
196
187
  }
197
188
  }
@@ -214,21 +205,6 @@ export class KTSelect extends KTComponent {
214
205
  * @param message Error message
215
206
  */
216
207
  private _renderErrorState(message: string) {
217
- // Create error option if the select is empty
218
- if (this._element.querySelectorAll('option').length <= 1) {
219
- const loadingOptions = this._element.querySelectorAll(
220
- 'option[disabled]:not([value])',
221
- );
222
- loadingOptions.forEach((option) => option.remove());
223
-
224
- // Use template function for error option instead of hardcoded element
225
- const errorOption = defaultTemplates.errorOption({
226
- ...this._config,
227
- errorMessage: message,
228
- });
229
- this._element.appendChild(errorOption);
230
- }
231
-
232
208
  // If dropdown is already created, show error message there
233
209
  this._showDropdownMessage('error', message);
234
210
 
@@ -255,7 +231,7 @@ export class KTSelect extends KTComponent {
255
231
 
256
232
  // Add to dropdown
257
233
  const optionsContainer = this._dropdownContentElement.querySelector(
258
- '[data-kt-select-options-container]',
234
+ '[data-kt-select-options]',
259
235
  );
260
236
  if (optionsContainer) {
261
237
  optionsContainer.appendChild(this._loadMoreIndicator);
@@ -337,54 +313,28 @@ export class KTSelect extends KTComponent {
337
313
  if (!this._dropdownContentElement || !newItems.length) return;
338
314
 
339
315
  const optionsContainer = this._dropdownContentElement.querySelector(
340
- '[data-kt-select-options-container]',
316
+ `[data-kt-select-options]`,
341
317
  );
342
318
  if (!optionsContainer) return;
343
319
 
344
320
  // Get the load more button
345
321
  const loadMoreButton = optionsContainer.querySelector(
346
- '[data-kt-select-load-more]',
322
+ `[data-kt-select-load-more]`,
347
323
  );
348
324
 
349
325
  // Process each new item
350
326
  newItems.forEach((item) => {
351
327
  // Create option for the original select
352
- const selectOption = defaultTemplates.emptyOption({
353
- ...this._config,
354
- placeholder: item.title || 'Unnamed option',
355
- });
328
+ const selectOption = document.createElement('option');
356
329
  selectOption.value = item.id || '';
357
330
 
358
- // Add description and icon attributes if available and valid
359
- if (
360
- item.description &&
361
- item.description !== 'null' &&
362
- item.description !== 'undefined'
363
- ) {
364
- selectOption.setAttribute(
365
- 'data-kt-select-option-description',
366
- item.description,
367
- );
368
- }
369
- if (item.icon && item.icon !== 'null' && item.icon !== 'undefined') {
370
- selectOption.setAttribute('data-kt-select-option-icon', item.icon);
371
- }
372
-
373
- // Add the option to the original select element
374
- this._element.appendChild(selectOption);
375
-
376
- // Create option element for the dropdown using the KTSelectOption class
377
- // This ensures consistent option rendering
378
- const ktOption = new KTSelectOption(selectOption, this._config);
379
- const renderedOption = ktOption.render();
380
-
381
331
  // Add to dropdown container
382
332
  if (loadMoreButton) {
383
333
  // Insert before the load more button
384
- optionsContainer.insertBefore(renderedOption, loadMoreButton);
334
+ optionsContainer.insertBefore(selectOption, loadMoreButton);
385
335
  } else {
386
336
  // Append to the end
387
- optionsContainer.appendChild(renderedOption);
337
+ optionsContainer.appendChild(selectOption);
388
338
  }
389
339
  });
390
340
 
@@ -413,7 +363,7 @@ export class KTSelect extends KTComponent {
413
363
  this._initZIndex();
414
364
 
415
365
  // Initialize options
416
- this._initializeOptionsHtml();
366
+ // this._initializeOptionsHtml();
417
367
  this._preSelectOptions(this._element);
418
368
 
419
369
  // Apply disabled state if needed
@@ -425,12 +375,12 @@ export class KTSelect extends KTComponent {
425
375
  }
426
376
 
427
377
  // Initialize combobox if enabled
428
- if (this._config.mode === SelectMode.COMBOBOX) {
378
+ if (this._config.combobox) {
429
379
  this._comboboxModule = new KTSelectCombobox(this);
430
380
  }
431
381
 
432
382
  // Initialize tags if enabled
433
- if (this._config.mode === SelectMode.TAGS) {
383
+ if (this._config.tags) {
434
384
  this._tagsModule = new KTSelectTags(this);
435
385
  }
436
386
 
@@ -461,9 +411,9 @@ export class KTSelect extends KTComponent {
461
411
  /**
462
412
  * Initialize options HTML from data
463
413
  */
464
- private _initializeOptionsHtml() {
465
- this._generateOptionsHtml(this._element);
466
- }
414
+ // private _initializeOptionsHtml() {
415
+ // this._generateOptionsHtml(this._element);
416
+ // }
467
417
 
468
418
  /**
469
419
  * Creates the HTML structure for the select component
@@ -472,29 +422,36 @@ export class KTSelect extends KTComponent {
472
422
  const options = Array.from(this._element.querySelectorAll('option'));
473
423
 
474
424
  // Create wrapper and display elements
475
- const wrapperElement = defaultTemplates.main(this._config);
425
+ const wrapperElement = defaultTemplates.wrapper(this._config);
426
+
476
427
  const displayElement = defaultTemplates.display(this._config);
477
428
 
478
429
  // Add the display element to the wrapper
479
430
  wrapperElement.appendChild(displayElement);
480
431
 
432
+ // Move classes from original select to display element
433
+ if (this._element.classList.length > 0) {
434
+ displayElement.classList.add(...Array.from(this._element.classList));
435
+ this._element.className = '';
436
+ }
437
+
481
438
  // Create an empty dropdown first (without options) using template
482
- const dropdownElement = defaultTemplates.dropdownContent({
439
+ const dropdownElement = defaultTemplates.dropdown({
483
440
  ...this._config,
484
441
  zindex: this._config.dropdownZindex,
485
442
  });
486
443
 
487
444
  // Add search input if needed
488
- const isCombobox = this._config.mode === SelectMode.COMBOBOX;
489
- const hasSearch = this._config.enableSearch && !isCombobox;
490
-
491
- if (hasSearch) {
445
+ if (this._config.enableSearch) {
492
446
  const searchElement = defaultTemplates.search(this._config);
493
447
  dropdownElement.appendChild(searchElement);
494
448
  }
495
449
 
496
450
  // Create options container using template
497
- const optionsContainer = defaultTemplates.optionsContainer(this._config);
451
+ const optionsContainer = defaultTemplates.options(this._config);
452
+
453
+ // Clear the options container
454
+ optionsContainer.innerHTML = '';
498
455
 
499
456
  // Add each option directly to the container
500
457
  options.forEach((optionElement) => {
@@ -539,37 +496,24 @@ export class KTSelect extends KTComponent {
539
496
 
540
497
  // Get dropdown content element - this is critical for dropdown functionality
541
498
  this._dropdownContentElement = this._wrapperElement.querySelector(
542
- `[data-kt-select-dropdown-content]`,
499
+ `[data-kt-select-dropdown]`,
543
500
  ) as HTMLElement;
544
501
 
545
502
  if (!this._dropdownContentElement) {
503
+ console.log(this._element)
546
504
  console.error('Dropdown content element not found', this._wrapperElement);
547
505
  }
548
506
 
549
507
  // Get search input element - this is used for the search functionality
550
- // First check if it's in dropdown, then check if it's in display (for combobox)
551
508
  this._searchInputElement = this._dropdownContentElement.querySelector(
552
509
  `[data-kt-select-search]`,
553
510
  ) as HTMLInputElement;
554
511
 
555
- // If not found in dropdown, check if it's the display element itself (for combobox)
556
- if (
557
- !this._searchInputElement &&
558
- this._config.mode === SelectMode.COMBOBOX
559
- ) {
512
+ // If not found in dropdown, check if it's the display element itself
513
+ if (!this._searchInputElement) {
560
514
  this._searchInputElement = this._displayElement as HTMLInputElement;
561
515
  }
562
516
 
563
- if (this._config.debug)
564
- console.log(
565
- 'Search input found:',
566
- this._searchInputElement ? 'Yes' : 'No',
567
- 'Mode:',
568
- this._config.mode,
569
- 'EnableSearch:',
570
- this._config.enableSearch,
571
- );
572
-
573
517
  this._valueDisplayElement = this._wrapperElement.querySelector(
574
518
  `[data-kt-select-value]`,
575
519
  ) as HTMLElement;
@@ -585,7 +529,6 @@ export class KTSelect extends KTComponent {
585
529
  private _attachEventListeners() {
586
530
  // Document level event listeners
587
531
  document.addEventListener('click', this._handleDocumentClick.bind(this));
588
- document.addEventListener('keydown', this._handleEscKey.bind(this));
589
532
 
590
533
  // Dropdown option click events
591
534
  this._eventManager.addListener(
@@ -595,24 +538,16 @@ export class KTSelect extends KTComponent {
595
538
  );
596
539
 
597
540
  // Only attach click handler to display element
598
- this._eventManager.addListener(
599
- this._displayElement,
600
- 'click',
601
- this._handleDropdownClick.bind(this),
602
- );
603
-
604
- // Only attach keyboard navigation to display element if NOT in combobox mode
605
- // This prevents conflicts with the combobox module's keyboard handler
606
- if (this._config.mode !== SelectMode.COMBOBOX) {
607
- if (this._config.debug)
608
- console.log(
609
- 'Attaching keyboard navigation to display element (non-combobox mode)',
610
- );
611
- this._eventManager.addListener(
612
- this._displayElement,
613
- 'keydown',
614
- this._handleDropdownKeyDown.bind(this),
615
- );
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));
616
551
  }
617
552
  }
618
553
 
@@ -701,75 +636,16 @@ export class KTSelect extends KTComponent {
701
636
  extractedLabel !== null ? String(extractedLabel) : 'Unnamed option';
702
637
  }
703
638
 
704
- // Get description - skip if null, undefined, or "null" string
705
- let description = null;
706
- if (
707
- item.description !== undefined &&
708
- item.description !== null &&
709
- String(item.description) !== 'null' &&
710
- String(item.description) !== 'undefined'
711
- ) {
712
- description = String(item.description);
713
- } else if (this._config.dataFieldDescription) {
714
- const extractedDesc = this._getValueByKey(
715
- item,
716
- this._config.dataFieldDescription,
717
- );
718
- if (
719
- extractedDesc !== null &&
720
- extractedDesc !== undefined &&
721
- String(extractedDesc) !== 'null' &&
722
- String(extractedDesc) !== 'undefined'
723
- ) {
724
- description = String(extractedDesc);
725
- }
726
- }
727
-
728
- // Get icon - skip if null, undefined, or "null" string
729
- let icon = null;
730
- if (
731
- item.icon !== undefined &&
732
- item.icon !== null &&
733
- String(item.icon) !== 'null' &&
734
- String(item.icon) !== 'undefined'
735
- ) {
736
- icon = String(item.icon);
737
- } else if (this._config.dataFieldIcon) {
738
- const extractedIcon = this._getValueByKey(
739
- item,
740
- this._config.dataFieldIcon,
741
- );
742
- if (
743
- extractedIcon !== null &&
744
- extractedIcon !== undefined &&
745
- String(extractedIcon) !== 'null' &&
746
- String(extractedIcon) !== 'undefined'
747
- ) {
748
- icon = String(extractedIcon);
749
- }
750
- }
751
-
752
639
  // Log the extracted values for debugging
753
640
  if (this._config.debug)
754
641
  console.log(
755
- `Option: value=${value}, label=${label}, desc=${description ? description : 'none'}, icon=${icon ? icon : 'none'}`,
642
+ `Option: value=${value}, label=${label}`,
756
643
  );
757
644
 
758
645
  // Set option attributes
759
646
  optionElement.value = value;
760
647
  optionElement.textContent = label || 'Unnamed option';
761
648
 
762
- if (description) {
763
- optionElement.setAttribute(
764
- 'data-kt-select-option-description',
765
- description,
766
- );
767
- }
768
-
769
- if (icon) {
770
- optionElement.setAttribute('data-kt-select-option-icon', icon);
771
- }
772
-
773
649
  if (item.selected) {
774
650
  optionElement.setAttribute('selected', 'selected');
775
651
  }
@@ -855,8 +731,13 @@ export class KTSelect extends KTComponent {
855
731
 
856
732
  /**
857
733
  * Toggle dropdown visibility
734
+ * @deprecated
858
735
  */
859
736
  public toggleDropdown() {
737
+ if (this._config.disabled) {
738
+ if (this._config.debug) console.log('toggleDropdown: select is disabled, not opening');
739
+ return;
740
+ }
860
741
  if (this._config.debug) console.log('toggleDropdown called');
861
742
  if (this._dropdownModule) {
862
743
  // Always use the dropdown module's state to determine whether to open or close
@@ -874,6 +755,10 @@ export class KTSelect extends KTComponent {
874
755
  * Open the dropdown
875
756
  */
876
757
  public openDropdown() {
758
+ if (this._config.disabled) {
759
+ if (this._config.debug) console.log('openDropdown: select is disabled, not opening');
760
+ return;
761
+ }
877
762
  if (this._config.debug)
878
763
  console.log(
879
764
  'openDropdown called, dropdownModule exists:',
@@ -1010,6 +895,12 @@ export class KTSelect extends KTComponent {
1010
895
  * Select an option by value
1011
896
  */
1012
897
  private _selectOption(value: string) {
898
+ // Prevent selection if the option is disabled (in dropdown or original select)
899
+ if (this._isOptionDisabled(value)) {
900
+ if (this._config.debug) console.log('_selectOption: Option is disabled, ignoring selection');
901
+ return;
902
+ }
903
+
1013
904
  // Get current selection state
1014
905
  const isSelected = this._state.isSelected(value);
1015
906
 
@@ -1064,106 +955,37 @@ export class KTSelect extends KTComponent {
1064
955
  public updateSelectedOptionDisplay() {
1065
956
  const selectedOptions = this.getSelectedOptions();
1066
957
 
1067
- if (this._config.renderSelected) {
958
+ // Tag mode: render tags if enabled
959
+ if (this._config.tags && this._tagsModule) {
960
+ this._tagsModule.updateTagsDisplay(selectedOptions);
961
+ return;
962
+ }
963
+
964
+ if (typeof this._config.renderSelected === 'function') {
1068
965
  // Use the custom renderSelected function if provided
1069
- this._updateValueDisplay(this._config.renderSelected(selectedOptions));
966
+ this._valueDisplayElement.innerHTML = this._config.renderSelected(selectedOptions);
1070
967
  } else {
968
+
1071
969
  if (selectedOptions.length === 0) {
1072
- if (this._config.mode !== SelectMode.COMBOBOX) {
1073
- this._updateValueDisplay(this._config.placeholder); // Use innerHTML for placeholder
1074
- }
1075
- } else if (this._config.multiple) {
1076
- if (this._config.mode === SelectMode.TAGS) {
1077
- // Use the tags module to render selected options as tags
1078
- if (this._tagsModule) {
1079
- this._tagsModule.updateTagsDisplay(selectedOptions);
1080
- } else {
1081
- // Fallback if tags module not initialized for some reason
1082
- this._updateValueDisplay(selectedOptions.join(', '));
1083
- }
1084
- } else {
1085
- // Render as comma-separated values
1086
- const displayText = selectedOptions
1087
- .map((option) => this._getOptionInnerHtml(option) || '')
1088
- .join(', ');
1089
- this._updateValueDisplay(displayText);
1090
- }
970
+ const placeholder = defaultTemplates.placeholder(this._config);
971
+ this._valueDisplayElement.replaceChildren(placeholder);
972
+
1091
973
  } else {
1092
- const selectedOption = selectedOptions[0];
1093
- if (selectedOption) {
1094
- const selectedText = this._getOptionInnerHtml(selectedOption);
1095
- this._updateValueDisplay(selectedText);
974
+ let content = '';
1096
975
 
1097
- // Update combobox input value if in combobox mode
1098
- if (
1099
- this._config.mode === SelectMode.COMBOBOX &&
1100
- this._comboboxModule
1101
- ) {
1102
- this._comboboxModule.updateSelectedValue(selectedText);
1103
- }
976
+ if (this._config.displayTemplate) {
977
+ const selectedValues = this.getSelectedOptions();
978
+ content = this.renderDisplayTemplateForSelected(selectedValues);
1104
979
  } else {
1105
- this._updateValueDisplay(this._config.placeholder);
980
+ // If no displayTemplate is provided, use the default comma-separated list of selected options
981
+ content = this.getSelectedOptionsText();
1106
982
  }
1107
- }
1108
- }
1109
983
 
1110
- // Update any debug display boxes if they exist
1111
- this._updateDebugDisplays();
1112
- }
1113
-
1114
- /**
1115
- * Update the value display element
1116
- */
1117
- private _updateValueDisplay(value: string) {
1118
- if (this._config.mode === SelectMode.COMBOBOX) {
1119
- // For combobox, we only update the hidden value element, not the input
1120
- // The combobox module will handle updating the input value
1121
- if (!this._comboboxModule) {
1122
- (this._valueDisplayElement as HTMLInputElement).value = value;
984
+ this._valueDisplayElement.innerHTML = content;
1123
985
  }
1124
- } else {
1125
- this._valueDisplayElement.innerHTML = value;
1126
986
  }
1127
987
  }
1128
988
 
1129
- /**
1130
- * Update debug displays if present
1131
- */
1132
- private _updateDebugDisplays() {
1133
- // Check if we're in a test environment with debug boxes
1134
- const selectId = this.getElement().id;
1135
- if (selectId) {
1136
- const debugElement = document.getElementById(`${selectId}-value`);
1137
- if (debugElement) {
1138
- const selectedOptions = this.getSelectedOptions();
1139
-
1140
- // Format display based on selection mode
1141
- if (this._config.multiple) {
1142
- // For multiple selection, show comma-separated list
1143
- debugElement.textContent =
1144
- selectedOptions.length > 0 ? selectedOptions.join(', ') : 'None';
1145
- } else {
1146
- // For single selection, show just the one value
1147
- debugElement.textContent =
1148
- selectedOptions.length > 0 ? selectedOptions[0] : 'None';
1149
- }
1150
- }
1151
- }
1152
- }
1153
-
1154
- /**
1155
- * Get option inner HTML content by option value
1156
- */
1157
- private _getOptionInnerHtml(optionValue: string) {
1158
- const option = Array.from(this._options).find(
1159
- (opt) => opt.dataset.value === optionValue,
1160
- );
1161
- if (this._config.mode == SelectMode.COMBOBOX) {
1162
- return option.textContent;
1163
- }
1164
- return option.innerHTML; // Get the entire HTML content of the option
1165
- }
1166
-
1167
989
  /**
1168
990
  * Update CSS classes for selected options
1169
991
  */
@@ -1215,19 +1037,6 @@ export class KTSelect extends KTComponent {
1215
1037
  this.updateSelectedOptionDisplay();
1216
1038
  this._updateSelectedOptionClass();
1217
1039
 
1218
- // For combobox, also clear the input value
1219
- if (this._config.mode === SelectMode.COMBOBOX) {
1220
- if (this._searchInputElement) {
1221
- this._searchInputElement.value = '';
1222
- }
1223
-
1224
- // If combobox has a clear button, hide it
1225
- if (this._comboboxModule) {
1226
- // The combobox module will handle hiding the clear button
1227
- this._comboboxModule.resetInputValueToSelection();
1228
- }
1229
- }
1230
-
1231
1040
  // Dispatch change event
1232
1041
  this._dispatchEvent('change');
1233
1042
  this._fireEvent('change');
@@ -1241,56 +1050,6 @@ export class KTSelect extends KTComponent {
1241
1050
  this._state.setSelectedOptions(values);
1242
1051
  }
1243
1052
 
1244
- /**
1245
- * ========================================================================
1246
- * KEYBOARD NAVIGATION
1247
- * ========================================================================
1248
- */
1249
-
1250
- /**
1251
- * Handle dropdown key down events for keyboard navigation
1252
- * Only used for standard (non-combobox) dropdowns
1253
- */
1254
- private _handleDropdownKeyDown(event: KeyboardEvent) {
1255
- // Log event for debugging
1256
- if (this._config.debug)
1257
- console.log('Standard dropdown keydown:', event.key);
1258
-
1259
- // Use the shared handler
1260
- handleDropdownKeyNavigation(event, this, {
1261
- multiple: this._config.multiple,
1262
- closeOnSelect: this._config.closeOnSelect,
1263
- });
1264
- }
1265
-
1266
- /**
1267
- * Focus next option in dropdown
1268
- */
1269
- private _focusNextOption(): Element | null {
1270
- return this._focusManager.focusNext();
1271
- }
1272
-
1273
- /**
1274
- * Focus previous option in dropdown
1275
- */
1276
- private _focusPreviousOption(): Element | null {
1277
- return this._focusManager.focusPrevious();
1278
- }
1279
-
1280
- /**
1281
- * Apply hover/focus state to focused option
1282
- */
1283
- private _hoverFocusedOption(option: Element) {
1284
- this._focusManager.applyFocus(option as HTMLElement);
1285
- }
1286
-
1287
- /**
1288
- * Scroll option into view when navigating
1289
- */
1290
- private _scrollOptionIntoView(option: Element) {
1291
- this._focusManager.scrollIntoView(option as HTMLElement);
1292
- }
1293
-
1294
1053
  /**
1295
1054
  * Select the currently focused option
1296
1055
  */
@@ -1317,54 +1076,7 @@ export class KTSelect extends KTComponent {
1317
1076
  if (selectedValue) {
1318
1077
  this._selectOption(selectedValue);
1319
1078
  }
1320
-
1321
- // For combobox mode, update input value AFTER selection to ensure consistency
1322
- if (this._config.mode === SelectMode.COMBOBOX && this._comboboxModule) {
1323
- this._comboboxModule.updateSelectedValue(selectedText);
1324
- // Also directly update the input value for immediate visual feedback
1325
- if (this._searchInputElement) {
1326
- this._searchInputElement.value = selectedText;
1327
- }
1328
- }
1329
- }
1330
- }
1331
-
1332
- /**
1333
- * ========================================================================
1334
- * COMBOBOX SPECIFIC METHODS
1335
- * ========================================================================
1336
- */
1337
-
1338
- /**
1339
- * Handle combobox input events
1340
- */
1341
- private _handleComboboxInput(event: Event) {
1342
- if (this._comboboxModule) {
1343
- return;
1344
- }
1345
-
1346
- const inputElement = event.target as HTMLInputElement;
1347
- const query = inputElement.value.toLowerCase();
1348
-
1349
- // If dropdown isn't open, open it when user starts typing
1350
- if (!this._dropdownIsOpen) {
1351
- this.openDropdown();
1352
1079
  }
1353
-
1354
- // Filter options based on input
1355
- this._filterOptionsForCombobox(query);
1356
- }
1357
-
1358
- /**
1359
- * Filter options for combobox based on input query
1360
- * Uses the shared filterOptions function
1361
- */
1362
- private _filterOptionsForCombobox(query: string) {
1363
- const options = Array.from(
1364
- this._dropdownContentElement.querySelectorAll('[data-kt-select-option]'),
1365
- ) as HTMLElement[];
1366
-
1367
- filterOptions(options, query, this._config, this._dropdownContentElement);
1368
1080
  }
1369
1081
 
1370
1082
  /**
@@ -1375,6 +1087,7 @@ export class KTSelect extends KTComponent {
1375
1087
 
1376
1088
  /**
1377
1089
  * Handle display element click
1090
+ * @deprecated
1378
1091
  */
1379
1092
  private _handleDropdownClick(event: Event) {
1380
1093
  if (this._config.debug)
@@ -1447,15 +1160,6 @@ export class KTSelect extends KTComponent {
1447
1160
  }
1448
1161
  }
1449
1162
 
1450
- /**
1451
- * Handle escape key press
1452
- */
1453
- private _handleEscKey(event: KeyboardEvent) {
1454
- if (event.key === 'Escape' && this._dropdownIsOpen) {
1455
- this.closeDropdown();
1456
- }
1457
- }
1458
-
1459
1163
  /**
1460
1164
  * ========================================================================
1461
1165
  * ACCESSIBILITY METHODS
@@ -1472,20 +1176,6 @@ export class KTSelect extends KTComponent {
1472
1176
  );
1473
1177
  }
1474
1178
 
1475
- /**
1476
- * Handle focus events
1477
- */
1478
- private _handleFocus() {
1479
- // Implementation pending
1480
- }
1481
-
1482
- /**
1483
- * Handle blur events
1484
- */
1485
- private _handleBlur() {
1486
- // Implementation pending
1487
- }
1488
-
1489
1179
  /**
1490
1180
  * ========================================================================
1491
1181
  * PUBLIC API
@@ -1571,7 +1261,7 @@ export class KTSelect extends KTComponent {
1571
1261
  });
1572
1262
 
1573
1263
  // If search input exists, clear it
1574
- if (this._searchInputElement && this._config.mode !== SelectMode.COMBOBOX) {
1264
+ if (this._searchInputElement) {
1575
1265
  this._searchInputElement.value = '';
1576
1266
  // If we have a search module, clear any search filtering
1577
1267
  if (this._searchModule) {
@@ -1598,26 +1288,26 @@ export class KTSelect extends KTComponent {
1598
1288
  * Toggle the selection of an option
1599
1289
  */
1600
1290
  public toggleSelection(value: string): void {
1291
+ // Prevent selection if the option is disabled (in dropdown or original select)
1292
+ if (this._isOptionDisabled(value)) {
1293
+ if (this._config.debug) console.log('toggleSelection: Option is disabled, ignoring selection');
1294
+ return;
1295
+ }
1296
+
1601
1297
  // Get current selection state
1602
1298
  const isSelected = this._state.isSelected(value);
1603
1299
  if (this._config.debug)
1604
- console.log(
1605
- `toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}, closeOnSelect: ${this._config.closeOnSelect}`,
1606
- );
1300
+ console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}, closeOnSelect: ${this._config.closeOnSelect}`);
1607
1301
 
1608
1302
  // If already selected in single select mode, do nothing (can't deselect in single select)
1609
1303
  if (isSelected && !this._config.multiple) {
1610
1304
  if (this._config.debug)
1611
- console.log(
1612
- 'Early return from toggleSelection - already selected in single select mode',
1613
- );
1305
+ console.log('Early return from toggleSelection - already selected in single select mode');
1614
1306
  return;
1615
1307
  }
1616
1308
 
1617
1309
  if (this._config.debug)
1618
- console.log(
1619
- `Toggling selection for option: ${value}, currently selected: ${isSelected}`,
1620
- );
1310
+ console.log(`Toggling selection for option: ${value}, currently selected: ${isSelected}`);
1621
1311
 
1622
1312
  // Ensure any search highlights are cleared when selection changes
1623
1313
  if (this._searchModule) {
@@ -1654,15 +1344,11 @@ export class KTSelect extends KTComponent {
1654
1344
  // For multiple select mode, only close if closeOnSelect is true
1655
1345
  if (!this._config.multiple) {
1656
1346
  if (this._config.debug)
1657
- console.log(
1658
- 'About to call closeDropdown() for single select mode - always close after selection',
1659
- );
1347
+ console.log('About to call closeDropdown() for single select mode - always close after selection');
1660
1348
  this.closeDropdown();
1661
1349
  } else if (this._config.closeOnSelect) {
1662
1350
  if (this._config.debug)
1663
- console.log(
1664
- 'About to call closeDropdown() for multiple select with closeOnSelect:true',
1665
- );
1351
+ console.log('About to call closeDropdown() for multiple select with closeOnSelect:true');
1666
1352
  this.closeDropdown();
1667
1353
  }
1668
1354
 
@@ -1806,7 +1492,7 @@ export class KTSelect extends KTComponent {
1806
1492
  console.error('Error fetching search results:', error);
1807
1493
  this._renderSearchErrorState(
1808
1494
  this._remoteModule.getErrorMessage() ||
1809
- 'Failed to load search results',
1495
+ 'Failed to load search results',
1810
1496
  );
1811
1497
  });
1812
1498
  }, this._config.searchDebounce || 300);
@@ -1821,7 +1507,7 @@ export class KTSelect extends KTComponent {
1821
1507
  private _renderSearchLoadingState() {
1822
1508
  if (!this._originalOptionsHtml && this._dropdownContentElement) {
1823
1509
  const optionsContainer = this._dropdownContentElement.querySelector(
1824
- '[data-kt-select-options-container]',
1510
+ '[data-kt-select-options]',
1825
1511
  );
1826
1512
  if (optionsContainer) {
1827
1513
  this._originalOptionsHtml = optionsContainer.innerHTML;
@@ -1849,7 +1535,7 @@ export class KTSelect extends KTComponent {
1849
1535
  if (!this._dropdownContentElement) return;
1850
1536
 
1851
1537
  const optionsContainer = this._dropdownContentElement.querySelector(
1852
- '[data-kt-select-options-container]',
1538
+ '[data-kt-select-options]',
1853
1539
  );
1854
1540
  if (!optionsContainer) return;
1855
1541
 
@@ -1858,7 +1544,7 @@ export class KTSelect extends KTComponent {
1858
1544
 
1859
1545
  if (items.length === 0) {
1860
1546
  // Show no results message using template for consistency and customization
1861
- const noResultsElement = defaultTemplates.noResults(this._config);
1547
+ const noResultsElement = defaultTemplates.empty(this._config);
1862
1548
  optionsContainer.appendChild(noResultsElement);
1863
1549
  return;
1864
1550
  }
@@ -1866,27 +1552,11 @@ export class KTSelect extends KTComponent {
1866
1552
  // Process each item individually to create options
1867
1553
  items.forEach((item) => {
1868
1554
  // Create option for the original select
1869
- const selectOption = defaultTemplates.emptyOption({
1870
- ...this._config,
1871
- placeholder: item.title,
1872
- });
1555
+ const selectOption = document.createElement('option');
1873
1556
  selectOption.value = item.id;
1874
- if (item.description) {
1875
- selectOption.setAttribute(
1876
- 'data-kt-select-option-description',
1877
- item.description,
1878
- );
1879
- }
1880
- if (item.icon) {
1881
- selectOption.setAttribute('data-kt-select-option-icon', item.icon);
1882
- }
1883
-
1884
- // Create option element for the dropdown
1885
- const ktOption = new KTSelectOption(selectOption, this._config);
1886
- const renderedOption = ktOption.render();
1887
1557
 
1888
1558
  // Add to dropdown container
1889
- optionsContainer.appendChild(renderedOption);
1559
+ optionsContainer.appendChild(selectOption);
1890
1560
  });
1891
1561
 
1892
1562
  // Add pagination "Load More" button if needed
@@ -1901,16 +1571,140 @@ export class KTSelect extends KTComponent {
1901
1571
  }
1902
1572
 
1903
1573
  /**
1904
- * Filter options by query
1574
+ * Check if dropdown is open
1575
+ */
1576
+ public isDropdownOpen(): boolean {
1577
+ return this._dropdownIsOpen;
1578
+ }
1579
+
1580
+ public getSelectedOptionsText(): string {
1581
+ const selectedValues = this.getSelectedOptions();
1582
+ const displaySeparator = this._config.displaySeparator || ', ';
1583
+ const texts = selectedValues.map(value => {
1584
+ const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1585
+ return option?.getAttribute('data-text') || '';
1586
+ }).filter(Boolean);
1587
+ return texts.join(displaySeparator);
1588
+ }
1589
+
1590
+ /**
1591
+ * Check if an option is disabled (either in dropdown or original select)
1905
1592
  */
1906
- public filterOptions(query: string): void {
1907
- this._filterOptionsForCombobox(query);
1593
+ private _isOptionDisabled(value: string): boolean {
1594
+ const dropdownOption = Array.from(this._options).find(
1595
+ (opt) => opt.getAttribute('data-value') === value
1596
+ );
1597
+ const isDropdownDisabled = dropdownOption && (dropdownOption.classList.contains('disabled') || dropdownOption.getAttribute('aria-disabled') === 'true');
1598
+ const selectOption = Array.from(this._element.querySelectorAll('option')).find(
1599
+ (opt) => opt.value === value
1600
+ ) as HTMLOptionElement;
1601
+ const isNativeDisabled = selectOption && selectOption.disabled;
1602
+ return Boolean(isDropdownDisabled || isNativeDisabled);
1908
1603
  }
1909
1604
 
1910
1605
  /**
1911
- * Check if dropdown is open
1606
+ * Centralized keyboard event handler for all select modes
1912
1607
  */
1913
- public isDropdownOpen(): boolean {
1914
- return this._dropdownIsOpen;
1608
+ private _handleKeyboardEvent(event: KeyboardEvent) {
1609
+ const isOpen = this._dropdownIsOpen;
1610
+ const config = this._config;
1611
+ const focusManager = this._focusManager;
1612
+ const buffer = this._typeToSearchBuffer;
1613
+
1614
+ // Ignore modifier keys
1615
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
1616
+
1617
+ // Type-to-search: only for single char keys
1618
+ if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/)) {
1619
+ buffer.push(event.key);
1620
+ const str = buffer.getBuffer();
1621
+ focusManager.focusByString(str);
1622
+ return;
1623
+ }
1624
+
1625
+ switch (event.key) {
1626
+ case 'ArrowDown':
1627
+ event.preventDefault();
1628
+ if (!isOpen) {
1629
+ this.openDropdown();
1630
+ } else {
1631
+ focusManager.focusNext();
1632
+ }
1633
+ break;
1634
+ case 'ArrowUp':
1635
+ event.preventDefault();
1636
+ if (!isOpen) {
1637
+ this.openDropdown();
1638
+ } else {
1639
+ focusManager.focusPrevious();
1640
+ }
1641
+ break;
1642
+ case 'Home':
1643
+ event.preventDefault();
1644
+ if (isOpen) focusManager.focusFirst();
1645
+ break;
1646
+ case 'End':
1647
+ event.preventDefault();
1648
+ if (isOpen) focusManager.focusLast();
1649
+ break;
1650
+ case 'Enter':
1651
+ case ' ': // Space
1652
+ 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
+ }
1661
+ }
1662
+ }
1663
+ // Prevent form submit
1664
+ event.preventDefault();
1665
+ } else {
1666
+ this.openDropdown();
1667
+ }
1668
+ break;
1669
+ case 'Escape':
1670
+ if (isOpen) {
1671
+ this.closeDropdown();
1672
+ (event.target as HTMLElement).blur();
1673
+ }
1674
+ break;
1675
+ case 'Tab':
1676
+ // Let Tab propagate for normal focus movement
1677
+ break;
1678
+ default:
1679
+ break;
1680
+ }
1681
+ }
1682
+
1683
+ public renderDisplayTemplateForSelected(selectedValues: string[]): string {
1684
+ const optionsConfig = this._config.optionsConfig as any || {};
1685
+ const displaySeparator = this._config.displaySeparator || ', ';
1686
+ const contentArray = Array.from(new Set(
1687
+ selectedValues.map(value => {
1688
+ const option = Array.from(this._options).find(opt => opt.getAttribute('data-value') === value);
1689
+ if (!option) return '';
1690
+
1691
+ let displayTemplate = this._config.displayTemplate;
1692
+ const text = option.getAttribute('data-text') || '';
1693
+
1694
+ // Replace all {{varname}} in option.innerHTML with values from _config
1695
+ Object.entries(optionsConfig[value] || {}).forEach(([key, val]) => {
1696
+ if (["string", "number", "boolean"].includes(typeof val)) {
1697
+ displayTemplate = displayTemplate.replace(new RegExp(`{{${key}}}`, 'g'), String(val));
1698
+ }
1699
+ });
1700
+
1701
+ return renderTemplateString(displayTemplate, {
1702
+ selectedCount: selectedValues.length || 0,
1703
+ selectedTexts: this.getSelectedOptionsText() || '',
1704
+ text,
1705
+ });
1706
+ }).filter(Boolean)
1707
+ ));
1708
+ return contentArray.join(displaySeparator);
1915
1709
  }
1916
1710
  }