@keenthemes/ktui 1.0.19 → 1.0.20

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 (42) hide show
  1. package/dist/ktui.js +277 -27
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +27 -1
  5. package/lib/cjs/components/component.js +57 -5
  6. package/lib/cjs/components/component.js.map +1 -1
  7. package/lib/cjs/components/datatable/datatable.js +71 -10
  8. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  9. package/lib/cjs/components/select/config.js +1 -0
  10. package/lib/cjs/components/select/config.js.map +1 -1
  11. package/lib/cjs/components/select/search.js +8 -0
  12. package/lib/cjs/components/select/search.js.map +1 -1
  13. package/lib/cjs/components/select/select.js +75 -0
  14. package/lib/cjs/components/select/select.js.map +1 -1
  15. package/lib/cjs/components/select/templates.js +6 -0
  16. package/lib/cjs/components/select/templates.js.map +1 -1
  17. package/lib/cjs/components/stepper/stepper.js +59 -12
  18. package/lib/cjs/components/stepper/stepper.js.map +1 -1
  19. package/lib/esm/components/component.js +57 -5
  20. package/lib/esm/components/component.js.map +1 -1
  21. package/lib/esm/components/datatable/datatable.js +70 -10
  22. package/lib/esm/components/datatable/datatable.js.map +1 -1
  23. package/lib/esm/components/select/config.js +1 -0
  24. package/lib/esm/components/select/config.js.map +1 -1
  25. package/lib/esm/components/select/search.js +8 -0
  26. package/lib/esm/components/select/search.js.map +1 -1
  27. package/lib/esm/components/select/select.js +75 -0
  28. package/lib/esm/components/select/select.js.map +1 -1
  29. package/lib/esm/components/select/templates.js +6 -0
  30. package/lib/esm/components/select/templates.js.map +1 -1
  31. package/lib/esm/components/stepper/stepper.js +59 -12
  32. package/lib/esm/components/stepper/stepper.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/components/component.ts +18 -4
  35. package/src/components/datatable/datatable.ts +86 -12
  36. package/src/components/datatable/types.ts +5 -1
  37. package/src/components/select/config.ts +2 -0
  38. package/src/components/select/search.ts +9 -0
  39. package/src/components/select/select.css +14 -6
  40. package/src/components/select/select.ts +102 -0
  41. package/src/components/select/templates.ts +13 -0
  42. package/src/components/stepper/stepper.ts +2 -2
@@ -143,6 +143,7 @@ export class KTSelectSearch {
143
143
  this._searchInput?.focus(); // Focus search input
144
144
  }, 50); // Delay to ensure dropdown is visible
145
145
  }
146
+ this._select.updateSelectAllButtonState();
146
147
  });
147
148
  }
148
149
  }
@@ -164,6 +165,11 @@ export class KTSelectSearch {
164
165
  const key = event.key;
165
166
 
166
167
  switch (key) {
168
+ case ' ': // Spacebar
169
+ // Do nothing, allow space to be typed into the input
170
+ // Stop propagation to prevent parent handlers from processing this event
171
+ event.stopPropagation();
172
+ break;
167
173
  case 'ArrowDown':
168
174
  event.preventDefault();
169
175
  this._focusManager.focusNext();
@@ -292,6 +298,8 @@ export class KTSelectSearch {
292
298
  const visibleCount = filterOptions(options, query, config, dropdownElement, (count) =>
293
299
  this._handleNoResults(count),
294
300
  );
301
+
302
+ this._select.updateSelectAllButtonState();
295
303
  }
296
304
 
297
305
  /**
@@ -324,6 +332,7 @@ export class KTSelectSearch {
324
332
  });
325
333
 
326
334
  this._clearNoResultsMessage(); // Ensure no results message is cleared when resetting
335
+ this._select.updateSelectAllButtonState();
327
336
  }
328
337
 
329
338
  private _handleNoResults(visibleOptionsCount: number) {
@@ -29,7 +29,7 @@
29
29
  &:-moz-focusring {
30
30
  color: transparent;
31
31
  text-shadow: none;
32
- }
32
+ }
33
33
 
34
34
  &.active {
35
35
  @apply ring-ring/30 border-ring outline-none ring-[3px];
@@ -54,7 +54,7 @@
54
54
 
55
55
  .kt-select-search {
56
56
  @apply px-4 py-1 border-b border-border;
57
-
57
+
58
58
  .kt-input {
59
59
  @apply text-sm;
60
60
  }
@@ -68,6 +68,14 @@
68
68
  @apply rounded-md shadow-md shadow-[rgba(0,0,0,0.05)] border border-border bg-popover text-popover-foreground;
69
69
  }
70
70
 
71
+ .kt-select-select-all {
72
+ @apply px-2 py-1 border-b border-border;
73
+ }
74
+
75
+ .kt-select-select-all-button {
76
+ @apply w-full text-start p-1.5 rounded-md text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer;
77
+ }
78
+
71
79
  .kt-select-options {
72
80
  @apply p-1 space-y-0.5;
73
81
  }
@@ -98,7 +106,7 @@
98
106
  }
99
107
  }
100
108
 
101
- .kt-select-option-text {
109
+ .kt-select-option-text {
102
110
  @apply overflow-ellipsis truncate;
103
111
  }
104
112
 
@@ -153,7 +161,7 @@
153
161
  }
154
162
 
155
163
  /* RTL */
156
- @layer components {
164
+ @layer components {
157
165
  [dir='rtl'] {
158
166
  .kt-select {
159
167
  background-position: left 0.5rem center;
@@ -162,7 +170,7 @@
162
170
  background-position: left 0.5rem top 0.675rem;
163
171
  }
164
172
  }
165
-
173
+
166
174
  .kt-select-sm {
167
175
  background-position: left 0.5rem center;
168
176
 
@@ -171,7 +179,7 @@
171
179
  }
172
180
  }
173
181
 
174
- .kt-select-lg {
182
+ .kt-select-lg {
175
183
  background-position: left 0.75rem center;
176
184
 
177
185
  &[data-multiple=true] {
@@ -48,6 +48,8 @@ export class KTSelect extends KTComponent {
48
48
  private _tagsModule: KTSelectTags | null = null;
49
49
  private _dropdownModule: KTSelectDropdown | null = null;
50
50
  private _loadMoreIndicator: HTMLElement | null = null;
51
+ private _selectAllButton: HTMLElement | null = null;
52
+ private _selectAllButtonToggle: HTMLButtonElement | null = null;
51
53
  private _focusManager: FocusManager;
52
54
  private _eventManager: EventManager;
53
55
  private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
@@ -405,6 +407,12 @@ export class KTSelect extends KTComponent {
405
407
  this.updateSelectedOptionDisplay();
406
408
  this._setAriaAttributes();
407
409
 
410
+ // Update select all button state
411
+ this.updateSelectAllButtonState();
412
+
413
+ // Focus the first selected option or first option if nothing selected
414
+ this._focusSelectedOption();
415
+
408
416
  // Attach event listeners after all modules are initialized
409
417
  this._attachEventListeners();
410
418
 
@@ -460,6 +468,12 @@ export class KTSelect extends KTComponent {
460
468
  dropdownElement.appendChild(searchElement);
461
469
  }
462
470
 
471
+ // Add select all button if needed
472
+ if (this._config.multiple && this._config.enableSelectAll) {
473
+ const selectAllElement = defaultTemplates.selectAll(this._config);
474
+ dropdownElement.appendChild(selectAllElement);
475
+ }
476
+
463
477
  // Create options container using template
464
478
  const optionsContainer = defaultTemplates.options(this._config);
465
479
 
@@ -524,6 +538,10 @@ export class KTSelect extends KTComponent {
524
538
  this._searchInputElement = this._displayElement as HTMLInputElement;
525
539
  }
526
540
 
541
+ this._selectAllButton = this._wrapperElement.querySelector(
542
+ '[data-kt-select-select-all]',
543
+ ) as HTMLElement;
544
+
527
545
  this._options = this._wrapperElement.querySelectorAll(
528
546
  `[data-kt-select-option]`,
529
547
  ) as NodeListOf<HTMLElement>;
@@ -543,6 +561,17 @@ export class KTSelect extends KTComponent {
543
561
  this._handleDropdownOptionClick.bind(this),
544
562
  );
545
563
 
564
+ if (this._selectAllButton) {
565
+ this._selectAllButtonToggle = this._selectAllButton.querySelector('button');
566
+ if (this._selectAllButtonToggle) {
567
+ this._eventManager.addListener(
568
+ this._selectAllButtonToggle,
569
+ 'click',
570
+ this._handleSelectAllClick.bind(this),
571
+ );
572
+ }
573
+ }
574
+
546
575
  // Attach centralized keyboard handler to the wrapper element.
547
576
  // Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
548
577
  if (this._wrapperElement) {
@@ -752,6 +781,9 @@ export class KTSelect extends KTComponent {
752
781
  // Update ARIA states
753
782
  this._setAriaAttributes();
754
783
 
784
+ // Update select all button state
785
+ this.updateSelectAllButtonState();
786
+
755
787
  // Focus the first selected option or first option if nothing selected
756
788
  this._focusSelectedOption();
757
789
  }
@@ -1013,6 +1045,9 @@ export class KTSelect extends KTComponent {
1013
1045
  this.updateSelectedOptionDisplay();
1014
1046
  this._updateSelectedOptionClass();
1015
1047
 
1048
+ // Update select all button state
1049
+ this.updateSelectAllButtonState();
1050
+
1016
1051
  // Dispatch change event
1017
1052
  this._dispatchEvent('change');
1018
1053
  this._fireEvent('change');
@@ -1328,6 +1363,7 @@ export class KTSelect extends KTComponent {
1328
1363
  if (this._config.debug)
1329
1364
  console.log('Multiple select mode - keeping dropdown open for additional selections');
1330
1365
  // Don't close dropdown in multiple select mode to allow multiple selections
1366
+ this.updateSelectAllButtonState();
1331
1367
  }
1332
1368
 
1333
1369
  // Dispatch custom change event with additional data
@@ -1458,6 +1494,7 @@ export class KTSelect extends KTComponent {
1458
1494
  if (this._searchModule) {
1459
1495
  this._searchModule.refreshAfterSearch();
1460
1496
  }
1497
+ this.updateSelectAllButtonState();
1461
1498
  })
1462
1499
  .catch((error) => {
1463
1500
  console.error('Error updating search results:', error);
@@ -1799,5 +1836,70 @@ export class KTSelect extends KTComponent {
1799
1836
  this._state.setSelectedOptions(this._config.multiple ? selected : selected[0] || '');
1800
1837
  this.updateSelectedOptionDisplay();
1801
1838
  this._updateSelectedOptionClass();
1839
+ this.updateSelectAllButtonState();
1840
+ }
1841
+
1842
+ private _handleSelectAllClick(event: Event): void {
1843
+ event.preventDefault();
1844
+ event.stopPropagation();
1845
+
1846
+ const visibleOptions = this._focusManager
1847
+ .getVisibleOptions()
1848
+ .filter((opt) => opt.getAttribute('aria-disabled') !== 'true');
1849
+ if (visibleOptions.length === 0) return;
1850
+
1851
+ const visibleValues = visibleOptions.map(
1852
+ (opt) => opt.dataset.value as string,
1853
+ );
1854
+ const selectedValues = new Set(this.getSelectedOptions());
1855
+ const isAllSelected = visibleOptions.every((opt) =>
1856
+ selectedValues.has(opt.dataset.value as string),
1857
+ );
1858
+
1859
+ if (isAllSelected) {
1860
+ // Deselect all visible
1861
+ visibleValues.forEach((value) => selectedValues.delete(value));
1862
+ } else {
1863
+ // Select all visible
1864
+ visibleValues.forEach((value) => selectedValues.add(value));
1865
+ }
1866
+
1867
+ this._state.setSelectedOptions(Array.from(selectedValues));
1868
+ this.updateSelectedOptionDisplay();
1869
+ this._updateSelectedOptionClass();
1870
+ this.updateSelectAllButtonState();
1871
+
1872
+ this._dispatchEvent('change');
1873
+ this._fireEvent('change');
1874
+ }
1875
+
1876
+ public updateSelectAllButtonState(): void {
1877
+ if (
1878
+ !this._config.multiple ||
1879
+ !this._config.enableSelectAll ||
1880
+ !this._selectAllButtonToggle
1881
+ ) {
1882
+ return;
1883
+ }
1884
+
1885
+ const visibleOptions = this._focusManager
1886
+ .getVisibleOptions()
1887
+ .filter((opt) => opt.getAttribute('aria-disabled') !== 'true');
1888
+
1889
+ if (visibleOptions.length === 0) {
1890
+ this._selectAllButton.style.display = 'none';
1891
+ return;
1892
+ }
1893
+
1894
+ this._selectAllButton.style.display = '';
1895
+
1896
+ const selectedValues = new Set(this.getSelectedOptions());
1897
+ const isAllSelected = visibleOptions.every((opt) =>
1898
+ selectedValues.has(opt.dataset.value as string),
1899
+ );
1900
+
1901
+ this._selectAllButtonToggle.textContent = isAllSelected
1902
+ ? this._config.clearAllText
1903
+ : this._config.selectAllText;
1802
1904
  }
1803
1905
  }
@@ -43,6 +43,7 @@ export const coreTemplateStrings = {
43
43
  loading: `<li class="kt-select-loading {{class}}" role="status" aria-live="polite"></li>`,
44
44
  tag: `<div data-kt-select-tag="true" class="kt-select-tag {{class}}"></div>`,
45
45
  loadMore: `<li class="kt-select-load-more {{class}}" data-kt-select-load-more="true"></li>`,
46
+ selectAll: `<div data-kt-select-select-all class="kt-select-select-all"><button type="button" class="kt-select-select-all-button">{{text}}</button></div>`,
46
47
  tagRemoveButton: `<button type="button" data-kt-select-remove-button class="kt-select-tag-remove" aria-label="Remove tag" tabindex="0"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="3" x2="9" y2="9"/><line x1="9" y1="3" x2="3" y2="9"/></svg></button>`,
47
48
  };
48
49
 
@@ -99,6 +100,7 @@ export interface KTSelectTemplateInterface {
99
100
  ) => HTMLElement;
100
101
 
101
102
  placeholder: (config: KTSelectConfigInterface) => HTMLElement;
103
+ selectAll: (config: KTSelectConfigInterface) => HTMLElement;
102
104
  }
103
105
 
104
106
  /**
@@ -462,4 +464,15 @@ export const defaultTemplates: KTSelectTemplateInterface = {
462
464
  return element;
463
465
  }
464
466
  },
467
+
468
+ selectAll: (config: KTSelectConfigInterface): HTMLElement => {
469
+ const template = getTemplateStrings(config).selectAll;
470
+ const element = stringToElement(
471
+ template.replace(
472
+ '{{text}}',
473
+ config.selectAllText || 'Select All',
474
+ ),
475
+ );
476
+ return element;
477
+ },
465
478
  };
@@ -131,12 +131,12 @@ export class KTStepper extends KTComponent implements KTStepperInterface {
131
131
  return elements;
132
132
  }
133
133
 
134
- protected _go(step: number): void {
134
+ protected async _go(step: number): Promise<void> {
135
135
  if (step === this._activeStep || step > this._getTotalSteps() || step < 0)
136
136
  return;
137
137
 
138
138
  const payload = { step: step, cancel: false };
139
- this._fireEvent('change', payload);
139
+ await this._fireEvent('change', payload);
140
140
  this._dispatchEvent('change', payload);
141
141
  if (payload.cancel === true) {
142
142
  return;