@keenthemes/ktui 1.0.12 → 1.0.13

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 (92) hide show
  1. package/dist/ktui.js +668 -685
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +5822 -0
  5. package/examples/select/avatar.html +47 -0
  6. package/examples/select/basic-usage.html +10 -14
  7. package/examples/select/{test.html → combobox-icons_.html} +13 -48
  8. package/examples/select/country.html +43 -0
  9. package/examples/select/description.html +25 -41
  10. package/examples/select/disable-option.html +10 -16
  11. package/examples/select/disable-select.html +7 -6
  12. package/examples/select/icon-multiple.html +23 -31
  13. package/examples/select/icon.html +20 -30
  14. package/examples/select/max-selection.html +8 -9
  15. package/examples/select/modal.html +19 -17
  16. package/examples/select/multiple.html +11 -13
  17. package/examples/select/placeholder.html +9 -12
  18. package/examples/select/search.html +30 -22
  19. package/examples/select/sizes.html +94 -0
  20. package/examples/select/template-customization.html +0 -3
  21. package/lib/cjs/components/component.js +1 -1
  22. package/lib/cjs/components/component.js.map +1 -1
  23. package/lib/cjs/components/datatable/datatable.js +1 -1
  24. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  25. package/lib/cjs/components/select/combobox.js +96 -61
  26. package/lib/cjs/components/select/combobox.js.map +1 -1
  27. package/lib/cjs/components/select/config.js +1 -3
  28. package/lib/cjs/components/select/config.js.map +1 -1
  29. package/lib/cjs/components/select/dropdown.js +32 -96
  30. package/lib/cjs/components/select/dropdown.js.map +1 -1
  31. package/lib/cjs/components/select/option.js +53 -20
  32. package/lib/cjs/components/select/option.js.map +1 -1
  33. package/lib/cjs/components/select/search.js +146 -97
  34. package/lib/cjs/components/select/search.js.map +1 -1
  35. package/lib/cjs/components/select/select.js +174 -120
  36. package/lib/cjs/components/select/select.js.map +1 -1
  37. package/lib/cjs/components/select/tags.js +0 -26
  38. package/lib/cjs/components/select/tags.js.map +1 -1
  39. package/lib/cjs/components/select/templates.js +130 -103
  40. package/lib/cjs/components/select/templates.js.map +1 -1
  41. package/lib/cjs/components/select/utils.js +33 -132
  42. package/lib/cjs/components/select/utils.js.map +1 -1
  43. package/lib/cjs/helpers/dom.js +0 -24
  44. package/lib/cjs/helpers/dom.js.map +1 -1
  45. package/lib/esm/components/component.js +1 -1
  46. package/lib/esm/components/component.js.map +1 -1
  47. package/lib/esm/components/datatable/datatable.js +1 -1
  48. package/lib/esm/components/datatable/datatable.js.map +1 -1
  49. package/lib/esm/components/select/combobox.js +96 -61
  50. package/lib/esm/components/select/combobox.js.map +1 -1
  51. package/lib/esm/components/select/config.js +1 -3
  52. package/lib/esm/components/select/config.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js +32 -96
  54. package/lib/esm/components/select/dropdown.js.map +1 -1
  55. package/lib/esm/components/select/option.js +53 -20
  56. package/lib/esm/components/select/option.js.map +1 -1
  57. package/lib/esm/components/select/search.js +146 -97
  58. package/lib/esm/components/select/search.js.map +1 -1
  59. package/lib/esm/components/select/select.js +174 -120
  60. package/lib/esm/components/select/select.js.map +1 -1
  61. package/lib/esm/components/select/tags.js +0 -26
  62. package/lib/esm/components/select/tags.js.map +1 -1
  63. package/lib/esm/components/select/templates.js +130 -103
  64. package/lib/esm/components/select/templates.js.map +1 -1
  65. package/lib/esm/components/select/utils.js +32 -130
  66. package/lib/esm/components/select/utils.js.map +1 -1
  67. package/lib/esm/helpers/dom.js +0 -24
  68. package/lib/esm/helpers/dom.js.map +1 -1
  69. package/package.json +9 -6
  70. package/src/components/component.ts +0 -4
  71. package/src/components/datatable/datatable.ts +1 -1
  72. package/src/components/input/input.css +1 -1
  73. package/src/components/scrollable/scrollable.css +9 -5
  74. package/src/components/select/combobox.ts +99 -88
  75. package/src/components/select/config.ts +2 -8
  76. package/src/components/select/dropdown.ts +43 -108
  77. package/src/components/select/option.ts +44 -25
  78. package/src/components/select/search.ts +158 -117
  79. package/src/components/select/select.css +97 -27
  80. package/src/components/select/select.ts +181 -127
  81. package/src/components/select/tags.ts +1 -27
  82. package/src/components/select/templates.ts +194 -131
  83. package/src/components/select/utils.ts +30 -166
  84. package/src/helpers/dom.ts +0 -30
  85. package/webpack.config.js +6 -1
  86. package/examples/select/combobox-icons.html +0 -58
  87. package/examples/select/icon-description.html +0 -56
  88. /package/examples/select/{combobox.html → combobox_.html} +0 -0
  89. /package/examples/select/{remote-data.html → remote-data_.html} +0 -0
  90. /package/examples/select/{tags-icons.html → tags-icons_.html} +0 -0
  91. /package/examples/select/{tags-selected.html → tags-selected_.html} +0 -0
  92. /package/examples/select/{tags.html → tags_.html} +0 -0
@@ -30,133 +30,47 @@ export function filterOptions(
30
30
  ): number {
31
31
  let visibleOptionsCount = 0;
32
32
 
33
- // Clear existing "no results" messages
34
- const noResultsElement = dropdownElement.querySelector(
35
- '[data-kt-select-no-results]',
36
- );
37
- if (noResultsElement) {
38
- noResultsElement.remove();
39
- }
40
-
41
- // For empty query, ensure highlights are cleared from all options
33
+ // For empty query, make all options visible
34
+ // The KTSelectSearch class is now responsible for restoring original content before calling this.
42
35
  if (!query || query.trim() === '') {
43
- // Just make all options visible without highlighting
44
36
  for (const option of options) {
45
- // Make option visible by removing hidden classes and inline styles
46
37
  option.classList.remove('hidden');
47
-
48
- // Clean up any inline display styles from legacy code
49
- if (
50
- option.hasAttribute('style') &&
51
- option.getAttribute('style').includes('display:')
52
- ) {
53
- const styleAttr = option.getAttribute('style');
54
- if (
55
- styleAttr.trim() === 'display: none;' ||
56
- styleAttr.trim() === 'display: block;'
57
- ) {
58
- option.removeAttribute('style');
59
- } else {
60
- option.setAttribute(
61
- 'style',
62
- styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
63
- );
38
+ // Remove inline display style if it was used to hide
39
+ if (option.style.display === 'none') {
40
+ option.style.display = '';
64
41
  }
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
-
42
+ // At this point, option.innerHTML should be its original.
78
43
  visibleOptionsCount++;
79
44
  }
80
45
 
81
- // Call the callback with the visible count if provided
82
46
  if (onVisibleCount) {
83
47
  onVisibleCount(visibleOptionsCount);
84
48
  }
85
-
86
49
  return visibleOptionsCount;
87
50
  }
88
51
 
89
- // Filter options based on query
90
- for (const option of options) {
91
- const optionText = option.textContent?.toLowerCase() || '';
92
- const isMatch = optionText.includes(query.toLowerCase());
52
+ const queryLower = query.toLowerCase();
93
53
 
94
- // Check if option is disabled
95
- const isDisabled = option.classList.contains('disabled') || option.getAttribute('aria-disabled') === 'true';
54
+ for (const option of options) {
55
+ // Use data-text for matching if available, otherwise fall back to textContent
56
+ const optionText = (option.dataset.text || option.textContent || '').toLowerCase();
57
+ const isMatch = optionText.includes(queryLower);
96
58
 
97
- if (isMatch || query.trim() === '') {
98
- // Show option by removing the hidden class and any display inline styles
59
+ if (isMatch) {
99
60
  option.classList.remove('hidden');
100
-
101
- // Remove any inline display styles that might be present
102
- if (
103
- option.hasAttribute('style') &&
104
- option.getAttribute('style').includes('display:')
105
- ) {
106
- const styleAttr = option.getAttribute('style');
107
- if (
108
- styleAttr.trim() === 'display: none;' ||
109
- styleAttr.trim() === 'display: block;'
110
- ) {
111
- option.removeAttribute('style');
112
- } else {
113
- option.setAttribute(
114
- 'style',
115
- styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
116
- );
117
- }
118
- }
119
-
61
+ if (option.style.display === 'none') option.style.display = ''; // Ensure visible
120
62
  visibleOptionsCount++;
121
63
 
122
- if (config.searchHighlight && query.trim() !== '') {
123
- if (option.dataset && !option.dataset.originalText) {
124
- option.dataset.originalText = option.innerHTML;
125
- }
126
- highlightTextInElementDebounced(option, query, config);
127
- }
128
-
129
64
  } else {
130
- // Hide option using hidden class
131
65
  option.classList.add('hidden');
132
-
133
- // Remove any inline display styles
134
- if (
135
- option.hasAttribute('style') &&
136
- option.getAttribute('style').includes('display:')
137
- ) {
138
- const styleAttr = option.getAttribute('style');
139
- if (
140
- styleAttr.trim() === 'display: none;' ||
141
- styleAttr.trim() === 'display: block;'
142
- ) {
143
- option.removeAttribute('style');
144
- } else {
145
- option.setAttribute(
146
- 'style',
147
- styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
148
- );
149
- }
150
- }
151
66
  }
152
67
 
153
- // Early exit if maxItems limit is reached
154
- if (config.searchMaxItems && visibleOptionsCount >= config.searchMaxItems) {
155
- break;
156
- }
68
+ // Early exit if maxItems limit is reached (optional)
69
+ // if (config.searchMaxItems && visibleOptionsCount >= config.searchMaxItems) {
70
+ // break;
71
+ // }
157
72
  }
158
73
 
159
- // Call the callback with the visible count if provided
160
74
  if (onVisibleCount) {
161
75
  onVisibleCount(visibleOptionsCount);
162
76
  }
@@ -164,56 +78,6 @@ export function filterOptions(
164
78
  return visibleOptionsCount;
165
79
  }
166
80
 
167
- /**
168
- * Highlight text only within a specific element, preserving other elements
169
- */
170
- export function highlightTextInElement(
171
- element: HTMLElement,
172
- query: string,
173
- config: KTSelectConfigInterface,
174
- ): void {
175
- if (!element || !query || query.trim() === '') return;
176
-
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
- }
193
-
194
- if (matches.length === 0) {
195
- element.innerHTML = text;
196
- return;
197
- }
198
-
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
- }
208
- // Add remaining text
209
- result += text.slice(lastIndex);
210
-
211
- element.innerHTML = result;
212
- }
213
-
214
- // Debounced version for performance
215
- export const highlightTextInElementDebounced = debounce(highlightTextInElement, 100);
216
-
217
81
  /**
218
82
  * Focus manager for keyboard navigation
219
83
  * Consolidates redundant focus management logic into shared functions
@@ -331,6 +195,7 @@ export class FocusManager {
331
195
  option.getAttribute('aria-disabled') !== 'true' &&
332
196
  (option.textContent?.toLowerCase().startsWith(lowerStr) || option.dataset.value?.toLowerCase().startsWith(lowerStr))
333
197
  ) {
198
+ this.resetFocus();
334
199
  this._focusedOptionIndex = idx;
335
200
  this.applyFocus(option);
336
201
  this.scrollIntoView(option);
@@ -389,12 +254,14 @@ export class FocusManager {
389
254
  */
390
255
  public applyFocus(option: HTMLElement): void {
391
256
  if (!option) return;
257
+ // Ensure it's not disabled
392
258
  if (option.classList.contains('disabled') || option.getAttribute('aria-disabled') === 'true') {
393
259
  return;
394
260
  }
395
- this.resetFocus();
261
+ // DO NOT CALL resetFocus() here. Caller's responsibility.
396
262
  option.classList.add(this._focusClass);
397
263
  option.classList.add(this._hoverClass);
264
+ // _triggerFocusChange needs _focusedOptionIndex to be set by the caller before this.
398
265
  this._triggerFocusChange();
399
266
  }
400
267
 
@@ -409,14 +276,7 @@ export class FocusManager {
409
276
  element.classList.remove(this._focusClass, this._hoverClass);
410
277
  });
411
278
 
412
- // Reset index if visible options have changed
413
- const visibleOptions = this.getVisibleOptions();
414
- if (
415
- this._focusedOptionIndex !== null &&
416
- this._focusedOptionIndex >= visibleOptions.length
417
- ) {
418
- this._focusedOptionIndex = null;
419
- }
279
+ this._focusedOptionIndex = null; // Always reset the index
420
280
  }
421
281
 
422
282
  /**
@@ -451,10 +311,14 @@ export class FocusManager {
451
311
  const index = options.findIndex((option) => option.dataset.value === value);
452
312
 
453
313
  if (index >= 0) {
454
- this._focusedOptionIndex = index;
455
- this.applyFocus(options[index]);
456
- this.scrollIntoView(options[index]);
457
- return true;
314
+ const optionToFocus = options[index];
315
+ if (!optionToFocus.classList.contains('disabled') && optionToFocus.getAttribute('aria-disabled') !== 'true'){
316
+ this.resetFocus();
317
+ this._focusedOptionIndex = index;
318
+ this.applyFocus(optionToFocus);
319
+ this.scrollIntoView(optionToFocus);
320
+ return true;
321
+ }
458
322
  }
459
323
 
460
324
  return false;
@@ -391,36 +391,6 @@ const KTDom = {
391
391
  return attributes;
392
392
  },
393
393
 
394
- getDataAttributesByJson(element: HTMLElement, prefix: string): object {
395
- if (!element) {
396
- return {};
397
- }
398
-
399
- const rawValue = element.dataset[prefix];
400
-
401
- if (!rawValue) {
402
- return {};
403
- }
404
-
405
- const parsedValue = KTUtils.parseDataAttribute(rawValue);
406
-
407
- if (typeof parsedValue === 'string') {
408
- try {
409
- return JSON.parse(parsedValue);
410
- } catch (e) {
411
- console.error(`Invalid JSON format for '${prefix}': ${e instanceof Error ? e.message : e} ${rawValue}`);
412
- }
413
- }
414
-
415
- // If it's already an object, return as is
416
- if (typeof parsedValue === 'object' && parsedValue !== null) {
417
- return parsedValue;
418
- }
419
-
420
- // For other types (number, boolean, null), return an empty object
421
- return {};
422
- },
423
-
424
394
  ready(callback: CallableFunction): void {
425
395
  if (document.readyState === 'loading') {
426
396
  document.addEventListener('DOMContentLoaded', () => {
package/webpack.config.js CHANGED
@@ -91,7 +91,12 @@ module.exports = (env) => {
91
91
  ...baseConfig.optimization,
92
92
  minimize: false, // Disable minimization for normal JS files
93
93
  },
94
- plugins: [...baseConfig.plugins, new CleanWebpackPlugin()],
94
+ plugins: [
95
+ ...baseConfig.plugins,
96
+ new CleanWebpackPlugin({
97
+ cleanOnceBeforeBuildPatterns: ['**/*', '!styles.css'],
98
+ }),
99
+ ],
95
100
  };
96
101
 
97
102
  const minifiedConfig = {
@@ -1,58 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Combobox Icons</title>
8
- <link rel="stylesheet" href="../../dist/styles.css">
9
- </head>
10
-
11
- <body class="bg-gray-50 min-h-screen">
12
- <div class="bg-white rounded-lg shadow p-8 w-full max-w-sm mt-10 mx-auto">
13
- <form class="flex flex-col gap-6">
14
- <div>
15
- <label class="block mb-2 font-medium text-gray-700" for="select-basic">Combobox Icons</label>
16
-
17
- <select
18
- class="kt-select"
19
- data-kt-select
20
- data-kt-select-combobox="true"
21
- data-kt-select-placeholder="Type or select a country..."
22
- data-kt-select-config='{
23
- "displayTemplate": "<div class=\"flex items-center gap-2\">{{icon}}<span class=\"font-semibold text-gray-900\">{{text}}</span></div>",
24
- "displayClass": "kt-select-display",
25
- "displayToggle": true,
26
- "optionTemplate": "<div class=\"flex items-center gap-2\">{{icon}}<span class=\"text-gray-800\">{{text}}</span></div>",
27
- "optionClass": "kt-select-option"
28
- }'>
29
- <option value="us"
30
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/us.png\" class=\"w-6 h-6\" />"}'>
31
- United States</option>
32
- <option value="de"
33
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/de.png\" class=\"w-6 h-6\" />"}'>
34
- Germany</option>
35
- <option value="it"
36
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/it.png\" class=\"w-6 h-6\" />"}'>
37
- Italy</option>
38
- <option value="fr"
39
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/fr.png\" class=\"w-6 h-6\" />"}'>
40
- France</option>
41
- <option value="es"
42
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/es.png\" class=\"w-6 h-6\" />"}'>
43
- Spain</option>
44
- <option value="jp"
45
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/jp.png\" class=\"w-6 h-6\" />"}'>
46
- Japan</option>
47
- <option value="ru"
48
- data-kt-select-option='{"icon": "<img src=\"https://flagcdn.com/w40/ru.png\" class=\"w-6 h-6\" />"}'>
49
- Russia</option>
50
- </select>
51
-
52
- </div>
53
- </form>
54
- </div>
55
- <script src="../../dist/ktui.js"></script>
56
- </body>
57
-
58
- </html>
@@ -1,56 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Icon Description</title>
8
- <link rel="stylesheet" href="../../dist/styles.css">
9
- </head>
10
-
11
- <body class="bg-gray-50 min-h-screen">
12
- <div class="bg-white rounded-lg shadow p-8 w-full max-w-sm mt-10 mx-auto">
13
- <form class="flex flex-col gap-6">
14
- <div>
15
- <label class="block mb-2 font-medium text-gray-700" for="select-basic">Icon Description</label>
16
-
17
- <select
18
- class="kt-select"
19
- data-kt-select
20
- data-kt-select-placeholder="Select with description..."
21
- data-kt-select-config='{
22
- "displayTemplate": "<div class=\"flex items-start gap-3\">{{icon}}<div class=\"flex flex-col\"><span class=\"font-semibold text-gray-900 leading-tight\">{{text}}</span><span class=\"text-xs text-gray-500 leading-tight\">{{desc}}</span></div></div>",
23
- "displayClass": "kt-select-display",
24
- "displayToggle": true,
25
- "optionTemplate": "<div class=\"flex items-start gap-3\">{{icon}}<div class=\"flex flex-col\"><span class=\"text-gray-800 leading-tight\">{{text}}</span><span class=\"text-xs text-gray-500 leading-tight\">{{desc}}</span></div></div>",
26
- "optionClass": "kt-select-option"
27
- }'>
28
- <option value="mysql"
29
- data-kt-select-option='{"icon": "<img src=\"https://cdn.simpleicons.org/mysql/4479A1\" class=\"w-6 h-6\" />", "desc": "MySQL is a relational database management system"}'>
30
- MySQL
31
- </option>
32
- <option value="postgres"
33
- data-kt-select-option='{"icon": "<img src=\"https://cdn.simpleicons.org/postgresql/4169E1\" class=\"w-6 h-6\" />", "desc": "PostgreSQL is a powerful object-relational database system"}'>
34
- PostgreSQL
35
- </option>
36
- <option value="mongodb"
37
- data-kt-select-option='{"icon": "<img src=\"https://cdn.simpleicons.org/mongodb/47A248\" class=\"w-6 h-6\" />", "desc": "MongoDB is a NoSQL database that uses JSON-like documents with optional schemas"}'>
38
- MongoDB
39
- </option>
40
- <option value="redis"
41
- data-kt-select-option='{"icon": "<img src=\"https://cdn.simpleicons.org/redis/DC382D\" class=\"w-6 h-6\" />", "desc": "Redis is an in-memory data structure store"}'>
42
- Redis
43
- </option>
44
- <option value="sqlite"
45
- data-kt-select-option='{"icon": "<img src=\"https://cdn.simpleicons.org/sqlite/003B57\" class=\"w-6 h-6\" />", "desc": "SQLite is a lightweight, disk-based database"}'>
46
- SQLite
47
- </option>
48
- </select>
49
-
50
- </div>
51
- </form>
52
- </div>
53
- <script src="../../dist/ktui.js"></script>
54
- </body>
55
-
56
- </html>
File without changes