@keenthemes/ktui 1.1.1 → 1.1.3

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 (81) hide show
  1. package/dist/ktui.js +674 -225
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +13 -1
  5. package/lib/cjs/components/component.js +22 -0
  6. package/lib/cjs/components/component.js.map +1 -1
  7. package/lib/cjs/components/datatable/datatable.js +7 -1
  8. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  9. package/lib/cjs/components/drawer/drawer.js +255 -9
  10. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  11. package/lib/cjs/components/dropdown/dropdown.js +55 -8
  12. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  13. package/lib/cjs/components/select/combobox.js +0 -2
  14. package/lib/cjs/components/select/combobox.js.map +1 -1
  15. package/lib/cjs/components/select/config.js +4 -1
  16. package/lib/cjs/components/select/config.js.map +1 -1
  17. package/lib/cjs/components/select/dropdown.js +0 -16
  18. package/lib/cjs/components/select/dropdown.js.map +1 -1
  19. package/lib/cjs/components/select/remote.js +0 -40
  20. package/lib/cjs/components/select/remote.js.map +1 -1
  21. package/lib/cjs/components/select/search.js +93 -22
  22. package/lib/cjs/components/select/search.js.map +1 -1
  23. package/lib/cjs/components/select/select.js +180 -114
  24. package/lib/cjs/components/select/select.js.map +1 -1
  25. package/lib/cjs/components/select/tags.js +0 -2
  26. package/lib/cjs/components/select/tags.js.map +1 -1
  27. package/lib/cjs/components/sticky/sticky.js +44 -5
  28. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  29. package/lib/cjs/helpers/data.js +8 -0
  30. package/lib/cjs/helpers/data.js.map +1 -1
  31. package/lib/cjs/helpers/event-handler.js +6 -5
  32. package/lib/cjs/helpers/event-handler.js.map +1 -1
  33. package/lib/cjs/index.js.map +1 -1
  34. package/lib/esm/components/component.js +22 -0
  35. package/lib/esm/components/component.js.map +1 -1
  36. package/lib/esm/components/datatable/datatable.js +7 -1
  37. package/lib/esm/components/datatable/datatable.js.map +1 -1
  38. package/lib/esm/components/drawer/drawer.js +255 -9
  39. package/lib/esm/components/drawer/drawer.js.map +1 -1
  40. package/lib/esm/components/dropdown/dropdown.js +55 -8
  41. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  42. package/lib/esm/components/select/combobox.js +0 -2
  43. package/lib/esm/components/select/combobox.js.map +1 -1
  44. package/lib/esm/components/select/config.js +4 -1
  45. package/lib/esm/components/select/config.js.map +1 -1
  46. package/lib/esm/components/select/dropdown.js +0 -16
  47. package/lib/esm/components/select/dropdown.js.map +1 -1
  48. package/lib/esm/components/select/remote.js +0 -40
  49. package/lib/esm/components/select/remote.js.map +1 -1
  50. package/lib/esm/components/select/search.js +93 -22
  51. package/lib/esm/components/select/search.js.map +1 -1
  52. package/lib/esm/components/select/select.js +180 -114
  53. package/lib/esm/components/select/select.js.map +1 -1
  54. package/lib/esm/components/select/tags.js +0 -2
  55. package/lib/esm/components/select/tags.js.map +1 -1
  56. package/lib/esm/components/sticky/sticky.js +44 -5
  57. package/lib/esm/components/sticky/sticky.js.map +1 -1
  58. package/lib/esm/helpers/data.js +8 -0
  59. package/lib/esm/helpers/data.js.map +1 -1
  60. package/lib/esm/helpers/event-handler.js +6 -5
  61. package/lib/esm/helpers/event-handler.js.map +1 -1
  62. package/lib/esm/index.js.map +1 -1
  63. package/package.json +6 -4
  64. package/src/components/component.ts +26 -0
  65. package/src/components/datatable/__tests__/race-conditions.test.ts +7 -7
  66. package/src/components/datatable/datatable.ts +8 -1
  67. package/src/components/drawer/drawer.ts +266 -10
  68. package/src/components/dropdown/dropdown.ts +63 -8
  69. package/src/components/select/__tests__/ux-behaviors.test.ts +997 -0
  70. package/src/components/select/combobox.ts +0 -1
  71. package/src/components/select/config.ts +7 -1
  72. package/src/components/select/dropdown.ts +0 -24
  73. package/src/components/select/remote.ts +0 -49
  74. package/src/components/select/search.ts +97 -24
  75. package/src/components/select/select.css +5 -1
  76. package/src/components/select/select.ts +211 -153
  77. package/src/components/select/tags.ts +0 -1
  78. package/src/components/sticky/sticky.ts +55 -5
  79. package/src/helpers/data.ts +10 -0
  80. package/src/helpers/event-handler.ts +7 -6
  81. package/src/index.ts +2 -0
@@ -54,7 +54,6 @@ export class KTSelectCombobox {
54
54
  // this._select.showAllOptions(); // showAllOptions might be too broad, filtering is managed by typing.
55
55
  });
56
56
 
57
- if (this._config.debug) console.log('KTSelectCombobox initialized');
58
57
  }
59
58
 
60
59
  /**
@@ -52,7 +52,8 @@ export const DefaultConfig: KTSelectConfigInterface = {
52
52
  searchMinLength: 0, // Minimum characters required to trigger search
53
53
  searchMaxItems: 50, // Maximum number of search results to display
54
54
  searchEmpty: 'No results', // Text to display when no search results are found
55
- clearSearchOnClose: true, // Clear search input when dropdown closes
55
+ clearSearchOnClose: false, // Clear search input when dropdown closes (default: false to persist search text)
56
+ closeOnEnter: true, // Close dropdown when Enter is pressed in search input
56
57
 
57
58
  // Multi-Select Display
58
59
  selectAllText: 'Select all', // Text for the "Select All" option (if implemented)
@@ -73,6 +74,8 @@ export const DefaultConfig: KTSelectConfigInterface = {
73
74
  dropdownPreventOverflow: false,
74
75
  dropdownStrategy: null,
75
76
  dropdownWidth: null, // Custom width for dropdown (e.g., '300px'), null to match toggle element width
77
+ closeOnOtherOpen: true, // Close other open dropdowns when this one opens
78
+ dispatchGlobalEvents: true, // Dispatch events on document for global listeners (jQuery compatibility)
76
79
 
77
80
  // New Config
78
81
  dropdownTemplate: '',
@@ -102,6 +105,7 @@ export interface KTSelectConfigInterface {
102
105
  searchDebounce?: number;
103
106
  searchParam?: string;
104
107
  clearSearchOnClose?: boolean;
108
+ closeOnEnter?: boolean;
105
109
 
106
110
  // Multi-Select Display
107
111
  selectAllText?: string;
@@ -144,6 +148,8 @@ export interface KTSelectConfigInterface {
144
148
  dropdownPreventOverflow?: boolean;
145
149
  dropdownStrategy?: 'fixed' | 'absolute';
146
150
  dropdownWidth?: string | null; // Custom width for dropdown, null to match toggle element width
151
+ closeOnOtherOpen?: boolean;
152
+ dispatchGlobalEvents?: boolean;
147
153
 
148
154
  // Styling
149
155
  dropdownClass?: string;
@@ -113,8 +113,6 @@ export class KTSelectDropdown extends KTComponent {
113
113
  event.stopPropagation();
114
114
 
115
115
  if (this._config.disabled) {
116
- if (this._config.debug)
117
- console.log('KTSelectDropdown._handleToggleClick: select is disabled');
118
116
  return;
119
117
  }
120
118
 
@@ -313,8 +311,6 @@ export class KTSelectDropdown extends KTComponent {
313
311
  */
314
312
  public open(): void {
315
313
  if (this._config.disabled) {
316
- if (this._config.debug)
317
- console.log('KTSelectDropdown.open: select is disabled, not opening');
318
314
  return;
319
315
  }
320
316
  if (this._isOpen || this._isTransitioning) return;
@@ -390,26 +386,12 @@ export class KTSelectDropdown extends KTComponent {
390
386
  * Close the dropdown
391
387
  */
392
388
  public close(): void {
393
- if (this._config.debug)
394
- console.log(
395
- 'KTSelectDropdown.close called - isOpen:',
396
- this._isOpen,
397
- 'isTransitioning:',
398
- this._isTransitioning,
399
- );
400
-
401
389
  if (!this._isOpen || this._isTransitioning) {
402
- if (this._config.debug)
403
- console.log(
404
- 'KTSelectDropdown.close - early return: dropdown not open or is transitioning',
405
- );
406
390
  return;
407
391
  }
408
392
 
409
393
  // Events and ARIA will be handled by KTSelect
410
394
 
411
- if (this._config.debug)
412
- console.log('KTSelectDropdown.close - starting transition');
413
395
  this._isTransitioning = true;
414
396
 
415
397
  this._dropdownElement.style.opacity = '0';
@@ -417,8 +399,6 @@ export class KTSelectDropdown extends KTComponent {
417
399
  let transitionComplete = false;
418
400
  const fallbackTimer = setTimeout(() => {
419
401
  if (!transitionComplete) {
420
- if (this._config.debug)
421
- console.log('KTSelectDropdown.close - fallback timer triggered');
422
402
  completeTransition();
423
403
  }
424
404
  }, 300);
@@ -428,8 +408,6 @@ export class KTSelectDropdown extends KTComponent {
428
408
  transitionComplete = true;
429
409
  clearTimeout(fallbackTimer);
430
410
 
431
- if (this._config.debug)
432
- console.log('KTSelectDropdown.close - transition ended');
433
411
 
434
412
  this._dropdownElement.classList.add('hidden');
435
413
  this._dropdownElement.classList.remove('open');
@@ -443,8 +421,6 @@ export class KTSelectDropdown extends KTComponent {
443
421
 
444
422
  // Events will be handled by KTSelect
445
423
 
446
- if (this._config.debug)
447
- console.log('KTSelectDropdown.close - visual part complete');
448
424
  };
449
425
 
450
426
  KTDom.transitionEnd(this._dropdownElement, completeTransition);
@@ -50,8 +50,6 @@ export class KTSelectRemote {
50
50
 
51
51
  let url = this._buildUrl(query, page);
52
52
 
53
- if (this._config.debug) console.log('Fetching remote data from:', url);
54
-
55
53
  // Dispatch search start event
56
54
  this._dispatchEvent('remoteSearchStart');
57
55
 
@@ -148,32 +146,21 @@ export class KTSelectRemote {
148
146
  */
149
147
  private _processData(data: any): KTSelectOptionData[] {
150
148
  try {
151
- if (this._config.debug) console.log('Processing API response:', data);
152
149
 
153
150
  let processedData = data;
154
151
 
155
152
  // Extract data from the API property if specified
156
153
  if (this._config.apiDataProperty && data[this._config.apiDataProperty]) {
157
- if (this._config.debug)
158
- console.log(
159
- `Extracting data from property: ${this._config.apiDataProperty}`,
160
- );
161
154
 
162
155
  // If pagination metadata is available, extract it
163
156
  if (this._config.pagination) {
164
157
  if (data.total_pages) {
165
158
  this._totalPages = data.total_pages;
166
- if (this._config.debug)
167
- console.log(`Total pages found: ${this._totalPages}`);
168
159
  }
169
160
  if (data.total) {
170
161
  this._totalPages = Math.ceil(
171
162
  data.total / (this._config.paginationLimit || 10),
172
163
  );
173
- if (this._config.debug)
174
- console.log(
175
- `Calculated total pages: ${this._totalPages} from total: ${data.total}`,
176
- );
177
164
  }
178
165
  }
179
166
 
@@ -186,10 +173,6 @@ export class KTSelectRemote {
186
173
  return [];
187
174
  }
188
175
 
189
- if (this._config.debug)
190
- console.log(
191
- `Mapping ${processedData.length} items to KTSelectOptionData format`,
192
- );
193
176
 
194
177
  // Map data to KTSelectOptionData format
195
178
  const mappedData = processedData.map((item: any): KTSelectOptionData => {
@@ -221,10 +204,6 @@ export class KTSelectRemote {
221
204
  // If we found a value, verify it matches what was extracted
222
205
  if (nestedValue !== null && nestedValue !== undefined) {
223
206
  const expectedValue = String(nestedValue);
224
- if (this._config.debug)
225
- console.log(
226
- `Data path verification for [${this._config.dataValueField}]: Expected: ${expectedValue}, Got: ${mappedItem.id}`,
227
- );
228
207
 
229
208
  if (mappedItem.id !== expectedValue && expectedValue) {
230
209
  console.warn(
@@ -234,13 +213,9 @@ export class KTSelectRemote {
234
213
  }
235
214
  }
236
215
 
237
- if (this._config.debug)
238
- console.log(`Mapped item: ${JSON.stringify(mappedItem)}`);
239
216
  return mappedItem;
240
217
  });
241
218
 
242
- if (this._config.debug)
243
- console.log(`Returned ${mappedData.length} mapped items`);
244
219
  return mappedData;
245
220
  } catch (error) {
246
221
  console.error('Error processing remote data:', error);
@@ -260,10 +235,6 @@ export class KTSelectRemote {
260
235
  const valueField = this._config.dataValueField || 'id';
261
236
  const labelField = this._config.dataFieldText || 'title';
262
237
 
263
- if (this._config.debug)
264
- console.log(`Mapping fields: value=${valueField}, label=${labelField}`);
265
- if (this._config.debug)
266
- console.log('Item data:', JSON.stringify(item).substring(0, 200) + '...'); // Trimmed for readability
267
238
 
268
239
  // Extract values using improved getValue function
269
240
  const getValue = (obj: any, path: string): any => {
@@ -285,17 +256,6 @@ export class KTSelectRemote {
285
256
  result = result[part];
286
257
  }
287
258
 
288
- // Log the extraction result
289
- if (this._config.debug)
290
- console.log(
291
- `Extracted [${path}] => ${
292
- result !== null && result !== undefined
293
- ? typeof result === 'object'
294
- ? JSON.stringify(result).substring(0, 50)
295
- : String(result).substring(0, 50)
296
- : 'null'
297
- }`,
298
- );
299
259
 
300
260
  return result;
301
261
  } catch (error) {
@@ -312,8 +272,6 @@ export class KTSelectRemote {
312
272
  for (const field of fallbackFields) {
313
273
  if (item[field] !== null && item[field] !== undefined) {
314
274
  id = String(item[field]);
315
- if (this._config.debug)
316
- console.log(`Using fallback field '${field}' for ID: ${id}`);
317
275
  break;
318
276
  }
319
277
  }
@@ -324,7 +282,6 @@ export class KTSelectRemote {
324
282
  // If still no ID, generate one
325
283
  if (!id) {
326
284
  id = `option-${Math.random().toString(36).substr(2, 9)}`;
327
- if (this._config.debug) console.log(`Generated fallback ID: ${id}`);
328
285
  }
329
286
 
330
287
  // Get label with proper fallbacks
@@ -342,8 +299,6 @@ export class KTSelectRemote {
342
299
  for (const field of fallbackFields) {
343
300
  if (item[field] !== null && item[field] !== undefined) {
344
301
  title = String(item[field]);
345
- if (this._config.debug)
346
- console.log(`Using fallback field '${field}' for title: ${title}`);
347
302
  break;
348
303
  }
349
304
  }
@@ -354,8 +309,6 @@ export class KTSelectRemote {
354
309
  // If still no title, use ID as fallback
355
310
  if (!title) {
356
311
  title = `Option ${id}`;
357
- if (this._config.debug)
358
- console.log(`Using ID as fallback title: ${title}`);
359
312
  }
360
313
 
361
314
  // Create the option object with consistent structure
@@ -366,8 +319,6 @@ export class KTSelectRemote {
366
319
  disabled: Boolean(item.disabled),
367
320
  };
368
321
 
369
- if (this._config.debug)
370
- console.log('Final mapped item:', JSON.stringify(result));
371
322
  return result;
372
323
  }
373
324
 
@@ -39,11 +39,6 @@ export class KTSelectSearch {
39
39
  this._searchInput = this._select.getSearchInput();
40
40
 
41
41
  if (this._searchInput) {
42
- if (this._config.debug)
43
- console.log(
44
- 'Initializing search module with input:',
45
- this._searchInput,
46
- );
47
42
 
48
43
  // First remove any existing listeners to prevent duplicates
49
44
  this._removeEventListeners();
@@ -96,24 +91,40 @@ export class KTSelectSearch {
96
91
  .getWrapperElement()
97
92
  .addEventListener('dropdown.close', () => {
98
93
  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
94
+ const config = this._select.getConfig();
95
+
96
+ // Clear highlights from current options (always do this)
97
+ this.clearSearch();
98
+
99
+ // Respect clearSearchOnClose config option
100
+ if (config.clearSearchOnClose) {
101
+ // Clear the search input field
102
+ this._searchInput.value = '';
103
+ // Reset all options to their original state
104
+ this._resetAllOptions();
105
+ // Clear any "no results" message
106
+ this._clearNoResultsMessage();
107
+ } else {
108
+ // When clearSearchOnClose is false, preserve search text
109
+ // The search input value is already preserved by KTSelect's closeDropdown method
110
+ // Reset options visibility to show all (they will be re-filtered when dropdown reopens)
111
+ this._resetAllOptions();
112
+ // Clear any "no results" message
113
+ this._clearNoResultsMessage();
114
+ // Note: The search input value is preserved, so when dropdown reopens,
115
+ // the dropdown.show listener will detect it and re-filter options accordingly
116
+ }
108
117
  });
109
118
 
110
119
  // Clear highlights when an option is selected - ATTACH TO ORIGINAL SELECT (standard 'change' event)
111
120
  this._select.getElement().addEventListener('change', () => {
112
121
  this.clearSearch();
113
122
 
114
- // Close dropdown only for single select mode
123
+ // Close dropdown only for single select mode, and only if closeOnEnter is not false
115
124
  // Keep dropdown open for multiple select mode to allow additional selections
116
- if (!this._select.getConfig().multiple) {
125
+ // Also respect closeOnEnter config when it's explicitly set to false
126
+ const config = this._select.getConfig();
127
+ if (!config.multiple && config.closeOnEnter !== false) {
117
128
  this._select.closeDropdown();
118
129
  }
119
130
  });
@@ -137,11 +148,9 @@ export class KTSelectSearch {
137
148
  this._clearNoResultsMessage();
138
149
  }
139
150
 
140
- // Handle autofocus for the search input (this was one of the original separate listeners)
151
+ // Handle autofocus for the search input with retry mechanism
141
152
  if (this._select.getConfig().searchAutofocus) {
142
- setTimeout(() => {
143
- this._searchInput?.focus(); // Focus search input
144
- }, 50); // Delay to ensure dropdown is visible
153
+ this._focusSearchInputWithRetry();
145
154
  }
146
155
  this._select.updateSelectAllButtonState();
147
156
  });
@@ -158,6 +167,58 @@ export class KTSelectSearch {
158
167
  }
159
168
  }
160
169
 
170
+ /**
171
+ * Focus the search input with retry mechanism for reliability
172
+ * Retries up to 3 times with exponential backoff (50ms, 100ms, 200ms)
173
+ */
174
+ private _focusSearchInputWithRetry(attempt: number = 0): void {
175
+ if (!this._searchInput) {
176
+ return;
177
+ }
178
+
179
+ const maxAttempts = 3;
180
+ const delays = [0, 50, 100, 200]; // Initial attempt + 3 retries
181
+
182
+ if (attempt > maxAttempts) {
183
+ if (this._config.debug) {
184
+ console.warn(
185
+ 'KTSelect: Failed to focus search input after',
186
+ maxAttempts,
187
+ 'attempts',
188
+ );
189
+ }
190
+ return;
191
+ }
192
+
193
+ const delay = delays[attempt] || 200;
194
+ const focusAttempt = () => {
195
+ try {
196
+ this._searchInput?.focus();
197
+ // Check if focus was successful
198
+ const isFocused = document.activeElement === this._searchInput || this._searchInput === document.activeElement;
199
+ if (isFocused) {
200
+ // Focus successful
201
+ return;
202
+ }
203
+ // Focus failed, retry if we haven't exceeded max attempts
204
+ if (attempt < maxAttempts) {
205
+ this._focusSearchInputWithRetry(attempt + 1);
206
+ }
207
+ } catch (error) {
208
+ // Focus failed with error, retry if we haven't exceeded max attempts
209
+ if (attempt < maxAttempts) {
210
+ this._focusSearchInputWithRetry(attempt + 1);
211
+ }
212
+ }
213
+ };
214
+
215
+ if (delay === 0) {
216
+ focusAttempt();
217
+ } else {
218
+ setTimeout(focusAttempt, delay);
219
+ }
220
+ }
221
+
161
222
  /**
162
223
  * Handles keydown events on the search input for navigation and actions.
163
224
  */
@@ -180,15 +241,27 @@ export class KTSelectSearch {
180
241
  break;
181
242
  case 'Enter':
182
243
  event.preventDefault();
183
- // Always attempt to select the first available option in the list.
184
- // focusFirst() finds, focuses, and returns the first visible, non-disabled option.
185
244
  const firstAvailableOption = this._focusManager.focusFirst();
186
245
 
187
246
  if (firstAvailableOption) {
188
247
  const optionValue = firstAvailableOption.getAttribute('data-value');
189
248
  if (optionValue) {
190
- this._select.toggleSelection(optionValue);
191
- // KTSelect.toggleSelection handles closing the dropdown based on config.closeOnSelect and config.multiple
249
+ const config = this._select.getConfig();
250
+ const isAlreadySelected = !config.multiple && this._select.getSelectedOptions().includes(optionValue);
251
+ const shouldClose = !config.multiple && config.closeOnEnter !== false;
252
+
253
+ if (isAlreadySelected && shouldClose) {
254
+ this._select.closeDropdown();
255
+ } else {
256
+ this._select.toggleSelection(optionValue);
257
+ }
258
+
259
+ // Focus display element after closing so user can press Enter again
260
+ if (shouldClose) {
261
+ setTimeout(() => {
262
+ this._select.getDisplayElement()?.focus();
263
+ }, 0);
264
+ }
192
265
  }
193
266
  }
194
267
  break;
@@ -127,7 +127,11 @@
127
127
  }
128
128
 
129
129
  .kt-select-display:not([data-multiple='true']) {
130
- @apply w-full overflow-ellipsis truncate;
130
+ @apply w-full flex items-center min-w-0;
131
+ }
132
+
133
+ .kt-select-display:not([data-multiple='true']) .kt-select-option-text {
134
+ @apply min-w-0 overflow-ellipsis truncate;
131
135
  }
132
136
 
133
137
  /* Enhanced Tag Styles */