@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
@@ -26,9 +26,15 @@ export class KTSelectCombobox {
26
26
 
27
27
  const displayElement = select.getDisplayElement(); // KTSelect's main display element for combobox
28
28
 
29
- this._searchInputElement = displayElement.querySelector('input[data-kt-select-search]');
30
- this._clearButtonElement = displayElement.querySelector('[data-kt-select-clear-button]');
31
- this._valuesContainerElement = displayElement.querySelector('[data-kt-select-combobox-values]');
29
+ this._searchInputElement = displayElement.querySelector(
30
+ 'input[data-kt-select-search]',
31
+ );
32
+ this._clearButtonElement = displayElement.querySelector(
33
+ '[data-kt-select-clear-button]',
34
+ );
35
+ this._valuesContainerElement = displayElement.querySelector(
36
+ '[data-kt-select-combobox-values]',
37
+ );
32
38
 
33
39
  this._boundInputHandler = this._handleComboboxInput.bind(this);
34
40
  this._boundClearHandler = this._handleClearButtonClick.bind(this);
@@ -42,7 +48,7 @@ export class KTSelectCombobox {
42
48
  this.updateDisplay(this._select.getSelectedOptions());
43
49
  } else {
44
50
  // For tags or displayTemplate, the input should be clear for typing.
45
- this._searchInputElement.value = '';
51
+ this._searchInputElement.value = '';
46
52
  }
47
53
  this._toggleClearButtonVisibility(this._searchInputElement.value);
48
54
  // this._select.showAllOptions(); // showAllOptions might be too broad, filtering is managed by typing.
@@ -56,11 +62,18 @@ export class KTSelectCombobox {
56
62
  */
57
63
  private _attachEventListeners(): void {
58
64
  this._removeEventListeners();
59
- if (this._searchInputElement) { // Ensure element exists
60
- this._searchInputElement.addEventListener('input', this._boundInputHandler);
65
+ if (this._searchInputElement) {
66
+ // Ensure element exists
67
+ this._searchInputElement.addEventListener(
68
+ 'input',
69
+ this._boundInputHandler,
70
+ );
61
71
  }
62
72
  if (this._clearButtonElement) {
63
- this._clearButtonElement.addEventListener('click', this._boundClearHandler);
73
+ this._clearButtonElement.addEventListener(
74
+ 'click',
75
+ this._boundClearHandler,
76
+ );
64
77
  }
65
78
  }
66
79
 
@@ -69,10 +82,16 @@ export class KTSelectCombobox {
69
82
  */
70
83
  private _removeEventListeners(): void {
71
84
  if (this._searchInputElement) {
72
- this._searchInputElement.removeEventListener('input', this._boundInputHandler);
85
+ this._searchInputElement.removeEventListener(
86
+ 'input',
87
+ this._boundInputHandler,
88
+ );
73
89
  }
74
90
  if (this._clearButtonElement) {
75
- this._clearButtonElement.removeEventListener('click', this._boundClearHandler);
91
+ this._clearButtonElement.removeEventListener(
92
+ 'click',
93
+ this._boundClearHandler,
94
+ );
76
95
  }
77
96
  }
78
97
 
@@ -85,7 +104,8 @@ export class KTSelectCombobox {
85
104
 
86
105
  this._toggleClearButtonVisibility(query);
87
106
 
88
- if (!(this._select as any).isDropdownOpen()) { // Use public getter
107
+ if (!(this._select as any).isDropdownOpen()) {
108
+ // Use public getter
89
109
  this._select.openDropdown();
90
110
  }
91
111
  // For single select without displayTemplate, if user types, they are essentially clearing the previous selection text
@@ -127,7 +147,11 @@ export class KTSelectCombobox {
127
147
  if (!this._clearButtonElement) return;
128
148
  const hasSelectedItems = this._select.getSelectedOptions().length > 0;
129
149
 
130
- if (inputValue.length > 0 || (hasSelectedItems && (this._config.multiple || this._config.displayTemplate))) {
150
+ if (
151
+ inputValue.length > 0 ||
152
+ (hasSelectedItems &&
153
+ (this._config.multiple || this._config.displayTemplate))
154
+ ) {
131
155
  this._clearButtonElement.classList.remove('hidden');
132
156
  } else {
133
157
  this._clearButtonElement.classList.add('hidden');
@@ -138,7 +162,9 @@ export class KTSelectCombobox {
138
162
  * Filter options for combobox based on input query
139
163
  */
140
164
  private _filterOptionsForCombobox(query: string): void {
141
- const options = Array.from(this._select.getOptionsElement()) as HTMLElement[];
165
+ const options = Array.from(
166
+ this._select.getOptionsElement(),
167
+ ) as HTMLElement[];
142
168
  const config = this._select.getConfig();
143
169
  const dropdownElement = this._select.getDropdownElement();
144
170
  filterOptions(options, query, config, dropdownElement);
@@ -160,41 +186,65 @@ export class KTSelectCombobox {
160
186
  this._valuesContainerElement.innerHTML = '';
161
187
  }
162
188
 
163
- if (this._config.tags && this._valuesContainerElement) { // Combobox + Tags
164
- selectedOptions.forEach(value => {
189
+ if (this._config.tags && this._valuesContainerElement) {
190
+ // Combobox + Tags
191
+ selectedOptions.forEach((value) => {
165
192
  // Ensure value is properly escaped for querySelector
166
- const optionElement = this._select.getElement().querySelector(`option[value="${CSS.escape(value)}"]`) as HTMLOptionElement;
193
+ const optionElement = this._select
194
+ .getElement()
195
+ .querySelector(
196
+ `option[value="${CSS.escape(value)}"]`,
197
+ ) as HTMLOptionElement;
167
198
  if (optionElement) {
168
199
  const tagElement = defaultTemplates.tag(optionElement, this._config);
169
200
  this._valuesContainerElement.appendChild(tagElement);
170
201
  }
171
202
  });
172
203
  this._searchInputElement.value = ''; // Input field is for typing new searches
173
- this._searchInputElement.placeholder = selectedOptions.length > 0 ? '' : (this._config.placeholder || 'Select...');
174
-
175
- } else if (this._config.displayTemplate && this._valuesContainerElement) { // Combobox + DisplayTemplate (no tags)
176
- this._valuesContainerElement.innerHTML = this._select.renderDisplayTemplateForSelected(selectedOptions);
204
+ this._searchInputElement.placeholder =
205
+ selectedOptions.length > 0
206
+ ? ''
207
+ : this._config.placeholder || 'Select...';
208
+ } else if (this._config.displayTemplate && this._valuesContainerElement) {
209
+ // Combobox + DisplayTemplate (no tags)
210
+ this._valuesContainerElement.innerHTML =
211
+ this._select.renderDisplayTemplateForSelected(selectedOptions);
177
212
  this._searchInputElement.value = ''; // Input field is for typing new searches
178
- this._searchInputElement.placeholder = selectedOptions.length > 0 ? '' : (this._config.placeholder || 'Select...');
179
-
180
- } else if (this._config.multiple && this._valuesContainerElement) { // Combobox + Multiple (no tags, no display template)
213
+ this._searchInputElement.placeholder =
214
+ selectedOptions.length > 0
215
+ ? ''
216
+ : this._config.placeholder || 'Select...';
217
+ } else if (this._config.multiple && this._valuesContainerElement) {
218
+ // Combobox + Multiple (no tags, no display template)
181
219
  // For simplicity, join text. A proper tag implementation would be more complex here.
182
- this._valuesContainerElement.innerHTML = selectedOptions.map(value => {
183
- const optionEl = this._select.getElement().querySelector(`option[value="${CSS.escape(value)}"]`);
184
- return optionEl ? optionEl.textContent : '';
185
- }).join(', '); // Basic comma separation
220
+ this._valuesContainerElement.innerHTML = selectedOptions
221
+ .map((value) => {
222
+ const optionEl = this._select
223
+ .getElement()
224
+ .querySelector(`option[value="${CSS.escape(value)}"]`);
225
+ return optionEl ? optionEl.textContent : '';
226
+ })
227
+ .join(', '); // Basic comma separation
186
228
  this._searchInputElement.value = '';
187
- this._searchInputElement.placeholder = selectedOptions.length > 0 ? '' : (this._config.placeholder || 'Select...');
188
-
189
- } else if (!this._config.multiple && selectedOptions.length > 0) { // Single select combobox: display selected option's text in the input
229
+ this._searchInputElement.placeholder =
230
+ selectedOptions.length > 0
231
+ ? ''
232
+ : this._config.placeholder || 'Select...';
233
+ } else if (!this._config.multiple && selectedOptions.length > 0) {
234
+ // Single select combobox: display selected option's text in the input
190
235
  const selectedValue = selectedOptions[0];
191
- const optionElement = this._select.getElement().querySelector(`option[value="${CSS.escape(selectedValue)}"]`);
192
- this._searchInputElement.value = optionElement ? optionElement.textContent || '' : '';
236
+ const optionElement = this._select
237
+ .getElement()
238
+ .querySelector(`option[value="${CSS.escape(selectedValue)}"]`);
239
+ this._searchInputElement.value = optionElement
240
+ ? optionElement.textContent || ''
241
+ : '';
193
242
  // placeholder is implicitly handled by input value for single select
194
-
195
- } else { // No selection or not fitting above categories (e.g. single select, no items)
243
+ } else {
244
+ // No selection or not fitting above categories (e.g. single select, no items)
196
245
  this._searchInputElement.value = '';
197
- this._searchInputElement.placeholder = this._config.placeholder || 'Select...';
246
+ this._searchInputElement.placeholder =
247
+ this._config.placeholder || 'Select...';
198
248
  // _valuesContainerElement is already cleared if it exists
199
249
  }
200
250
  this._toggleClearButtonVisibility(this._searchInputElement.value);
@@ -57,6 +57,7 @@ export const DefaultConfig: KTSelectConfigInterface = {
57
57
  // Multi-Select Display
58
58
  selectAllText: 'Select all', // Text for the "Select All" option (if implemented)
59
59
  clearAllText: 'Clear all', // Text for the "Clear All" option (if implemented)
60
+ enableSelectAll: false, // Enable/disable "Select All" button for multi-select
60
61
  showSelectedCount: true, // Show the number of selected options in multi-select mode
61
62
  renderSelected: null, // Custom function to render the selected value(s) in the display area
62
63
 
@@ -105,6 +106,7 @@ export interface KTSelectConfigInterface {
105
106
  // Multi-Select Display
106
107
  selectAllText?: string;
107
108
  clearAllText?: string;
109
+ enableSelectAll?: boolean;
108
110
  showSelectedCount?: boolean;
109
111
  renderSelected?: (selectedOptions: any[]) => string; // Assuming any[] for now, adjust based on your option data structure
110
112
 
@@ -106,9 +106,7 @@ export class KTSelectDropdown extends KTComponent {
106
106
 
107
107
  if (this._config.disabled) {
108
108
  if (this._config.debug)
109
- console.log(
110
- 'KTSelectDropdown._handleToggleClick: select is disabled',
111
- );
109
+ console.log('KTSelectDropdown._handleToggleClick: select is disabled');
112
110
  return;
113
111
  }
114
112
 
@@ -251,9 +249,7 @@ export class KTSelectDropdown extends KTComponent {
251
249
  public open(): void {
252
250
  if (this._config.disabled) {
253
251
  if (this._config.debug)
254
- console.log(
255
- 'KTSelectDropdown.open: select is disabled, not opening',
256
- );
252
+ console.log('KTSelectDropdown.open: select is disabled, not opening');
257
253
  return;
258
254
  }
259
255
  if (this._isOpen || this._isTransitioning) return;
@@ -279,17 +275,26 @@ export class KTSelectDropdown extends KTComponent {
279
275
  }
280
276
 
281
277
  // Consider the dropdown's current z-index if it's already set and higher
282
- const currentDropdownZIndexStr = KTDom.getCssProp(this._dropdownElement, 'z-index');
278
+ const currentDropdownZIndexStr = KTDom.getCssProp(
279
+ this._dropdownElement,
280
+ 'z-index',
281
+ );
283
282
  if (currentDropdownZIndexStr && currentDropdownZIndexStr !== 'auto') {
284
283
  const currentDropdownZIndex = parseInt(currentDropdownZIndexStr);
285
- if (!isNaN(currentDropdownZIndex) && currentDropdownZIndex > (zIndexToApply || 0)) {
284
+ if (
285
+ !isNaN(currentDropdownZIndex) &&
286
+ currentDropdownZIndex > (zIndexToApply || 0)
287
+ ) {
286
288
  zIndexToApply = currentDropdownZIndex;
287
289
  }
288
290
  }
289
291
 
290
292
  // Ensure dropdown is above elements within its original toggle's parent context
291
293
  const toggleParentContextZindex = KTDom.getHighestZindex(this._element); // _element is the select wrapper
292
- if (toggleParentContextZindex !== null && toggleParentContextZindex >= (zIndexToApply || 0)) {
294
+ if (
295
+ toggleParentContextZindex !== null &&
296
+ toggleParentContextZindex >= (zIndexToApply || 0)
297
+ ) {
293
298
  zIndexToApply = toggleParentContextZindex + 1;
294
299
  }
295
300
 
@@ -379,7 +384,9 @@ export class KTSelectDropdown extends KTComponent {
379
384
 
380
385
  KTDom.transitionEnd(this._dropdownElement, completeTransition);
381
386
 
382
- if (KTDom.getCssProp(this._dropdownElement, 'transition-duration') === '0s') {
387
+ if (
388
+ KTDom.getCssProp(this._dropdownElement, 'transition-duration') === '0s'
389
+ ) {
383
390
  completeTransition();
384
391
  }
385
392
  }
@@ -422,7 +429,9 @@ export class KTSelectDropdown extends KTComponent {
422
429
  private _resolveDropdownContainer(): HTMLElement | null {
423
430
  const containerSelector = this._config.dropdownContainer;
424
431
  if (containerSelector && containerSelector !== 'body') {
425
- const containerElement = document.querySelector(containerSelector) as HTMLElement | null;
432
+ const containerElement = document.querySelector(
433
+ containerSelector,
434
+ ) as HTMLElement | null;
426
435
  if (!containerElement && this._config.debug) {
427
436
  console.warn(
428
437
  `KTSelectDropdown: dropdownContainer selector "${containerSelector}" not found. Dropdown will remain in its default position.`,
@@ -9,5 +9,10 @@ export { KTSelectCombobox } from './combobox';
9
9
  export { KTSelectSearch } from './search';
10
10
  export { KTSelectTags } from './tags';
11
11
  export { KTSelectDropdown } from './dropdown';
12
- export { filterOptions, FocusManager, EventManager, TypeToSearchBuffer } from './utils';
12
+ export {
13
+ filterOptions,
14
+ FocusManager,
15
+ EventManager,
16
+ TypeToSearchBuffer,
17
+ } from './utils';
13
18
  export { KTSelectConfigInterface, KTSelectOption } from './config';
@@ -4,9 +4,7 @@
4
4
  */
5
5
 
6
6
  import KTComponent from '../component';
7
- import {
8
- KTSelectConfigInterface,
9
- } from './config';
7
+ import { KTSelectConfigInterface } from './config';
10
8
  import { defaultTemplates } from './templates';
11
9
 
12
10
  export class KTSelectOption extends KTComponent {
@@ -15,7 +13,7 @@ export class KTSelectOption extends KTComponent {
15
13
  protected override readonly _config: KTSelectConfigInterface; // Holds option-specific data from data-kt-*
16
14
  private _globalConfig: KTSelectConfigInterface; // Main select's config
17
15
 
18
- constructor(element: HTMLElement, config?: KTSelectConfigInterface,) {
16
+ constructor(element: HTMLElement, config?: KTSelectConfigInterface) {
19
17
  super();
20
18
 
21
19
  // Always initialize a new option instance
@@ -30,13 +28,16 @@ export class KTSelectOption extends KTComponent {
30
28
  // Ensure optionsConfig is initialized
31
29
  if (this._globalConfig) {
32
30
  this._globalConfig.optionsConfig = this._globalConfig.optionsConfig || {};
33
- this._globalConfig.optionsConfig[(element as HTMLInputElement).value] = this._config;
31
+ this._globalConfig.optionsConfig[(element as HTMLInputElement).value] =
32
+ this._config;
34
33
  // console.log('[KTSelectOption] Populating _globalConfig.optionsConfig for value', (element as HTMLInputElement).value, 'with:', JSON.parse(JSON.stringify(this._config)));
35
34
  // console.log('[KTSelectOption] _globalConfig.optionsConfig is now:', JSON.parse(JSON.stringify(this._globalConfig.optionsConfig)));
36
35
  } else {
37
36
  // Handle case where _globalConfig might be undefined, though constructor expects it.
38
37
  // This might indicate a need to ensure config is always passed or has a default.
39
- console.warn('KTSelectOption: _globalConfig is undefined during constructor.');
38
+ console.warn(
39
+ 'KTSelectOption: _globalConfig is undefined during constructor.',
40
+ );
40
41
  }
41
42
 
42
43
  // Don't store in KTData to avoid Singleton pattern issues
@@ -261,16 +261,13 @@ export class KTSelectRemote {
261
261
  const labelField = this._config.dataFieldText || 'title';
262
262
 
263
263
  if (this._config.debug)
264
- console.log(
265
- `Mapping fields: value=${valueField}, label=${labelField}`,
266
- );
264
+ console.log(`Mapping fields: value=${valueField}, label=${labelField}`);
267
265
  if (this._config.debug)
268
266
  console.log('Item data:', JSON.stringify(item).substring(0, 200) + '...'); // Trimmed for readability
269
267
 
270
- // Extract values using dot notation if needed
268
+ // Extract values using improved getValue function
271
269
  const getValue = (obj: any, path: string): any => {
272
- if (!path) return null;
273
- if (!obj) return null;
270
+ if (!path || !obj) return null;
274
271
 
275
272
  try {
276
273
  // Handle dot notation to access nested properties
@@ -296,7 +293,7 @@ export class KTSelectRemote {
296
293
  ? typeof result === 'object'
297
294
  ? JSON.stringify(result).substring(0, 50)
298
295
  : String(result).substring(0, 50)
299
- : 'null'
296
+ : 'null'
300
297
  }`,
301
298
  );
302
299
 
@@ -307,64 +304,66 @@ export class KTSelectRemote {
307
304
  }
308
305
  };
309
306
 
310
- // Try to get a usable ID, with fallbacks
307
+ // Get ID and ensure it's a string
311
308
  let id = getValue(item, valueField);
312
-
313
- // Ensure id is always a proper string
314
309
  if (id === null || id === undefined) {
315
- // If no ID found, check for id.value or item.id as fallbacks
316
- if (
317
- item.id &&
318
- typeof item.id === 'object' &&
319
- 'value' in item.id &&
320
- item.id.value
321
- ) {
322
- id = String(item.id.value);
323
- if (this._config.debug)
324
- console.log(`Using id.value as fallback: ${id}`);
325
- } else if (item.id) {
326
- id = String(item.id);
327
- if (this._config.debug)
328
- console.log(`Using direct item.id as fallback: ${id}`);
329
- } else {
330
- // If no ID found at all, use the title instead (will be extracted below)
331
- if (this._config.debug)
332
- console.log(`No ID found, will use title as fallback`);
333
- id = null;
310
+ // Try common fallback fields for ID
311
+ const fallbackFields = ['id', 'value', 'key', 'pk'];
312
+ for (const field of fallbackFields) {
313
+ if (item[field] !== null && item[field] !== undefined) {
314
+ id = String(item[field]);
315
+ if (this._config.debug)
316
+ console.log(`Using fallback field '${field}' for ID: ${id}`);
317
+ break;
318
+ }
334
319
  }
335
- } else if (typeof id === 'object') {
336
- // If ID is an object, log the issue and set to null to use title fallback
337
- console.warn(
338
- `ID for path ${valueField} is an object, will use title fallback instead`,
339
- );
340
- id = null;
341
320
  } else {
342
- // Otherwise, ensure it's a string
343
321
  id = String(id);
344
- if (this._config.debug) console.log(`Final ID value: ${id}`);
345
322
  }
346
323
 
347
- // Try to get a usable title, with fallbacks
324
+ // If still no ID, generate one
325
+ if (!id) {
326
+ id = `option-${Math.random().toString(36).substr(2, 9)}`;
327
+ if (this._config.debug) console.log(`Generated fallback ID: ${id}`);
328
+ }
329
+
330
+ // Get label with proper fallbacks
348
331
  let title = getValue(item, labelField);
349
- title = title !== null ? String(title) : '';
350
- if (this._config.debug)
351
- console.log(`Title/label field [${labelField}]:`, title);
332
+ if (!title) {
333
+ // Try common fallback fields for title
334
+ const fallbackFields = [
335
+ 'name',
336
+ 'title',
337
+ 'label',
338
+ 'text',
339
+ 'displayName',
340
+ 'description',
341
+ ];
342
+ for (const field of fallbackFields) {
343
+ if (item[field] !== null && item[field] !== undefined) {
344
+ title = String(item[field]);
345
+ if (this._config.debug)
346
+ console.log(`Using fallback field '${field}' for title: ${title}`);
347
+ break;
348
+ }
349
+ }
350
+ } else {
351
+ title = String(title);
352
+ }
352
353
 
353
- // If title is still empty, try common field names
354
+ // If still no title, use ID as fallback
354
355
  if (!title) {
355
- // Try common field names if the configured one doesn't work
356
- if (item.name) title = String(item.name);
357
- else if (item.title) title = String(item.title);
358
- else if (item.label) title = String(item.label);
359
- else if (item.text) title = String(item.text);
356
+ title = `Option ${id}`;
360
357
  if (this._config.debug)
361
- console.log('After fallback checks, title:', title);
358
+ console.log(`Using ID as fallback title: ${title}`);
362
359
  }
363
360
 
364
- // Create the option object with non-empty values
365
- const result = {
366
- id: id || title || 'id-' + Math.random().toString(36).substr(2, 9), // Ensure we always have an ID
367
- title: title || 'Unnamed option',
361
+ // Create the option object with consistent structure
362
+ const result: KTSelectOptionData = {
363
+ id: id,
364
+ title: title,
365
+ selected: Boolean(item.selected),
366
+ disabled: Boolean(item.disabled),
368
367
  };
369
368
 
370
369
  if (this._config.debug)
@@ -6,11 +6,7 @@
6
6
  import { KTSelectConfigInterface } from './config';
7
7
  import { KTSelect } from './select';
8
8
  import { defaultTemplates } from './templates';
9
- import {
10
- filterOptions,
11
- FocusManager,
12
- EventManager,
13
- } from './utils';
9
+ import { filterOptions, FocusManager, EventManager } from './utils';
14
10
 
15
11
  export class KTSelectSearch {
16
12
  private _select: KTSelect;
@@ -63,7 +59,7 @@ export class KTSelectSearch {
63
59
  this._eventManager.addListener(
64
60
  this._searchInput,
65
61
  'keydown',
66
- this._handleSearchKeyDown.bind(this)
62
+ this._handleSearchKeyDown.bind(this),
67
63
  );
68
64
 
69
65
  // Add blur event listener to ensure highlights are cleared when focus is lost
@@ -96,18 +92,20 @@ export class KTSelectSearch {
96
92
  }
97
93
 
98
94
  // Listen for dropdown close to reset options - ATTACH TO WRAPPER
99
- this._select.getWrapperElement().addEventListener('dropdown.close', () => {
100
- this._focusManager.resetFocus();
101
- // If clearSearchOnClose is false and there's a value, the search term and filtered state should persist.
102
- // KTSelect's closeDropdown method already calls this._searchModule.clearSearch() (which clears highlights)
103
- // and conditionally clears the input value based on KTSelect's config.clearSearchOnClose.
104
- // This listener in search.ts seems to unconditionally clear everything.
105
- // For now, keeping its original behavior:
106
- this.clearSearch(); // Clears highlights from current options
107
- this._searchInput.value = ''; // Clears the search input field
108
- this._resetAllOptions(); // Shows all options, restores original text, removes highlights
109
- this._clearNoResultsMessage(); // Clears any "no results" message
110
- });
95
+ this._select
96
+ .getWrapperElement()
97
+ .addEventListener('dropdown.close', () => {
98
+ this._focusManager.resetFocus();
99
+ // If clearSearchOnClose is false and there's a value, the search term and filtered state should persist.
100
+ // KTSelect's closeDropdown method already calls this._searchModule.clearSearch() (which clears highlights)
101
+ // and conditionally clears the input value based on KTSelect's config.clearSearchOnClose.
102
+ // This listener in search.ts seems to unconditionally clear everything.
103
+ // For now, keeping its original behavior:
104
+ this.clearSearch(); // Clears highlights from current options
105
+ this._searchInput.value = ''; // Clears the search input field
106
+ this._resetAllOptions(); // Shows all options, restores original text, removes highlights
107
+ this._clearNoResultsMessage(); // Clears any "no results" message
108
+ });
111
109
 
112
110
  // Clear highlights when an option is selected - ATTACH TO ORIGINAL SELECT (standard 'change' event)
113
111
  this._select.getElement().addEventListener('change', () => {
@@ -121,29 +119,32 @@ export class KTSelectSearch {
121
119
  });
122
120
 
123
121
  // Consolidated 'dropdown.show' event listener - ATTACH TO WRAPPER
124
- this._select.getWrapperElement().addEventListener('dropdown.show', () => {
125
- this._focusManager.resetFocus(); // Always clear previous focus state
126
-
127
- if (this._searchInput?.value) {
128
- // If there's an existing search term:
129
- // 1. Re-filter options. This ensures the display (hidden/visible) is correct
130
- // and "no results" message is handled if query yields nothing.
131
- this._filterOptions(this._searchInput.value);
132
- } else {
133
- // If search input is empty:
134
- // 1. Reset all options to their full, unfiltered, original state.
135
- this._resetAllOptions(); // Shows all, clears highlights from options, restores original text
136
- // 2. Clear any "no results" message.
137
- this._clearNoResultsMessage();
138
- }
122
+ this._select
123
+ .getWrapperElement()
124
+ .addEventListener('dropdown.show', () => {
125
+ this._focusManager.resetFocus(); // Always clear previous focus state
126
+
127
+ if (this._searchInput?.value) {
128
+ // If there's an existing search term:
129
+ // 1. Re-filter options. This ensures the display (hidden/visible) is correct
130
+ // and "no results" message is handled if query yields nothing.
131
+ this._filterOptions(this._searchInput.value);
132
+ } else {
133
+ // If search input is empty:
134
+ // 1. Reset all options to their full, unfiltered, original state.
135
+ this._resetAllOptions(); // Shows all, clears highlights from options, restores original text
136
+ // 2. Clear any "no results" message.
137
+ this._clearNoResultsMessage();
138
+ }
139
139
 
140
- // Handle autofocus for the search input (this was one of the original separate listeners)
141
- if (this._select.getConfig().searchAutofocus) {
142
- setTimeout(() => {
143
- this._searchInput?.focus(); // Focus search input
144
- }, 50); // Delay to ensure dropdown is visible
145
- }
146
- });
140
+ // Handle autofocus for the search input (this was one of the original separate listeners)
141
+ if (this._select.getConfig().searchAutofocus) {
142
+ setTimeout(() => {
143
+ this._searchInput?.focus(); // Focus search input
144
+ }, 50); // Delay to ensure dropdown is visible
145
+ }
146
+ this._select.updateSelectAllButtonState();
147
+ });
147
148
  }
148
149
  }
149
150
  }
@@ -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();
@@ -223,8 +229,10 @@ export class KTSelectSearch {
223
229
  * This is typically called before applying new filters/highlights.
224
230
  */
225
231
  private _restoreOptionContentsBeforeFilter(): void {
226
- const options = Array.from(this._select.getOptionsElement()) as HTMLElement[];
227
- options.forEach(option => {
232
+ const options = Array.from(
233
+ this._select.getOptionsElement(),
234
+ ) as HTMLElement[];
235
+ options.forEach((option) => {
228
236
  const value = option.getAttribute('data-value');
229
237
  if (value && this._originalOptionContents.has(value)) {
230
238
  const originalContent = this._originalOptionContents.get(value)!;
@@ -289,9 +297,15 @@ export class KTSelectSearch {
289
297
  // Restore original content before filtering, so highlighting is applied fresh.
290
298
  this._restoreOptionContentsBeforeFilter();
291
299
 
292
- const visibleCount = filterOptions(options, query, config, dropdownElement, (count) =>
293
- this._handleNoResults(count),
300
+ const visibleCount = filterOptions(
301
+ options,
302
+ query,
303
+ config,
304
+ dropdownElement,
305
+ (count) => this._handleNoResults(count),
294
306
  );
307
+
308
+ this._select.updateSelectAllButtonState();
295
309
  }
296
310
 
297
311
  /**
@@ -324,6 +338,7 @@ export class KTSelectSearch {
324
338
  });
325
339
 
326
340
  this._clearNoResultsMessage(); // Ensure no results message is cleared when resetting
341
+ this._select.updateSelectAllButtonState();
327
342
  }
328
343
 
329
344
  private _handleNoResults(visibleOptionsCount: number) {