@keenthemes/ktui 1.0.10 → 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 (139) hide show
  1. package/README.md +2 -2
  2. package/dist/ktui.js +1283 -1100
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/examples/select/basic-usage.html +43 -0
  6. package/examples/select/combobox-icons.html +58 -0
  7. package/examples/select/combobox.html +46 -0
  8. package/examples/select/description.html +69 -0
  9. package/examples/select/disable-option.html +43 -0
  10. package/examples/select/disable-select.html +34 -0
  11. package/examples/select/icon-description.html +56 -0
  12. package/examples/select/icon-multiple.html +58 -0
  13. package/examples/select/icon.html +58 -0
  14. package/examples/select/max-selection.html +39 -0
  15. package/examples/select/modal.html +70 -0
  16. package/examples/select/multiple.html +42 -0
  17. package/examples/select/placeholder.html +43 -0
  18. package/examples/select/remote-data.html +32 -0
  19. package/examples/select/search.html +49 -0
  20. package/examples/select/tags-icons.html +58 -0
  21. package/examples/select/tags-selected.html +59 -0
  22. package/examples/select/tags.html +58 -0
  23. package/examples/select/template-customization.html +65 -0
  24. package/examples/select/test.html +94 -0
  25. package/examples/toast/example.html +427 -0
  26. package/lib/cjs/components/component.js +1 -1
  27. package/lib/cjs/components/component.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable.js +22 -6
  29. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  30. package/lib/cjs/components/modal/modal.js +0 -4
  31. package/lib/cjs/components/modal/modal.js.map +1 -1
  32. package/lib/cjs/components/select/combobox.js +38 -120
  33. package/lib/cjs/components/select/combobox.js.map +1 -1
  34. package/lib/cjs/components/select/config.js +4 -16
  35. package/lib/cjs/components/select/config.js.map +1 -1
  36. package/lib/cjs/components/select/dropdown.js +10 -49
  37. package/lib/cjs/components/select/dropdown.js.map +1 -1
  38. package/lib/cjs/components/select/index.js +2 -1
  39. package/lib/cjs/components/select/index.js.map +1 -1
  40. package/lib/cjs/components/select/option.js +21 -4
  41. package/lib/cjs/components/select/option.js.map +1 -1
  42. package/lib/cjs/components/select/remote.js +1 -37
  43. package/lib/cjs/components/select/remote.js.map +1 -1
  44. package/lib/cjs/components/select/search.js +11 -41
  45. package/lib/cjs/components/select/search.js.map +1 -1
  46. package/lib/cjs/components/select/select.js +213 -326
  47. package/lib/cjs/components/select/select.js.map +1 -1
  48. package/lib/cjs/components/select/tags.js +39 -31
  49. package/lib/cjs/components/select/tags.js.map +1 -1
  50. package/lib/cjs/components/select/templates.js +120 -179
  51. package/lib/cjs/components/select/templates.js.map +1 -1
  52. package/lib/cjs/components/select/types.js +0 -12
  53. package/lib/cjs/components/select/types.js.map +1 -1
  54. package/lib/cjs/components/select/utils.js +204 -257
  55. package/lib/cjs/components/select/utils.js.map +1 -1
  56. package/lib/cjs/components/toast/index.js +10 -0
  57. package/lib/cjs/components/toast/index.js.map +1 -0
  58. package/lib/cjs/components/toast/toast.js +543 -0
  59. package/lib/cjs/components/toast/toast.js.map +1 -0
  60. package/lib/cjs/components/toast/types.js +7 -0
  61. package/lib/cjs/components/toast/types.js.map +1 -0
  62. package/lib/cjs/helpers/dom.js +24 -0
  63. package/lib/cjs/helpers/dom.js.map +1 -1
  64. package/lib/cjs/index.js +5 -1
  65. package/lib/cjs/index.js.map +1 -1
  66. package/lib/esm/components/component.js +1 -1
  67. package/lib/esm/components/component.js.map +1 -1
  68. package/lib/esm/components/datatable/datatable.js +22 -6
  69. package/lib/esm/components/datatable/datatable.js.map +1 -1
  70. package/lib/esm/components/modal/modal.js +0 -4
  71. package/lib/esm/components/modal/modal.js.map +1 -1
  72. package/lib/esm/components/select/combobox.js +39 -121
  73. package/lib/esm/components/select/combobox.js.map +1 -1
  74. package/lib/esm/components/select/config.js +3 -15
  75. package/lib/esm/components/select/config.js.map +1 -1
  76. package/lib/esm/components/select/dropdown.js +10 -49
  77. package/lib/esm/components/select/dropdown.js.map +1 -1
  78. package/lib/esm/components/select/index.js +1 -1
  79. package/lib/esm/components/select/index.js.map +1 -1
  80. package/lib/esm/components/select/option.js +21 -4
  81. package/lib/esm/components/select/option.js.map +1 -1
  82. package/lib/esm/components/select/remote.js +1 -37
  83. package/lib/esm/components/select/remote.js.map +1 -1
  84. package/lib/esm/components/select/search.js +12 -42
  85. package/lib/esm/components/select/search.js.map +1 -1
  86. package/lib/esm/components/select/select.js +214 -327
  87. package/lib/esm/components/select/select.js.map +1 -1
  88. package/lib/esm/components/select/tags.js +39 -31
  89. package/lib/esm/components/select/tags.js.map +1 -1
  90. package/lib/esm/components/select/templates.js +119 -178
  91. package/lib/esm/components/select/templates.js.map +1 -1
  92. package/lib/esm/components/select/types.js +1 -11
  93. package/lib/esm/components/select/types.js.map +1 -1
  94. package/lib/esm/components/select/utils.js +201 -255
  95. package/lib/esm/components/select/utils.js.map +1 -1
  96. package/lib/esm/components/toast/index.js +6 -0
  97. package/lib/esm/components/toast/index.js.map +1 -0
  98. package/lib/esm/components/toast/toast.js +540 -0
  99. package/lib/esm/components/toast/toast.js.map +1 -0
  100. package/lib/esm/components/toast/types.js +6 -0
  101. package/lib/esm/components/toast/types.js.map +1 -0
  102. package/lib/esm/helpers/dom.js +24 -0
  103. package/lib/esm/helpers/dom.js.map +1 -1
  104. package/lib/esm/index.js +3 -0
  105. package/lib/esm/index.js.map +1 -1
  106. package/package.json +8 -6
  107. package/src/components/alert/alert.css +20 -2
  108. package/src/components/badge/badge.css +5 -0
  109. package/src/components/component.ts +4 -0
  110. package/src/components/datatable/datatable.ts +24 -16
  111. package/src/components/drawer/drawer.css +1 -1
  112. package/src/components/input/input.css +3 -1
  113. package/src/components/link/link.css +2 -2
  114. package/src/components/modal/modal.css +18 -2
  115. package/src/components/modal/modal.ts +0 -5
  116. package/src/components/select/combobox.ts +42 -149
  117. package/src/components/select/config.ts +38 -33
  118. package/src/components/select/dropdown.ts +8 -55
  119. package/src/components/select/index.ts +1 -1
  120. package/src/components/select/option.ts +28 -7
  121. package/src/components/select/remote.ts +2 -42
  122. package/src/components/select/search.ts +14 -54
  123. package/src/components/select/select.css +49 -0
  124. package/src/components/select/select.ts +231 -437
  125. package/src/components/select/tags.ts +40 -37
  126. package/src/components/select/templates.ts +166 -303
  127. package/src/components/select/types.ts +0 -10
  128. package/src/components/select/utils.ts +214 -304
  129. package/src/components/table/table.css +1 -1
  130. package/src/components/textarea/textarea.css +2 -1
  131. package/src/components/toast/index.ts +7 -0
  132. package/src/components/toast/toast.css +60 -0
  133. package/src/components/toast/toast.ts +605 -0
  134. package/src/components/toast/types.ts +169 -0
  135. package/src/helpers/dom.ts +30 -0
  136. package/src/index.ts +4 -0
  137. package/styles/main.css +3 -0
  138. package/styles/vars.css +138 -0
  139. package/styles.css +1 -0
@@ -7,20 +7,10 @@
7
7
  * Common type interfaces for the KTSelect component
8
8
  */
9
9
 
10
- /**
11
- * Select mode options
12
- */
13
- export enum SelectMode {
14
- TAGS = 'tags',
15
- COMBOBOX = 'combobox',
16
- }
17
-
18
10
  export interface KTSelectOption {
19
11
  id: string;
20
12
  title: string;
21
13
  selected?: boolean;
22
- description?: string;
23
- icon?: string;
24
14
  disabled?: boolean;
25
15
  }
26
16
 
@@ -5,8 +5,7 @@
5
5
 
6
6
  // utils.ts
7
7
 
8
- import { KTSelect } from './select';
9
- import { defaultTemplates, getTemplateStrings } from './templates';
8
+ import { defaultTemplates } from './templates';
10
9
  import { KTSelectConfigInterface } from './config';
11
10
 
12
11
  /**
@@ -64,6 +63,18 @@ export function filterOptions(
64
63
  );
65
64
  }
66
65
  }
66
+
67
+ // Clear highlights by restoring original text content
68
+ if (option.dataset && option.dataset.originalText) {
69
+ option.innerHTML = option.dataset.originalText;
70
+ } else {
71
+ option.innerHTML = option.textContent || '';
72
+ }
73
+ // Remove the cache if present
74
+ if (option.dataset && option.dataset.originalText) {
75
+ delete option.dataset.originalText;
76
+ }
77
+
67
78
  visibleOptionsCount++;
68
79
  }
69
80
 
@@ -80,6 +91,9 @@ export function filterOptions(
80
91
  const optionText = option.textContent?.toLowerCase() || '';
81
92
  const isMatch = optionText.includes(query.toLowerCase());
82
93
 
94
+ // Check if option is disabled
95
+ const isDisabled = option.classList.contains('disabled') || option.getAttribute('aria-disabled') === 'true';
96
+
83
97
  if (isMatch || query.trim() === '') {
84
98
  // Show option by removing the hidden class and any display inline styles
85
99
  option.classList.remove('hidden');
@@ -105,27 +119,13 @@ export function filterOptions(
105
119
 
106
120
  visibleOptionsCount++;
107
121
 
108
- // Apply highlighting if needed - but preserve the option structure
109
- if (isMatch && config.searchHighlight && query.trim() !== '') {
110
- // Clone option elements that contain icons or descriptions
111
- const hasIcon =
112
- option.querySelector('[data-kt-select-option-icon]') !== null;
113
- const hasDescription =
114
- option.querySelector('[data-kt-select-option-description]') !== null;
115
-
116
- if (hasIcon || hasDescription) {
117
- // Only highlight the text part without changing structure
118
- const titleElement = option.querySelector(
119
- '[data-kt-option-title]',
120
- ) as HTMLElement;
121
- if (titleElement) {
122
- highlightTextInElement(titleElement, query, config);
123
- }
124
- } else {
125
- // Simple option with just text - standard highlighting
126
- highlightTextInElement(option, query, config);
122
+ if (config.searchHighlight && query.trim() !== '') {
123
+ if (option.dataset && !option.dataset.originalText) {
124
+ option.dataset.originalText = option.innerHTML;
127
125
  }
126
+ highlightTextInElementDebounced(option, query, config);
128
127
  }
128
+
129
129
  } else {
130
130
  // Hide option using hidden class
131
131
  option.classList.add('hidden');
@@ -175,39 +175,45 @@ export function highlightTextInElement(
175
175
  if (!element || !query || query.trim() === '') return;
176
176
 
177
177
  const queryLower = query.toLowerCase();
178
+ const text = element.textContent || '';
179
+ if (!text) return;
180
+
181
+ // Escape regex special characters in query
182
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
183
+ const regex = new RegExp(escapedQuery, 'gi');
184
+
185
+ // Replace all matches with the highlight template
186
+ let lastIndex = 0;
187
+ let result = '';
188
+ let match: RegExpExecArray | null;
189
+ let matches = [];
190
+ while ((match = regex.exec(text)) !== null) {
191
+ matches.push({ start: match.index, end: regex.lastIndex });
192
+ }
178
193
 
179
- function walk(node: Node) {
180
- if (node.nodeType === Node.TEXT_NODE) {
181
- const text = node.nodeValue || '';
182
- const textLower = text.toLowerCase();
183
- const matchIndex = textLower.indexOf(queryLower);
184
-
185
- if (matchIndex !== -1) {
186
- const before = text.slice(0, matchIndex);
187
- const match = text.slice(matchIndex, matchIndex + query.length);
188
- const after = text.slice(matchIndex + query.length);
189
-
190
- const frag = document.createDocumentFragment();
191
- if (before) frag.appendChild(document.createTextNode(before));
192
-
193
- // Use the highlight template, which returns an HTMLElement
194
- const highlightSpan = defaultTemplates.highlight(config, match);
195
- frag.appendChild(highlightSpan);
196
-
197
- if (after) frag.appendChild(document.createTextNode(after));
194
+ if (matches.length === 0) {
195
+ element.innerHTML = text;
196
+ return;
197
+ }
198
198
 
199
- node.parentNode?.replaceChild(frag, node);
200
- // Only highlight the first occurrence in this node
201
- }
202
- } else if (node.nodeType === Node.ELEMENT_NODE) {
203
- // Don't re-highlight already highlighted nodes
204
- if ((node as HTMLElement).classList.contains('highlight')) return;
205
- Array.from(node.childNodes).forEach(walk);
206
- }
199
+ for (let i = 0; i < matches.length; i++) {
200
+ const { start, end } = matches[i];
201
+ // Add text before match
202
+ result += text.slice(lastIndex, start);
203
+ // Add highlighted match using template
204
+ const highlighted = defaultTemplates.highlight(config, text.slice(start, end)).outerHTML;
205
+ result += highlighted;
206
+ lastIndex = end;
207
207
  }
208
- walk(element);
208
+ // Add remaining text
209
+ result += text.slice(lastIndex);
210
+
211
+ element.innerHTML = result;
209
212
  }
210
213
 
214
+ // Debounced version for performance
215
+ export const highlightTextInElementDebounced = debounce(highlightTextInElement, 100);
216
+
211
217
  /**
212
218
  * Focus manager for keyboard navigation
213
219
  * Consolidates redundant focus management logic into shared functions
@@ -218,9 +224,8 @@ export class FocusManager {
218
224
  private _focusedOptionIndex: number | null = null;
219
225
  private _focusClass: string;
220
226
  private _hoverClass: string;
221
- private _bgClass: string;
222
- private _fontClass: string;
223
227
  private _eventManager: EventManager;
228
+ private _onFocusChange: ((option: HTMLElement | null, index: number | null) => void) | null = null;
224
229
 
225
230
  constructor(
226
231
  element: HTMLElement,
@@ -231,14 +236,11 @@ export class FocusManager {
231
236
  this._optionsSelector = optionsSelector;
232
237
  this._eventManager = new EventManager();
233
238
 
234
- // Use config values if provided, otherwise fallback to defaults
235
- this._focusClass = config?.focusClass || 'option-focused';
236
- this._hoverClass = config?.hoverClass || 'hovered';
237
- this._bgClass = config?.bgClass || 'bg-blue-50';
238
- this._fontClass = config?.fontClass || 'font-medium';
239
-
240
239
  // Add click handler to update focus state when options are clicked
241
240
  this._setupOptionClickHandlers();
241
+
242
+ this._focusClass = 'focus'; // or whatever your intended class is
243
+ this._hoverClass = 'hover'; // or your intended class
242
244
  }
243
245
 
244
246
  /**
@@ -251,17 +253,6 @@ export class FocusManager {
251
253
  const optionElement = target.closest(this._optionsSelector);
252
254
 
253
255
  if (optionElement) {
254
- // First clear all focus
255
- this.resetFocus();
256
-
257
- // Then update the focused index based on the clicked option
258
- const options = this.getVisibleOptions();
259
- const clickedIndex = options.indexOf(optionElement as HTMLElement);
260
-
261
- if (clickedIndex >= 0) {
262
- this._focusedOptionIndex = clickedIndex;
263
- this.applyFocus(options[clickedIndex]);
264
- }
265
256
  }
266
257
  });
267
258
  }
@@ -287,26 +278,88 @@ export class FocusManager {
287
278
  }
288
279
 
289
280
  /**
290
- * Focus the next visible option
281
+ * Focus the first visible option
291
282
  */
292
- public focusNext(): HTMLElement | null {
283
+ public focusFirst(): HTMLElement | null {
293
284
  const options = this.getVisibleOptions();
294
285
  if (options.length === 0) return null;
286
+ for (let i = 0; i < options.length; i++) {
287
+ const option = options[i];
288
+ if (!option.classList.contains('disabled') && option.getAttribute('aria-disabled') !== 'true') {
289
+ this.resetFocus();
290
+ this._focusedOptionIndex = i;
291
+ this.applyFocus(option);
292
+ this.scrollIntoView(option);
293
+ return option;
294
+ }
295
+ }
296
+ return null;
297
+ }
295
298
 
296
- this.resetFocus();
297
-
298
- if (this._focusedOptionIndex === null) {
299
- this._focusedOptionIndex = 0;
300
- } else {
301
- this._focusedOptionIndex =
302
- (this._focusedOptionIndex + 1) % options.length;
299
+ /**
300
+ * Focus the last visible option
301
+ */
302
+ public focusLast(): HTMLElement | null {
303
+ const options = this.getVisibleOptions();
304
+ if (options.length === 0) return null;
305
+ for (let i = options.length - 1; i >= 0; i--) {
306
+ const option = options[i];
307
+ if (!option.classList.contains('disabled') && option.getAttribute('aria-disabled') !== 'true') {
308
+ this.resetFocus();
309
+ this._focusedOptionIndex = i;
310
+ this.applyFocus(option);
311
+ this.scrollIntoView(option);
312
+ return option;
313
+ }
303
314
  }
315
+ return null;
316
+ }
304
317
 
305
- const option = options[this._focusedOptionIndex];
306
- this.applyFocus(option);
307
- this.scrollIntoView(option);
318
+ /**
319
+ * Focus the next visible option that matches the search string
320
+ */
321
+ public focusByString(str: string): HTMLElement | null {
322
+ const options = this.getVisibleOptions();
323
+ if (options.length === 0) return null;
324
+ const lowerStr = str.toLowerCase();
325
+ const startIdx = (this._focusedOptionIndex ?? -1) + 1;
326
+ for (let i = 0; i < options.length; i++) {
327
+ const idx = (startIdx + i) % options.length;
328
+ const option = options[idx];
329
+ if (
330
+ !option.classList.contains('disabled') &&
331
+ option.getAttribute('aria-disabled') !== 'true' &&
332
+ (option.textContent?.toLowerCase().startsWith(lowerStr) || option.dataset.value?.toLowerCase().startsWith(lowerStr))
333
+ ) {
334
+ this._focusedOptionIndex = idx;
335
+ this.applyFocus(option);
336
+ this.scrollIntoView(option);
337
+ return option;
338
+ }
339
+ }
340
+ return null;
341
+ }
308
342
 
309
- return option;
343
+ /**
344
+ * Focus the next visible option
345
+ */
346
+ public focusNext(): HTMLElement | null {
347
+ const options = this.getVisibleOptions();
348
+ if (options.length === 0) return null;
349
+ let idx = this._focusedOptionIndex === null ? 0 : (this._focusedOptionIndex + 1) % options.length;
350
+ let startIdx = idx;
351
+ do {
352
+ const option = options[idx];
353
+ if (!option.classList.contains('disabled') && option.getAttribute('aria-disabled') !== 'true') {
354
+ this.resetFocus();
355
+ this._focusedOptionIndex = idx;
356
+ this.applyFocus(option);
357
+ this.scrollIntoView(option);
358
+ return option;
359
+ }
360
+ idx = (idx + 1) % options.length;
361
+ } while (idx !== startIdx);
362
+ return null;
310
363
  }
311
364
 
312
365
  /**
@@ -315,21 +368,20 @@ export class FocusManager {
315
368
  public focusPrevious(): HTMLElement | null {
316
369
  const options = this.getVisibleOptions();
317
370
  if (options.length === 0) return null;
318
-
319
- this.resetFocus();
320
-
321
- if (this._focusedOptionIndex === null) {
322
- this._focusedOptionIndex = options.length - 1;
323
- } else {
324
- this._focusedOptionIndex =
325
- (this._focusedOptionIndex - 1 + options.length) % options.length;
326
- }
327
-
328
- const option = options[this._focusedOptionIndex];
329
- this.applyFocus(option);
330
- this.scrollIntoView(option);
331
-
332
- return option;
371
+ let idx = this._focusedOptionIndex === null ? options.length - 1 : (this._focusedOptionIndex - 1 + options.length) % options.length;
372
+ let startIdx = idx;
373
+ do {
374
+ const option = options[idx];
375
+ if (!option.classList.contains('disabled') && option.getAttribute('aria-disabled') !== 'true') {
376
+ this.resetFocus();
377
+ this._focusedOptionIndex = idx;
378
+ this.applyFocus(option);
379
+ this.scrollIntoView(option);
380
+ return option;
381
+ }
382
+ idx = (idx - 1 + options.length) % options.length;
383
+ } while (idx !== startIdx);
384
+ return null;
333
385
  }
334
386
 
335
387
  /**
@@ -337,32 +389,24 @@ export class FocusManager {
337
389
  */
338
390
  public applyFocus(option: HTMLElement): void {
339
391
  if (!option) return;
340
-
341
- // Remove focus from all options first
392
+ if (option.classList.contains('disabled') || option.getAttribute('aria-disabled') === 'true') {
393
+ return;
394
+ }
342
395
  this.resetFocus();
343
-
344
- // Add focus to this option
345
396
  option.classList.add(this._focusClass);
346
397
  option.classList.add(this._hoverClass);
347
- option.classList.add(this._bgClass);
348
- option.classList.add(this._fontClass);
398
+ this._triggerFocusChange();
349
399
  }
350
400
 
351
401
  /**
352
402
  * Reset focus on all options
353
403
  */
354
404
  public resetFocus(): void {
355
- // Find all elements with the focus classes
356
- const focusedElements = this._element.querySelectorAll(
357
- `.${this._focusClass}, .${this._hoverClass}, .${this._bgClass}, .${this._fontClass}`,
358
- );
405
+ const focusedElements = this._element.querySelectorAll(`.${this._focusClass}, .${this._hoverClass}`);
359
406
 
360
- // Remove classes from all elements
407
+ // Remove focus and hover classes from all options
361
408
  focusedElements.forEach((element) => {
362
- element.classList.remove(this._focusClass);
363
- element.classList.remove(this._hoverClass);
364
- element.classList.remove(this._bgClass);
365
- element.classList.remove(this._fontClass);
409
+ element.classList.remove(this._focusClass, this._hoverClass);
366
410
  });
367
411
 
368
412
  // Reset index if visible options have changed
@@ -382,7 +426,7 @@ export class FocusManager {
382
426
  if (!option) return;
383
427
 
384
428
  const container = this._element.querySelector(
385
- '[data-kt-select-options-container]',
429
+ '[data-kt-select-options]',
386
430
  );
387
431
  if (!container) return;
388
432
 
@@ -446,6 +490,19 @@ export class FocusManager {
446
490
  this._focusedOptionIndex = index;
447
491
  }
448
492
 
493
+ /**
494
+ * Set a callback to be called when focus changes
495
+ */
496
+ public setOnFocusChange(cb: (option: HTMLElement | null, index: number | null) => void) {
497
+ this._onFocusChange = cb;
498
+ }
499
+
500
+ private _triggerFocusChange() {
501
+ if (this._onFocusChange) {
502
+ this._onFocusChange(this.getFocusedOption(), this._focusedOptionIndex);
503
+ }
504
+ }
505
+
449
506
  /**
450
507
  * Clean up event listeners
451
508
  */
@@ -456,197 +513,6 @@ export class FocusManager {
456
513
  }
457
514
  }
458
515
 
459
- /**
460
- * Shared keyboard navigation handler for dropdown options
461
- * Can be used by both combobox and search modules
462
- */
463
- export function handleDropdownKeyNavigation(
464
- event: KeyboardEvent,
465
- select: KTSelect,
466
- config: {
467
- multiple?: boolean;
468
- closeOnSelect?: boolean;
469
- },
470
- callbacks?: {
471
- onArrowUp?: () => void;
472
- onArrowDown?: () => void;
473
- onEnter?: () => void;
474
- onClose?: () => void;
475
- },
476
- ): void {
477
- try {
478
- // Get the dropdown state
479
- const isDropdownOpen = (select as any)._dropdownIsOpen;
480
-
481
- // Log the event to help debug
482
- const origin = 'handleDropdownKeyNavigation';
483
- if (select.getConfig && select.getConfig().debug)
484
- console.log(
485
- `[${origin}] Key: ${event.key}, Dropdown open: ${isDropdownOpen}`,
486
- );
487
-
488
- // Handle basic keyboard navigation
489
- switch (event.key) {
490
- case 'ArrowDown':
491
- if (!isDropdownOpen) {
492
- if (select.getConfig && select.getConfig().debug)
493
- console.log(`[${origin}] Opening dropdown on ArrowDown`);
494
- select.openDropdown();
495
-
496
- // Focus the first option after opening
497
- setTimeout(() => {
498
- (select as any)._focusNextOption();
499
- }, 50);
500
- } else if (callbacks?.onArrowDown) {
501
- if (select.getConfig && select.getConfig().debug)
502
- console.log(`[${origin}] Using custom onArrowDown callback`);
503
- callbacks.onArrowDown();
504
- } else {
505
- if (select.getConfig && select.getConfig().debug)
506
- console.log(`[${origin}] Using default _focusNextOption`);
507
- const focusedOption = (select as any)._focusNextOption();
508
-
509
- // Ensure we have a focused option
510
- if (focusedOption) {
511
- if (select.getConfig && select.getConfig().debug)
512
- console.log(`[${origin}] Focused next option:`, focusedOption);
513
- }
514
- }
515
- event.preventDefault();
516
- break;
517
-
518
- case 'ArrowUp':
519
- if (!isDropdownOpen) {
520
- if (select.getConfig && select.getConfig().debug)
521
- console.log(`[${origin}] Opening dropdown on ArrowUp`);
522
- select.openDropdown();
523
-
524
- // Focus the last option after opening
525
- setTimeout(() => {
526
- (select as any)._focusPreviousOption();
527
- }, 50);
528
- } else if (callbacks?.onArrowUp) {
529
- if (select.getConfig && select.getConfig().debug)
530
- console.log(`[${origin}] Using custom onArrowUp callback`);
531
- callbacks.onArrowUp();
532
- } else {
533
- if (select.getConfig && select.getConfig().debug)
534
- console.log(`[${origin}] Using default _focusPreviousOption`);
535
- const focusedOption = (select as any)._focusPreviousOption();
536
-
537
- // Ensure we have a focused option
538
- if (focusedOption) {
539
- if (select.getConfig && select.getConfig().debug)
540
- console.log(
541
- `[${origin}] Focused previous option:`,
542
- focusedOption,
543
- );
544
- }
545
- }
546
- event.preventDefault();
547
- break;
548
-
549
- case 'Enter':
550
- // Prevent form submission
551
- event.preventDefault();
552
-
553
- if (isDropdownOpen) {
554
- if (select.getConfig && select.getConfig().debug)
555
- console.log(`[${origin}] Enter pressed with dropdown open`);
556
-
557
- // For combobox mode, ensure we update the input value directly
558
- const isCombobox = select.getConfig().mode === 'combobox';
559
- const comboboxModule = (select as any)._comboboxModule;
560
-
561
- if (callbacks?.onEnter) {
562
- if (select.getConfig && select.getConfig().debug)
563
- console.log(`[${origin}] Using custom onEnter callback`);
564
- callbacks.onEnter();
565
- } else {
566
- if (select.getConfig && select.getConfig().debug)
567
- console.log(`[${origin}] Using default selectFocusedOption`);
568
- // Make sure there is a focused option before trying to select it
569
- if (
570
- (select as any)._focusManager &&
571
- (select as any)._focusManager.getFocusedOption()
572
- ) {
573
- select.selectFocusedOption();
574
- } else {
575
- // If no option is focused, try to focus the first one
576
- const focusedOption = (select as any)._focusNextOption();
577
- // Only select if an option was successfully focused
578
- if (focusedOption) {
579
- select.selectFocusedOption();
580
- }
581
- }
582
- }
583
-
584
- // Close dropdown after selection if not multiple and closeOnSelect is true
585
- if (!config.multiple && config.closeOnSelect !== false) {
586
- if (select.getConfig && select.getConfig().debug)
587
- console.log(`[${origin}] Closing dropdown after selection`);
588
- select.closeDropdown();
589
- }
590
- } else {
591
- // If dropdown is closed, open it on Enter
592
- if (select.getConfig && select.getConfig().debug)
593
- console.log(`[${origin}] Opening dropdown on Enter`);
594
- select.openDropdown();
595
-
596
- // Focus the first option after opening
597
- setTimeout(() => {
598
- (select as any)._focusNextOption();
599
- }, 50);
600
- }
601
- break;
602
-
603
- case 'Tab':
604
- // Only handle tab if dropdown is open
605
- if (isDropdownOpen) {
606
- if (select.getConfig && select.getConfig().debug)
607
- console.log(`[${origin}] Closing dropdown on Tab`);
608
- select.closeDropdown();
609
- if (callbacks?.onClose) {
610
- callbacks.onClose();
611
- }
612
- // Don't prevent default tab behavior - let it move focus naturally
613
- }
614
- break;
615
-
616
- case 'Escape':
617
- // Only handle escape if dropdown is open
618
- if (isDropdownOpen) {
619
- if (select.getConfig && select.getConfig().debug)
620
- console.log(`[${origin}] Closing dropdown on Escape`);
621
- select.closeDropdown();
622
- if (callbacks?.onClose) {
623
- callbacks.onClose();
624
- }
625
- event.preventDefault(); // Prevent other escape handlers
626
- }
627
- break;
628
-
629
- case ' ': // Space key
630
- // If dropdown is closed, space should open it (but not if in combobox mode)
631
- if (!isDropdownOpen && !(select.getConfig().mode === 'combobox')) {
632
- if (select.getConfig && select.getConfig().debug)
633
- console.log(`[${origin}] Opening dropdown on Space`);
634
- select.openDropdown();
635
-
636
- // Focus the first option after opening
637
- setTimeout(() => {
638
- (select as any)._focusNextOption();
639
- }, 50);
640
-
641
- event.preventDefault();
642
- }
643
- break;
644
- }
645
- } catch (error) {
646
- console.error('Error in keyboard navigation handler:', error);
647
- }
648
- }
649
-
650
516
  /**
651
517
  * Centralized event listener management
652
518
  */
@@ -721,7 +587,7 @@ export class EventManager {
721
587
  // Go through each event type
722
588
  this._boundHandlers.forEach((eventMap, event) => {
723
589
  // For each event type, go through each handler
724
- eventMap.forEach((boundHandler, originalHandler) => {
590
+ eventMap.forEach((boundHandler) => {
725
591
  element.removeEventListener(event, boundHandler);
726
592
  });
727
593
  });
@@ -745,3 +611,47 @@ export function debounce(
745
611
  timeout = setTimeout(() => func(...args), delay);
746
612
  };
747
613
  }
614
+
615
+ /**
616
+ * Replaces all {{key}} in the template with the corresponding value from the data object.
617
+ * If a key is missing in data, replaces with an empty string.
618
+ */
619
+ export function renderTemplateString(template: string, data: Record<string, any>): string {
620
+ return template.replace(/{{(\w+)}}/g, (_, key) =>
621
+ data[key] !== undefined && data[key] !== null ? String(data[key]) : ''
622
+ );
623
+ }
624
+
625
+ // Type-to-search buffer utility for keyboard navigation
626
+ export class TypeToSearchBuffer {
627
+ private buffer: string = '';
628
+ private lastTime: number = 0;
629
+ private timeout: number;
630
+
631
+ constructor(timeout: number = 500) {
632
+ this.timeout = timeout;
633
+ }
634
+
635
+ public push(char: string) {
636
+ const now = Date.now();
637
+ if (now - this.lastTime > this.timeout) {
638
+ this.buffer = '';
639
+ }
640
+ this.buffer += char;
641
+ this.lastTime = now;
642
+ }
643
+
644
+ public getBuffer() {
645
+ return this.buffer;
646
+ }
647
+
648
+ public clear() {
649
+ this.buffer = '';
650
+ }
651
+ }
652
+
653
+ export function stringToElement(html: string): HTMLElement {
654
+ const template = document.createElement('template');
655
+ template.innerHTML = html.trim();
656
+ return template.content.firstElementChild as HTMLElement;
657
+ }
@@ -13,7 +13,7 @@
13
13
  caption-side: bottom;
14
14
 
15
15
  tr {
16
- @apply border-b border-border transition-colors;
16
+ @apply border-b border-border;
17
17
  }
18
18
 
19
19
  caption {
@@ -9,7 +9,8 @@
9
9
  .kt-textarea {
10
10
  @apply w-full bg-background border border-input text-foreground shadow-xs shadow-[rgba(0,0,0,0.05)] transition-[color,box-shadow] placeholder:text-muted-foreground/80;
11
11
  @apply focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/30;
12
- @apply disabled:cursor-not-allowed disabled:opacity-50 [&[readonly]]:opacity-70 aria-invalid:border-destructive/60;
12
+ @apply disabled:cursor-not-allowed disabled:opacity-60;
13
+ @apply [&[readonly]]:bg-muted/80 [&[readonly]]:cursor-not-allowed;
13
14
  @apply aria-invalid:border-destructive/60 aria-invalid:ring-destructive/10;
14
15
  }
15
16
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ export { KTToast } from './toast';
7
+ export type { KTToastConfigInterface, KTToastInterface } from './types';