@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.
- package/dist/ktui.js +418 -144
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +139 -31
- package/examples/image-input/file-upload-example.html +189 -0
- package/examples/select/remote-data_.html +5 -0
- package/examples/select/test-optimizations.html +227 -0
- package/examples/select/test-remote-search.html +151 -0
- package/examples/sticky/README.md +158 -0
- package/examples/sticky/debug-sticky.html +144 -0
- package/examples/sticky/test-runner.html +175 -0
- package/examples/sticky/test-sticky-logic.js +369 -0
- package/examples/sticky/test-sticky-positioning.html +386 -0
- package/examples/toast/example.html +52 -0
- package/lib/cjs/components/component.js +5 -3
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +4 -0
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +9 -3
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/image-input/image-input.js +10 -2
- package/lib/cjs/components/image-input/image-input.js.map +1 -1
- package/lib/cjs/components/select/combobox.js +50 -20
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js +4 -2
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/index.js.map +1 -1
- package/lib/cjs/components/select/option.js +2 -1
- package/lib/cjs/components/select/option.js.map +1 -1
- package/lib/cjs/components/select/remote.js +50 -50
- package/lib/cjs/components/select/remote.js.map +1 -1
- package/lib/cjs/components/select/search.js +7 -5
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +199 -33
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js +3 -1
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/select/templates.js.map +1 -1
- package/lib/cjs/components/select/utils.js +23 -10
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +52 -14
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/esm/components/component.js +5 -3
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +4 -0
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +9 -3
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/image-input/image-input.js +10 -2
- package/lib/esm/components/image-input/image-input.js.map +1 -1
- package/lib/esm/components/select/combobox.js +50 -20
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/dropdown.js +4 -2
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/index.js +1 -1
- package/lib/esm/components/select/index.js.map +1 -1
- package/lib/esm/components/select/option.js +2 -1
- package/lib/esm/components/select/option.js.map +1 -1
- package/lib/esm/components/select/remote.js +50 -50
- package/lib/esm/components/select/remote.js.map +1 -1
- package/lib/esm/components/select/search.js +8 -6
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +199 -33
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js +3 -1
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/select/templates.js.map +1 -1
- package/lib/esm/components/select/utils.js +23 -10
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/components/sticky/sticky.js +52 -14
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/package.json +1 -1
- package/src/components/component.ts +12 -11
- package/src/components/datatable/datatable-sort.ts +6 -0
- package/src/components/datatable/datatable.ts +90 -81
- package/src/components/image-input/image-input.ts +11 -2
- package/src/components/image-input/types.ts +1 -0
- package/src/components/input/input-group.css +1 -1
- package/src/components/input/input.css +1 -1
- package/src/components/scrollable/scrollable.css +3 -3
- package/src/components/select/combobox.ts +84 -34
- package/src/components/select/dropdown.ts +20 -11
- package/src/components/select/index.ts +6 -1
- package/src/components/select/option.ts +7 -6
- package/src/components/select/remote.ts +51 -52
- package/src/components/select/search.ts +51 -45
- package/src/components/select/select.css +12 -11
- package/src/components/select/select.ts +371 -102
- package/src/components/select/tags.ts +9 -3
- package/src/components/select/templates.ts +1 -4
- package/src/components/select/utils.ts +55 -20
- package/src/components/select/variants.css +0 -1
- package/src/components/sticky/sticky.ts +47 -16
- package/src/components/sticky/types.ts +3 -0
- package/src/components/table/table.css +1 -1
- package/src/components/textarea/textarea.css +1 -1
- package/src/components/toast/toast.css +84 -47
- package/src/components/toast/types.ts +3 -0
|
@@ -39,6 +39,9 @@ export class KTSelect extends KTComponent {
|
|
|
39
39
|
private _searchInputElement: HTMLInputElement | null;
|
|
40
40
|
private _options: NodeListOf<HTMLElement>;
|
|
41
41
|
|
|
42
|
+
// Cached DOM references for performance
|
|
43
|
+
private _optionsContainer: HTMLElement | null = null;
|
|
44
|
+
|
|
42
45
|
// State
|
|
43
46
|
private _dropdownIsOpen: boolean = false;
|
|
44
47
|
private _state: KTSelectState;
|
|
@@ -104,6 +107,11 @@ export class KTSelect extends KTComponent {
|
|
|
104
107
|
if (this._config.debug)
|
|
105
108
|
console.log('Initializing remote data with URL:', this._config.dataUrl);
|
|
106
109
|
|
|
110
|
+
// For remote data, we need to create the HTML structure first
|
|
111
|
+
// so that the component can be properly initialized
|
|
112
|
+
this._createHtmlStructure();
|
|
113
|
+
this._setupElementReferences();
|
|
114
|
+
|
|
107
115
|
// Show loading state
|
|
108
116
|
this._renderLoadingState();
|
|
109
117
|
|
|
@@ -125,7 +133,12 @@ export class KTSelect extends KTComponent {
|
|
|
125
133
|
|
|
126
134
|
if (this._config.debug)
|
|
127
135
|
console.log('Generating options HTML from remote data');
|
|
128
|
-
|
|
136
|
+
|
|
137
|
+
// Update the dropdown to show the new options
|
|
138
|
+
this._updateDropdownWithNewOptions();
|
|
139
|
+
|
|
140
|
+
// Complete the component setup with the fetched data
|
|
141
|
+
this._completeRemoteSetup();
|
|
129
142
|
|
|
130
143
|
// Add pagination "Load More" button if needed
|
|
131
144
|
if (this._config.pagination && this._remoteModule.hasMorePages()) {
|
|
@@ -156,6 +169,145 @@ export class KTSelect extends KTComponent {
|
|
|
156
169
|
options.forEach((option) => option.remove());
|
|
157
170
|
}
|
|
158
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Unified method to render options in dropdown - eliminates code duplication
|
|
174
|
+
*/
|
|
175
|
+
private _renderOptionsInDropdown(
|
|
176
|
+
optionsData: KTSelectOptionData[] | HTMLOptionElement[],
|
|
177
|
+
clearContainer: boolean = true,
|
|
178
|
+
): void {
|
|
179
|
+
if (!this._dropdownContentElement) return;
|
|
180
|
+
|
|
181
|
+
// Use cached options container for better performance
|
|
182
|
+
const optionsContainer =
|
|
183
|
+
this._optionsContainer ||
|
|
184
|
+
this._dropdownContentElement.querySelector('[data-kt-select-options]');
|
|
185
|
+
if (!optionsContainer) return;
|
|
186
|
+
|
|
187
|
+
// Clear container if requested
|
|
188
|
+
if (clearContainer) {
|
|
189
|
+
optionsContainer.innerHTML = '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Use DocumentFragment for efficient DOM manipulation
|
|
193
|
+
const fragment = document.createDocumentFragment();
|
|
194
|
+
|
|
195
|
+
// Process options data
|
|
196
|
+
optionsData.forEach((optionData) => {
|
|
197
|
+
let optionElement: HTMLOptionElement;
|
|
198
|
+
|
|
199
|
+
// Handle different input types
|
|
200
|
+
if (optionData instanceof HTMLOptionElement) {
|
|
201
|
+
// Skip empty placeholder options
|
|
202
|
+
if (optionData.value === '' && optionData.textContent.trim() === '') {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
optionElement = optionData;
|
|
206
|
+
} else {
|
|
207
|
+
// Handle KTSelectOptionData objects - cast to ensure type safety
|
|
208
|
+
const dataItem = optionData as KTSelectOptionData;
|
|
209
|
+
optionElement = document.createElement('option');
|
|
210
|
+
optionElement.value = dataItem.id || '';
|
|
211
|
+
optionElement.textContent = dataItem.title || '';
|
|
212
|
+
|
|
213
|
+
if (dataItem.selected) {
|
|
214
|
+
optionElement.setAttribute('selected', 'selected');
|
|
215
|
+
}
|
|
216
|
+
if (dataItem.disabled) {
|
|
217
|
+
optionElement.setAttribute('disabled', 'disabled');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create KTSelectOption instance for proper rendering
|
|
222
|
+
const selectOption = new KTSelectOption(optionElement, this._config);
|
|
223
|
+
const renderedOption = selectOption.render();
|
|
224
|
+
|
|
225
|
+
// Add to fragment for batch DOM operation
|
|
226
|
+
fragment.appendChild(renderedOption);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Batch append all options at once
|
|
230
|
+
optionsContainer.appendChild(fragment);
|
|
231
|
+
|
|
232
|
+
// Update options NodeList
|
|
233
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
234
|
+
'[data-kt-select-option]',
|
|
235
|
+
) as NodeListOf<HTMLElement>;
|
|
236
|
+
|
|
237
|
+
if (this._config.debug) {
|
|
238
|
+
console.log(`Rendered ${optionsData.length} options in dropdown`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update dropdown with new options from the original select element
|
|
244
|
+
*/
|
|
245
|
+
private _updateDropdownWithNewOptions() {
|
|
246
|
+
// Get all options from the original select element
|
|
247
|
+
const options = Array.from(this._element.querySelectorAll('option'));
|
|
248
|
+
|
|
249
|
+
// Use unified renderer
|
|
250
|
+
this._renderOptionsInDropdown(options, true);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Complete the setup for remote data after HTML structure is created
|
|
255
|
+
*/
|
|
256
|
+
private _completeRemoteSetup() {
|
|
257
|
+
// Initialize options
|
|
258
|
+
this._preSelectOptions(this._element);
|
|
259
|
+
|
|
260
|
+
// Apply disabled state if needed
|
|
261
|
+
this._applyInitialDisabledState();
|
|
262
|
+
|
|
263
|
+
// Initialize search if enabled
|
|
264
|
+
if (this._config.enableSearch) {
|
|
265
|
+
this._initializeSearchModule();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Initialize combobox if enabled
|
|
269
|
+
if (this._config.combobox) {
|
|
270
|
+
this._comboboxModule = new KTSelectCombobox(this);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Initialize tags if enabled
|
|
274
|
+
if (this._config.tags) {
|
|
275
|
+
this._tagsModule = new KTSelectTags(this);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Initialize focus manager after dropdown element is created
|
|
279
|
+
this._focusManager = new FocusManager(
|
|
280
|
+
this._dropdownContentElement,
|
|
281
|
+
'[data-kt-select-option]',
|
|
282
|
+
this._config,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Initialize dropdown module after all elements are created
|
|
286
|
+
this._dropdownModule = new KTSelectDropdown(
|
|
287
|
+
this._wrapperElement,
|
|
288
|
+
this._displayElement,
|
|
289
|
+
this._dropdownContentElement,
|
|
290
|
+
this._config,
|
|
291
|
+
this, // Pass the KTSelect instance to KTSelectDropdown
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Update display and set ARIA attributes
|
|
295
|
+
this._updateDisplayAndAriaAttributes();
|
|
296
|
+
this.updateSelectedOptionDisplay();
|
|
297
|
+
this._setAriaAttributes();
|
|
298
|
+
|
|
299
|
+
// Update select all button state
|
|
300
|
+
this.updateSelectAllButtonState();
|
|
301
|
+
|
|
302
|
+
// Focus the first selected option or first option if nothing selected
|
|
303
|
+
this._focusSelectedOption();
|
|
304
|
+
|
|
305
|
+
// Attach event listeners after all modules are initialized
|
|
306
|
+
this._attachEventListeners();
|
|
307
|
+
|
|
308
|
+
this._observeNativeSelect();
|
|
309
|
+
}
|
|
310
|
+
|
|
159
311
|
/**
|
|
160
312
|
* Helper to show a dropdown message (error, loading, noResults)
|
|
161
313
|
*/
|
|
@@ -174,19 +326,22 @@ export class KTSelect extends KTComponent {
|
|
|
174
326
|
|
|
175
327
|
switch (type) {
|
|
176
328
|
case 'error':
|
|
177
|
-
optionsContainer.appendChild(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
329
|
+
optionsContainer.appendChild(
|
|
330
|
+
defaultTemplates.error({
|
|
331
|
+
...this._config,
|
|
332
|
+
errorMessage: message,
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
181
335
|
break;
|
|
182
336
|
case 'loading':
|
|
183
|
-
optionsContainer.appendChild(
|
|
184
|
-
this._config,
|
|
185
|
-
|
|
186
|
-
));
|
|
337
|
+
optionsContainer.appendChild(
|
|
338
|
+
defaultTemplates.loading(this._config, message || 'Loading...'),
|
|
339
|
+
);
|
|
187
340
|
break;
|
|
188
341
|
case 'empty':
|
|
189
|
-
optionsContainer.appendChild(
|
|
342
|
+
optionsContainer.appendChild(
|
|
343
|
+
defaultTemplates.searchEmpty(this._config),
|
|
344
|
+
);
|
|
190
345
|
break;
|
|
191
346
|
}
|
|
192
347
|
}
|
|
@@ -436,18 +591,22 @@ export class KTSelect extends KTComponent {
|
|
|
436
591
|
// Move classes from original select to wrapper and display elements
|
|
437
592
|
if (this._element.classList.length > 0) {
|
|
438
593
|
const originalClasses = Array.from(this._element.classList);
|
|
439
|
-
const displaySpecificClasses = [
|
|
594
|
+
const displaySpecificClasses = [
|
|
595
|
+
'kt-select',
|
|
596
|
+
'kt-select-sm',
|
|
597
|
+
'kt-select-lg',
|
|
598
|
+
];
|
|
440
599
|
|
|
441
600
|
const classesForWrapper = originalClasses.filter(
|
|
442
|
-
(className) => !displaySpecificClasses.includes(className)
|
|
601
|
+
(className) => !displaySpecificClasses.includes(className),
|
|
443
602
|
);
|
|
444
603
|
if (classesForWrapper.length > 0) {
|
|
445
604
|
wrapperElement.classList.add(...classesForWrapper);
|
|
446
605
|
}
|
|
447
606
|
|
|
448
607
|
// Move display-specific classes to display element
|
|
449
|
-
const classesForDisplay = originalClasses.filter(
|
|
450
|
-
|
|
608
|
+
const classesForDisplay = originalClasses.filter((className) =>
|
|
609
|
+
displaySpecificClasses.includes(className),
|
|
451
610
|
);
|
|
452
611
|
if (classesForDisplay.length > 0) {
|
|
453
612
|
displayElement.classList.add(...classesForDisplay);
|
|
@@ -477,7 +636,7 @@ export class KTSelect extends KTComponent {
|
|
|
477
636
|
// Create options container using template
|
|
478
637
|
const optionsContainer = defaultTemplates.options(this._config);
|
|
479
638
|
|
|
480
|
-
// Add each option directly to the container
|
|
639
|
+
// Add each option directly to the container (only if options exist)
|
|
481
640
|
options.forEach((optionElement) => {
|
|
482
641
|
// Skip empty placeholder options (only if BOTH value AND text are empty)
|
|
483
642
|
// This allows options with empty value but visible text to display in dropdown
|
|
@@ -513,6 +672,14 @@ export class KTSelect extends KTComponent {
|
|
|
513
672
|
private _setupElementReferences() {
|
|
514
673
|
this._wrapperElement = this._element.nextElementSibling as HTMLElement;
|
|
515
674
|
|
|
675
|
+
// Safety check - ensure wrapper element exists
|
|
676
|
+
if (!this._wrapperElement) {
|
|
677
|
+
console.error(
|
|
678
|
+
'KTSelect: Wrapper element not found. HTML structure may not be created properly.',
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
516
683
|
// Get display element
|
|
517
684
|
this._displayElement = this._wrapperElement.querySelector(
|
|
518
685
|
`[data-kt-select-display]`,
|
|
@@ -524,8 +691,11 @@ export class KTSelect extends KTComponent {
|
|
|
524
691
|
) as HTMLElement;
|
|
525
692
|
|
|
526
693
|
if (!this._dropdownContentElement) {
|
|
527
|
-
console.
|
|
528
|
-
|
|
694
|
+
console.error(
|
|
695
|
+
'KTSelect: Dropdown content element not found',
|
|
696
|
+
this._wrapperElement,
|
|
697
|
+
);
|
|
698
|
+
return;
|
|
529
699
|
}
|
|
530
700
|
|
|
531
701
|
// Get search input element - this is used for the search functionality
|
|
@@ -542,6 +712,11 @@ export class KTSelect extends KTComponent {
|
|
|
542
712
|
'[data-kt-select-select-all]',
|
|
543
713
|
) as HTMLElement;
|
|
544
714
|
|
|
715
|
+
// Cache the options container for performance
|
|
716
|
+
this._optionsContainer = this._dropdownContentElement.querySelector(
|
|
717
|
+
'[data-kt-select-options]',
|
|
718
|
+
) as HTMLElement;
|
|
719
|
+
|
|
545
720
|
this._options = this._wrapperElement.querySelectorAll(
|
|
546
721
|
`[data-kt-select-option]`,
|
|
547
722
|
) as NodeListOf<HTMLElement>;
|
|
@@ -562,7 +737,8 @@ export class KTSelect extends KTComponent {
|
|
|
562
737
|
);
|
|
563
738
|
|
|
564
739
|
if (this._selectAllButton) {
|
|
565
|
-
this._selectAllButtonToggle =
|
|
740
|
+
this._selectAllButtonToggle =
|
|
741
|
+
this._selectAllButton.querySelector('button');
|
|
566
742
|
if (this._selectAllButtonToggle) {
|
|
567
743
|
this._eventManager.addListener(
|
|
568
744
|
this._selectAllButtonToggle,
|
|
@@ -575,7 +751,10 @@ export class KTSelect extends KTComponent {
|
|
|
575
751
|
// Attach centralized keyboard handler to the wrapper element.
|
|
576
752
|
// Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
|
|
577
753
|
if (this._wrapperElement) {
|
|
578
|
-
this._wrapperElement.addEventListener(
|
|
754
|
+
this._wrapperElement.addEventListener(
|
|
755
|
+
'keydown',
|
|
756
|
+
this._handleKeyboardEvent.bind(this),
|
|
757
|
+
);
|
|
579
758
|
}
|
|
580
759
|
}
|
|
581
760
|
|
|
@@ -666,9 +845,7 @@ export class KTSelect extends KTComponent {
|
|
|
666
845
|
|
|
667
846
|
// Log the extracted values for debugging
|
|
668
847
|
if (this._config.debug)
|
|
669
|
-
console.log(
|
|
670
|
-
`Option: value=${value}, label=${label}`,
|
|
671
|
-
);
|
|
848
|
+
console.log(`Option: value=${value}, label=${label}`);
|
|
672
849
|
|
|
673
850
|
// Set option attributes
|
|
674
851
|
optionElement.value = value;
|
|
@@ -743,7 +920,8 @@ export class KTSelect extends KTComponent {
|
|
|
743
920
|
*/
|
|
744
921
|
public openDropdown() {
|
|
745
922
|
if (this._config.disabled) {
|
|
746
|
-
if (this._config.debug)
|
|
923
|
+
if (this._config.debug)
|
|
924
|
+
console.log('openDropdown: select is disabled, not opening');
|
|
747
925
|
return;
|
|
748
926
|
}
|
|
749
927
|
if (this._config.debug)
|
|
@@ -877,7 +1055,8 @@ export class KTSelect extends KTComponent {
|
|
|
877
1055
|
private _selectOption(value: string) {
|
|
878
1056
|
// Prevent selection if the option is disabled (in dropdown or original select)
|
|
879
1057
|
if (this._isOptionDisabled(value)) {
|
|
880
|
-
if (this._config.debug)
|
|
1058
|
+
if (this._config.debug)
|
|
1059
|
+
console.log('_selectOption: Option is disabled, ignoring selection');
|
|
881
1060
|
return;
|
|
882
1061
|
}
|
|
883
1062
|
|
|
@@ -945,7 +1124,9 @@ export class KTSelect extends KTComponent {
|
|
|
945
1124
|
// Guard against valueDisplayEl being null due to template modifications
|
|
946
1125
|
if (!valueDisplayEl) {
|
|
947
1126
|
if (this._config.debug) {
|
|
948
|
-
console.warn(
|
|
1127
|
+
console.warn(
|
|
1128
|
+
'KTSelect: Value display element is null. Cannot update display or placeholder. Check template for [data-kt-select-value].',
|
|
1129
|
+
);
|
|
949
1130
|
}
|
|
950
1131
|
return; // Nothing to display on if the element is missing
|
|
951
1132
|
}
|
|
@@ -968,7 +1149,9 @@ export class KTSelect extends KTComponent {
|
|
|
968
1149
|
// Tags are not enabled AND options are selected: render normal text display.
|
|
969
1150
|
let content = '';
|
|
970
1151
|
if (this._config.displayTemplate) {
|
|
971
|
-
content = this.renderDisplayTemplateForSelected(
|
|
1152
|
+
content = this.renderDisplayTemplateForSelected(
|
|
1153
|
+
this.getSelectedOptions(),
|
|
1154
|
+
);
|
|
972
1155
|
} else {
|
|
973
1156
|
content = this.getSelectedOptionsText();
|
|
974
1157
|
}
|
|
@@ -982,9 +1165,9 @@ export class KTSelect extends KTComponent {
|
|
|
982
1165
|
* Check if an option was originally disabled in the HTML
|
|
983
1166
|
*/
|
|
984
1167
|
private _isOptionOriginallyDisabled(value: string): boolean {
|
|
985
|
-
const originalOption = Array.from(
|
|
986
|
-
(
|
|
987
|
-
) as HTMLOptionElement;
|
|
1168
|
+
const originalOption = Array.from(
|
|
1169
|
+
this._element.querySelectorAll('option'),
|
|
1170
|
+
).find((opt) => opt.value === value) as HTMLOptionElement;
|
|
988
1171
|
return originalOption ? originalOption.disabled : false;
|
|
989
1172
|
}
|
|
990
1173
|
|
|
@@ -1011,7 +1194,8 @@ export class KTSelect extends KTComponent {
|
|
|
1011
1194
|
if (!optionValue) return;
|
|
1012
1195
|
|
|
1013
1196
|
const isSelected = selectedValues.includes(optionValue);
|
|
1014
|
-
const isOriginallyDisabled =
|
|
1197
|
+
const isOriginallyDisabled =
|
|
1198
|
+
this._isOptionOriginallyDisabled(optionValue);
|
|
1015
1199
|
|
|
1016
1200
|
if (isSelected) {
|
|
1017
1201
|
option.classList.add('selected');
|
|
@@ -1146,7 +1330,10 @@ export class KTSelect extends KTComponent {
|
|
|
1146
1330
|
|
|
1147
1331
|
// If in single-select mode and the clicked option is already selected, just close the dropdown.
|
|
1148
1332
|
if (!this._config.multiple && this._state.isSelected(optionValue)) {
|
|
1149
|
-
if (this._config.debug)
|
|
1333
|
+
if (this._config.debug)
|
|
1334
|
+
console.log(
|
|
1335
|
+
'Single select mode: clicked already selected option. Closing dropdown.',
|
|
1336
|
+
);
|
|
1150
1337
|
this.closeDropdown();
|
|
1151
1338
|
return;
|
|
1152
1339
|
}
|
|
@@ -1303,24 +1490,31 @@ export class KTSelect extends KTComponent {
|
|
|
1303
1490
|
public toggleSelection(value: string): void {
|
|
1304
1491
|
// Prevent selection if the option is disabled (in dropdown or original select)
|
|
1305
1492
|
if (this._isOptionDisabled(value)) {
|
|
1306
|
-
if (this._config.debug)
|
|
1493
|
+
if (this._config.debug)
|
|
1494
|
+
console.log('toggleSelection: Option is disabled, ignoring selection');
|
|
1307
1495
|
return;
|
|
1308
1496
|
}
|
|
1309
1497
|
|
|
1310
1498
|
// Get current selection state
|
|
1311
1499
|
const isSelected = this._state.isSelected(value);
|
|
1312
1500
|
if (this._config.debug)
|
|
1313
|
-
console.log(
|
|
1501
|
+
console.log(
|
|
1502
|
+
`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}`,
|
|
1503
|
+
);
|
|
1314
1504
|
|
|
1315
1505
|
// If already selected in single select mode, do nothing (can't deselect in single select)
|
|
1316
1506
|
if (isSelected && !this._config.multiple) {
|
|
1317
1507
|
if (this._config.debug)
|
|
1318
|
-
console.log(
|
|
1508
|
+
console.log(
|
|
1509
|
+
'Early return from toggleSelection - already selected in single select mode',
|
|
1510
|
+
);
|
|
1319
1511
|
return;
|
|
1320
1512
|
}
|
|
1321
1513
|
|
|
1322
1514
|
if (this._config.debug)
|
|
1323
|
-
console.log(
|
|
1515
|
+
console.log(
|
|
1516
|
+
`Toggling selection for option: ${value}, currently selected: ${isSelected}`,
|
|
1517
|
+
);
|
|
1324
1518
|
|
|
1325
1519
|
// Ensure any search input is cleared when selection changes
|
|
1326
1520
|
if (this._searchModule) {
|
|
@@ -1357,11 +1551,15 @@ export class KTSelect extends KTComponent {
|
|
|
1357
1551
|
// For multiple select mode, keep the dropdown open to allow multiple selections
|
|
1358
1552
|
if (!this._config.multiple) {
|
|
1359
1553
|
if (this._config.debug)
|
|
1360
|
-
console.log(
|
|
1554
|
+
console.log(
|
|
1555
|
+
'About to call closeDropdown() for single select mode - always close after selection',
|
|
1556
|
+
);
|
|
1361
1557
|
this.closeDropdown();
|
|
1362
1558
|
} else {
|
|
1363
1559
|
if (this._config.debug)
|
|
1364
|
-
console.log(
|
|
1560
|
+
console.log(
|
|
1561
|
+
'Multiple select mode - keeping dropdown open for additional selections',
|
|
1562
|
+
);
|
|
1365
1563
|
// Don't close dropdown in multiple select mode to allow multiple selections
|
|
1366
1564
|
this.updateSelectAllButtonState();
|
|
1367
1565
|
}
|
|
@@ -1467,6 +1665,8 @@ export class KTSelect extends KTComponent {
|
|
|
1467
1665
|
|
|
1468
1666
|
// Check if the query is long enough
|
|
1469
1667
|
if (query.length < (this._config.searchMinLength || 0)) {
|
|
1668
|
+
// Restore original options if query is too short
|
|
1669
|
+
this._restoreOriginalOptions();
|
|
1470
1670
|
return;
|
|
1471
1671
|
}
|
|
1472
1672
|
|
|
@@ -1507,7 +1707,7 @@ export class KTSelect extends KTComponent {
|
|
|
1507
1707
|
console.error('Error fetching search results:', error);
|
|
1508
1708
|
this._renderSearchErrorState(
|
|
1509
1709
|
this._remoteModule.getErrorMessage() ||
|
|
1510
|
-
|
|
1710
|
+
'Failed to load search results',
|
|
1511
1711
|
);
|
|
1512
1712
|
});
|
|
1513
1713
|
}, this._config.searchDebounce || 300);
|
|
@@ -1540,6 +1740,42 @@ export class KTSelect extends KTComponent {
|
|
|
1540
1740
|
*/
|
|
1541
1741
|
private _renderSearchErrorState(message: string) {
|
|
1542
1742
|
this._showDropdownMessage('error', message);
|
|
1743
|
+
|
|
1744
|
+
// Restore original options after error with a delay
|
|
1745
|
+
setTimeout(() => {
|
|
1746
|
+
this._restoreOriginalOptions();
|
|
1747
|
+
}, 2000);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Restore original options when search is cleared
|
|
1752
|
+
*/
|
|
1753
|
+
private _restoreOriginalOptions() {
|
|
1754
|
+
if (!this._dropdownContentElement || !this._originalOptionsHtml) return;
|
|
1755
|
+
|
|
1756
|
+
// Use cached options container for better performance
|
|
1757
|
+
const optionsContainer =
|
|
1758
|
+
this._optionsContainer ||
|
|
1759
|
+
this._dropdownContentElement.querySelector('[data-kt-select-options]');
|
|
1760
|
+
if (!optionsContainer) return;
|
|
1761
|
+
|
|
1762
|
+
// Restore original options
|
|
1763
|
+
optionsContainer.innerHTML = this._originalOptionsHtml;
|
|
1764
|
+
|
|
1765
|
+
// Update options NodeList
|
|
1766
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
1767
|
+
'[data-kt-select-option]',
|
|
1768
|
+
) as NodeListOf<HTMLElement>;
|
|
1769
|
+
|
|
1770
|
+
// Refresh search module
|
|
1771
|
+
if (this._searchModule) {
|
|
1772
|
+
this._searchModule.refreshAfterSearch();
|
|
1773
|
+
}
|
|
1774
|
+
this.updateSelectAllButtonState();
|
|
1775
|
+
|
|
1776
|
+
if (this._config.debug) {
|
|
1777
|
+
console.log('Restored original options after search clear');
|
|
1778
|
+
}
|
|
1543
1779
|
}
|
|
1544
1780
|
|
|
1545
1781
|
/**
|
|
@@ -1549,40 +1785,27 @@ export class KTSelect extends KTComponent {
|
|
|
1549
1785
|
private _updateSearchResults(items: KTSelectOptionData[]) {
|
|
1550
1786
|
if (!this._dropdownContentElement) return;
|
|
1551
1787
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1788
|
+
// Use cached options container for better performance
|
|
1789
|
+
const optionsContainer =
|
|
1790
|
+
this._optionsContainer ||
|
|
1791
|
+
this._dropdownContentElement.querySelector('[data-kt-select-options]');
|
|
1555
1792
|
if (!optionsContainer) return;
|
|
1556
1793
|
|
|
1557
|
-
//
|
|
1558
|
-
optionsContainer.innerHTML = '';
|
|
1559
|
-
|
|
1794
|
+
// Handle empty results
|
|
1560
1795
|
if (items.length === 0) {
|
|
1561
|
-
|
|
1796
|
+
optionsContainer.innerHTML = '';
|
|
1562
1797
|
const noResultsElement = defaultTemplates.searchEmpty(this._config);
|
|
1563
1798
|
optionsContainer.appendChild(noResultsElement);
|
|
1564
1799
|
return;
|
|
1565
1800
|
}
|
|
1566
1801
|
|
|
1567
|
-
//
|
|
1568
|
-
|
|
1569
|
-
// Create option for the original select
|
|
1570
|
-
const selectOption = document.createElement('option');
|
|
1571
|
-
selectOption.value = item.id;
|
|
1572
|
-
|
|
1573
|
-
// Add to dropdown container
|
|
1574
|
-
optionsContainer.appendChild(selectOption);
|
|
1575
|
-
});
|
|
1802
|
+
// Use unified renderer for search results
|
|
1803
|
+
this._renderOptionsInDropdown(items, true);
|
|
1576
1804
|
|
|
1577
1805
|
// Add pagination "Load More" button if needed
|
|
1578
1806
|
if (this._config.pagination && this._remoteModule.hasMorePages()) {
|
|
1579
1807
|
this._addLoadMoreButton();
|
|
1580
1808
|
}
|
|
1581
|
-
|
|
1582
|
-
// Update options NodeList
|
|
1583
|
-
this._options = this._wrapperElement.querySelectorAll(
|
|
1584
|
-
`[data-kt-select-option]`,
|
|
1585
|
-
) as NodeListOf<HTMLElement>;
|
|
1586
1809
|
}
|
|
1587
1810
|
|
|
1588
1811
|
/**
|
|
@@ -1595,10 +1818,14 @@ export class KTSelect extends KTComponent {
|
|
|
1595
1818
|
public getSelectedOptionsText(): string {
|
|
1596
1819
|
const selectedValues = this.getSelectedOptions();
|
|
1597
1820
|
const displaySeparator = this._config.displaySeparator || ', ';
|
|
1598
|
-
const texts = selectedValues
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1821
|
+
const texts = selectedValues
|
|
1822
|
+
.map((value) => {
|
|
1823
|
+
const option = Array.from(this._options).find(
|
|
1824
|
+
(opt) => opt.getAttribute('data-value') === value,
|
|
1825
|
+
);
|
|
1826
|
+
return option?.getAttribute('data-text') || '';
|
|
1827
|
+
})
|
|
1828
|
+
.filter(Boolean);
|
|
1602
1829
|
return texts.join(displaySeparator);
|
|
1603
1830
|
}
|
|
1604
1831
|
|
|
@@ -1607,12 +1834,15 @@ export class KTSelect extends KTComponent {
|
|
|
1607
1834
|
*/
|
|
1608
1835
|
private _isOptionDisabled(value: string): boolean {
|
|
1609
1836
|
const dropdownOption = Array.from(this._options).find(
|
|
1610
|
-
(opt) => opt.getAttribute('data-value') === value
|
|
1837
|
+
(opt) => opt.getAttribute('data-value') === value,
|
|
1611
1838
|
);
|
|
1612
|
-
const isDropdownDisabled =
|
|
1613
|
-
|
|
1614
|
-
(
|
|
1615
|
-
|
|
1839
|
+
const isDropdownDisabled =
|
|
1840
|
+
dropdownOption &&
|
|
1841
|
+
(dropdownOption.classList.contains('disabled') ||
|
|
1842
|
+
dropdownOption.getAttribute('aria-disabled') === 'true');
|
|
1843
|
+
const selectOption = Array.from(
|
|
1844
|
+
this._element.querySelectorAll('option'),
|
|
1845
|
+
).find((opt) => opt.value === value) as HTMLOptionElement;
|
|
1616
1846
|
const isNativeDisabled = selectOption && selectOption.disabled;
|
|
1617
1847
|
return Boolean(isDropdownDisabled || isNativeDisabled);
|
|
1618
1848
|
}
|
|
@@ -1636,9 +1866,16 @@ export class KTSelect extends KTComponent {
|
|
|
1636
1866
|
if (event.target === this._searchInputElement) {
|
|
1637
1867
|
// Allow navigation keys like ArrowDown, ArrowUp, Escape, Enter (for search/selection) to be handled by the logic below.
|
|
1638
1868
|
// For other keys (characters, space, backspace, delete), let the input field process them.
|
|
1639
|
-
if (
|
|
1640
|
-
|
|
1641
|
-
|
|
1869
|
+
if (
|
|
1870
|
+
event.key !== 'ArrowDown' &&
|
|
1871
|
+
event.key !== 'ArrowUp' &&
|
|
1872
|
+
event.key !== 'Escape' &&
|
|
1873
|
+
event.key !== 'Enter' &&
|
|
1874
|
+
event.key !== 'Tab' &&
|
|
1875
|
+
event.key !== 'Home' &&
|
|
1876
|
+
event.key !== 'End' &&
|
|
1877
|
+
event.key !== ' '
|
|
1878
|
+
) {
|
|
1642
1879
|
// If it's a character key and we are NOT type-to-searching (because search has focus)
|
|
1643
1880
|
// then let the input field handle it for its own value.
|
|
1644
1881
|
// The search module's 'input' event will handle filtering based on the input's value.
|
|
@@ -1653,11 +1890,16 @@ export class KTSelect extends KTComponent {
|
|
|
1653
1890
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1654
1891
|
|
|
1655
1892
|
// Type-to-search: only for single char keys, when search input does not have focus
|
|
1656
|
-
if (
|
|
1893
|
+
if (
|
|
1894
|
+
event.key.length === 1 &&
|
|
1895
|
+
!event.repeat &&
|
|
1896
|
+
!event.key.match(/\s/) &&
|
|
1897
|
+
document.activeElement !== this._searchInputElement
|
|
1898
|
+
) {
|
|
1657
1899
|
buffer.push(event.key);
|
|
1658
1900
|
const str = buffer.getBuffer();
|
|
1659
1901
|
if (isOpen) {
|
|
1660
|
-
|
|
1902
|
+
focusManager.focusByString(str);
|
|
1661
1903
|
} else {
|
|
1662
1904
|
// If closed, type-to-search could potentially open and select.
|
|
1663
1905
|
// For now, let's assume it only works when open or opens it first.
|
|
@@ -1698,8 +1940,15 @@ export class KTSelect extends KTComponent {
|
|
|
1698
1940
|
if (focusedOptionEl) {
|
|
1699
1941
|
const val = focusedOptionEl.dataset.value;
|
|
1700
1942
|
// If single select, and the item is already selected, just close.
|
|
1701
|
-
if (
|
|
1702
|
-
|
|
1943
|
+
if (
|
|
1944
|
+
val !== undefined &&
|
|
1945
|
+
!this._config.multiple &&
|
|
1946
|
+
this._state.isSelected(val)
|
|
1947
|
+
) {
|
|
1948
|
+
if (this._config.debug)
|
|
1949
|
+
console.log(
|
|
1950
|
+
'Enter on already selected item in single-select mode. Closing.',
|
|
1951
|
+
);
|
|
1703
1952
|
this.closeDropdown();
|
|
1704
1953
|
event.preventDefault();
|
|
1705
1954
|
break;
|
|
@@ -1737,30 +1986,39 @@ export class KTSelect extends KTComponent {
|
|
|
1737
1986
|
}
|
|
1738
1987
|
|
|
1739
1988
|
public renderDisplayTemplateForSelected(selectedValues: string[]): string {
|
|
1740
|
-
const optionsConfig = this._config.optionsConfig as any || {};
|
|
1989
|
+
const optionsConfig = (this._config.optionsConfig as any) || {};
|
|
1741
1990
|
const displaySeparator = this._config.displaySeparator || ', ';
|
|
1742
|
-
const contentArray = Array.from(
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1991
|
+
const contentArray = Array.from(
|
|
1992
|
+
new Set(
|
|
1993
|
+
selectedValues
|
|
1994
|
+
.map((value) => {
|
|
1995
|
+
const option = Array.from(this._options).find(
|
|
1996
|
+
(opt) => opt.getAttribute('data-value') === value,
|
|
1997
|
+
);
|
|
1998
|
+
if (!option) return '';
|
|
1999
|
+
|
|
2000
|
+
let displayTemplate = this._config.displayTemplate;
|
|
2001
|
+
const text = option.getAttribute('data-text') || '';
|
|
2002
|
+
|
|
2003
|
+
// Replace all {{varname}} in option.innerHTML with values from _config
|
|
2004
|
+
Object.entries(optionsConfig[value] || {}).forEach(([key, val]) => {
|
|
2005
|
+
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
|
2006
|
+
displayTemplate = displayTemplate.replace(
|
|
2007
|
+
new RegExp(`{{${key}}}`, 'g'),
|
|
2008
|
+
String(val),
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
1756
2012
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2013
|
+
return renderTemplateString(displayTemplate, {
|
|
2014
|
+
selectedCount: selectedValues.length || 0,
|
|
2015
|
+
selectedTexts: this.getSelectedOptionsText() || '',
|
|
2016
|
+
text,
|
|
2017
|
+
});
|
|
2018
|
+
})
|
|
2019
|
+
.filter(Boolean),
|
|
2020
|
+
),
|
|
2021
|
+
);
|
|
1764
2022
|
return contentArray.join(displaySeparator);
|
|
1765
2023
|
}
|
|
1766
2024
|
|
|
@@ -1778,7 +2036,10 @@ export class KTSelect extends KTComponent {
|
|
|
1778
2036
|
if (mutation.type === 'childList') {
|
|
1779
2037
|
// Option(s) added or removed
|
|
1780
2038
|
needsRebuild = true;
|
|
1781
|
-
} else if (
|
|
2039
|
+
} else if (
|
|
2040
|
+
mutation.type === 'attributes' &&
|
|
2041
|
+
mutation.target instanceof HTMLOptionElement
|
|
2042
|
+
) {
|
|
1782
2043
|
if (mutation.attributeName === 'selected') {
|
|
1783
2044
|
needsSelectionSync = true;
|
|
1784
2045
|
}
|
|
@@ -1805,7 +2066,9 @@ export class KTSelect extends KTComponent {
|
|
|
1805
2066
|
private _rebuildOptionsFromNative() {
|
|
1806
2067
|
// Remove and rebuild the custom dropdown options from the native select
|
|
1807
2068
|
if (this._dropdownContentElement) {
|
|
1808
|
-
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
2069
|
+
const optionsContainer = this._dropdownContentElement.querySelector(
|
|
2070
|
+
'[data-kt-select-options]',
|
|
2071
|
+
);
|
|
1809
2072
|
if (optionsContainer) {
|
|
1810
2073
|
optionsContainer.innerHTML = '';
|
|
1811
2074
|
const options = Array.from(this._element.querySelectorAll('option'));
|
|
@@ -1821,7 +2084,9 @@ export class KTSelect extends KTComponent {
|
|
|
1821
2084
|
optionsContainer.appendChild(renderedOption);
|
|
1822
2085
|
});
|
|
1823
2086
|
// Update internal references
|
|
1824
|
-
this._options = this._wrapperElement.querySelectorAll(
|
|
2087
|
+
this._options = this._wrapperElement.querySelectorAll(
|
|
2088
|
+
'[data-kt-select-option]',
|
|
2089
|
+
) as NodeListOf<HTMLElement>;
|
|
1825
2090
|
}
|
|
1826
2091
|
}
|
|
1827
2092
|
// Sync selection after rebuilding
|
|
@@ -1832,8 +2097,12 @@ export class KTSelect extends KTComponent {
|
|
|
1832
2097
|
|
|
1833
2098
|
private _syncSelectionFromNative() {
|
|
1834
2099
|
// Sync internal state from the native select's selected options
|
|
1835
|
-
const selected = Array.from(
|
|
1836
|
-
|
|
2100
|
+
const selected = Array.from(
|
|
2101
|
+
this._element.querySelectorAll('option:checked'),
|
|
2102
|
+
).map((opt) => (opt as HTMLOptionElement).value);
|
|
2103
|
+
this._state.setSelectedOptions(
|
|
2104
|
+
this._config.multiple ? selected : selected[0] || '',
|
|
2105
|
+
);
|
|
1837
2106
|
this.updateSelectedOptionDisplay();
|
|
1838
2107
|
this._updateSelectedOptionClass();
|
|
1839
2108
|
this.updateSelectAllButtonState();
|