@keenthemes/ktui 1.0.20 → 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 (98) hide show
  1. package/dist/ktui.js +418 -144
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +139 -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 +5 -3
  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 +9 -3
  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/dropdown.js +4 -2
  26. package/lib/cjs/components/select/dropdown.js.map +1 -1
  27. package/lib/cjs/components/select/index.js.map +1 -1
  28. package/lib/cjs/components/select/option.js +2 -1
  29. package/lib/cjs/components/select/option.js.map +1 -1
  30. package/lib/cjs/components/select/remote.js +50 -50
  31. package/lib/cjs/components/select/remote.js.map +1 -1
  32. package/lib/cjs/components/select/search.js +7 -5
  33. package/lib/cjs/components/select/search.js.map +1 -1
  34. package/lib/cjs/components/select/select.js +199 -33
  35. package/lib/cjs/components/select/select.js.map +1 -1
  36. package/lib/cjs/components/select/tags.js +3 -1
  37. package/lib/cjs/components/select/tags.js.map +1 -1
  38. package/lib/cjs/components/select/templates.js.map +1 -1
  39. package/lib/cjs/components/select/utils.js +23 -10
  40. package/lib/cjs/components/select/utils.js.map +1 -1
  41. package/lib/cjs/components/sticky/sticky.js +52 -14
  42. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  43. package/lib/esm/components/component.js +5 -3
  44. package/lib/esm/components/component.js.map +1 -1
  45. package/lib/esm/components/datatable/datatable-sort.js +4 -0
  46. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  47. package/lib/esm/components/datatable/datatable.js +9 -3
  48. package/lib/esm/components/datatable/datatable.js.map +1 -1
  49. package/lib/esm/components/image-input/image-input.js +10 -2
  50. package/lib/esm/components/image-input/image-input.js.map +1 -1
  51. package/lib/esm/components/select/combobox.js +50 -20
  52. package/lib/esm/components/select/combobox.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js +4 -2
  54. package/lib/esm/components/select/dropdown.js.map +1 -1
  55. package/lib/esm/components/select/index.js +1 -1
  56. package/lib/esm/components/select/index.js.map +1 -1
  57. package/lib/esm/components/select/option.js +2 -1
  58. package/lib/esm/components/select/option.js.map +1 -1
  59. package/lib/esm/components/select/remote.js +50 -50
  60. package/lib/esm/components/select/remote.js.map +1 -1
  61. package/lib/esm/components/select/search.js +8 -6
  62. package/lib/esm/components/select/search.js.map +1 -1
  63. package/lib/esm/components/select/select.js +199 -33
  64. package/lib/esm/components/select/select.js.map +1 -1
  65. package/lib/esm/components/select/tags.js +3 -1
  66. package/lib/esm/components/select/tags.js.map +1 -1
  67. package/lib/esm/components/select/templates.js.map +1 -1
  68. package/lib/esm/components/select/utils.js +23 -10
  69. package/lib/esm/components/select/utils.js.map +1 -1
  70. package/lib/esm/components/sticky/sticky.js +52 -14
  71. package/lib/esm/components/sticky/sticky.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/components/component.ts +12 -11
  74. package/src/components/datatable/datatable-sort.ts +6 -0
  75. package/src/components/datatable/datatable.ts +90 -81
  76. package/src/components/image-input/image-input.ts +11 -2
  77. package/src/components/image-input/types.ts +1 -0
  78. package/src/components/input/input-group.css +1 -1
  79. package/src/components/input/input.css +1 -1
  80. package/src/components/scrollable/scrollable.css +3 -3
  81. package/src/components/select/combobox.ts +84 -34
  82. package/src/components/select/dropdown.ts +20 -11
  83. package/src/components/select/index.ts +6 -1
  84. package/src/components/select/option.ts +7 -6
  85. package/src/components/select/remote.ts +51 -52
  86. package/src/components/select/search.ts +51 -45
  87. package/src/components/select/select.css +12 -11
  88. package/src/components/select/select.ts +371 -102
  89. package/src/components/select/tags.ts +9 -3
  90. package/src/components/select/templates.ts +1 -4
  91. package/src/components/select/utils.ts +55 -20
  92. package/src/components/select/variants.css +0 -1
  93. package/src/components/sticky/sticky.ts +47 -16
  94. package/src/components/sticky/types.ts +3 -0
  95. package/src/components/table/table.css +1 -1
  96. package/src/components/textarea/textarea.css +1 -1
  97. package/src/components/toast/toast.css +84 -47
  98. package/src/components/toast/types.ts +3 -0
@@ -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,30 +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
- this._select.updateSelectAllButtonState();
147
- });
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
+ });
148
148
  }
149
149
  }
150
150
  }
@@ -229,8 +229,10 @@ export class KTSelectSearch {
229
229
  * This is typically called before applying new filters/highlights.
230
230
  */
231
231
  private _restoreOptionContentsBeforeFilter(): void {
232
- const options = Array.from(this._select.getOptionsElement()) as HTMLElement[];
233
- options.forEach(option => {
232
+ const options = Array.from(
233
+ this._select.getOptionsElement(),
234
+ ) as HTMLElement[];
235
+ options.forEach((option) => {
234
236
  const value = option.getAttribute('data-value');
235
237
  if (value && this._originalOptionContents.has(value)) {
236
238
  const originalContent = this._originalOptionContents.get(value)!;
@@ -295,8 +297,12 @@ export class KTSelectSearch {
295
297
  // Restore original content before filtering, so highlighting is applied fresh.
296
298
  this._restoreOptionContentsBeforeFilter();
297
299
 
298
- const visibleCount = filterOptions(options, query, config, dropdownElement, (count) =>
299
- this._handleNoResults(count),
300
+ const visibleCount = filterOptions(
301
+ options,
302
+ query,
303
+ config,
304
+ dropdownElement,
305
+ (count) => this._handleNoResults(count),
300
306
  );
301
307
 
302
308
  this._select.updateSelectAllButtonState();
@@ -60,7 +60,7 @@
60
60
  }
61
61
  }
62
62
 
63
- .kt-select-search-empty {
63
+ .kt-select-search-empty {
64
64
  @apply px-3.5 py-1 text-sm text-muted-foreground;
65
65
  }
66
66
 
@@ -92,12 +92,13 @@
92
92
  @apply grow flex items-center gap-2.5 py-1.75 px-2.5 rounded-md text-sm text-start cursor-pointer;
93
93
  @apply hover:bg-accent hover:text-accent-foreground;
94
94
 
95
- &[aria-disabled=true],
95
+ &[aria-disabled='true'],
96
96
  &.disabled {
97
97
  @apply opacity-60 pointer-events-none;
98
98
  }
99
99
 
100
- &.selected {}
100
+ &.selected {
101
+ }
101
102
 
102
103
  &.focused,
103
104
  &.hover,
@@ -122,7 +123,7 @@
122
123
  @apply text-muted-foreground overflow-ellipsis truncate;
123
124
  }
124
125
 
125
- .kt-select-display:not([data-multiple=true]) {
126
+ .kt-select-display:not([data-multiple='true']) {
126
127
  @apply w-full overflow-ellipsis truncate;
127
128
  }
128
129
  }
@@ -133,7 +134,7 @@
133
134
  @apply h-8.5 gap-1 ps-3 pe-6 text-[0.8125rem] leading-(--text-sm--line-height) rounded-md;
134
135
  background-position: right 0.5rem center;
135
136
 
136
- &[data-multiple=true] {
137
+ &[data-multiple='true'] {
137
138
  @apply h-auto min-h-8.5 py-1.5 flex-wrap;
138
139
  background-position: right 0.5rem top 0.675rem;
139
140
  }
@@ -143,7 +144,7 @@
143
144
  @apply h-7 gap-1 ps-2.5 pe-5 text-xs rounded-md;
144
145
  background-position: right 0.5rem center;
145
146
 
146
- &[data-multiple=true] {
147
+ &[data-multiple='true'] {
147
148
  @apply h-auto min-h-7;
148
149
  background-position: right 0.5rem top 0.575rem;
149
150
  }
@@ -153,7 +154,7 @@
153
154
  @apply h-10 gap-1.5 ps-4 pe-8 py-1 text-sm rounded-md;
154
155
  background-position: right 0.6rem center;
155
156
 
156
- &[data-multiple=true] {
157
+ &[data-multiple='true'] {
157
158
  @apply h-auto min-h-10 py-2;
158
159
  background-position: right 0.6rem top 0.85rem;
159
160
  }
@@ -166,7 +167,7 @@
166
167
  .kt-select {
167
168
  background-position: left 0.5rem center;
168
169
 
169
- &[data-multiple=true] {
170
+ &[data-multiple='true'] {
170
171
  background-position: left 0.5rem top 0.675rem;
171
172
  }
172
173
  }
@@ -174,7 +175,7 @@
174
175
  .kt-select-sm {
175
176
  background-position: left 0.5rem center;
176
177
 
177
- &[data-multiple=true] {
178
+ &[data-multiple='true'] {
178
179
  background-position: left 0.5rem top 0.575rem;
179
180
  }
180
181
  }
@@ -182,7 +183,7 @@
182
183
  .kt-select-lg {
183
184
  background-position: left 0.75rem center;
184
185
 
185
- &[data-multiple=true] {
186
+ &[data-multiple='true'] {
186
187
  background-position: left 0.75rem top 0.85rem;
187
188
  }
188
189
  }
@@ -235,4 +236,4 @@
235
236
  [data-kt-select-option]:hover & {
236
237
  @slot;
237
238
  }
238
- }
239
+ }