@keenthemes/ktui 1.0.12 → 1.0.14
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 +738 -700
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +5824 -0
- package/examples/select/avatar.html +47 -0
- package/examples/select/basic-usage.html +10 -14
- package/examples/select/{test.html → combobox-icons_.html} +13 -48
- package/examples/select/country.html +43 -0
- package/examples/select/description.html +25 -41
- package/examples/select/disable-option.html +10 -16
- package/examples/select/disable-select.html +7 -6
- package/examples/select/icon-multiple.html +23 -31
- package/examples/select/icon.html +20 -30
- package/examples/select/max-selection.html +8 -9
- package/examples/select/modal.html +16 -17
- package/examples/select/multiple.html +11 -13
- package/examples/select/placeholder.html +9 -12
- package/examples/select/search.html +30 -22
- package/examples/select/sizes.html +94 -0
- package/examples/select/template-customization.html +0 -3
- package/lib/cjs/components/component.js +1 -1
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +14 -11
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/select/combobox.js +96 -61
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/config.js +13 -8
- package/lib/cjs/components/select/config.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js +32 -96
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/option.js +53 -20
- package/lib/cjs/components/select/option.js.map +1 -1
- package/lib/cjs/components/select/search.js +146 -97
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +219 -118
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js +0 -26
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/select/templates.js +130 -105
- package/lib/cjs/components/select/templates.js.map +1 -1
- package/lib/cjs/components/select/utils.js +33 -132
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/helpers/dom.js +0 -24
- package/lib/cjs/helpers/dom.js.map +1 -1
- package/lib/esm/components/component.js +1 -1
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +14 -11
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/select/combobox.js +96 -61
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/config.js +13 -8
- package/lib/esm/components/select/config.js.map +1 -1
- package/lib/esm/components/select/dropdown.js +32 -96
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/option.js +53 -20
- package/lib/esm/components/select/option.js.map +1 -1
- package/lib/esm/components/select/search.js +146 -97
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +219 -118
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js +0 -26
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/select/templates.js +130 -105
- package/lib/esm/components/select/templates.js.map +1 -1
- package/lib/esm/components/select/utils.js +32 -130
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/helpers/dom.js +0 -24
- package/lib/esm/helpers/dom.js.map +1 -1
- package/package.json +9 -6
- package/src/components/component.ts +0 -4
- package/src/components/datatable/datatable.ts +14 -11
- package/src/components/input/input.css +1 -1
- package/src/components/scrollable/scrollable.css +9 -5
- package/src/components/select/combobox.ts +98 -87
- package/src/components/select/config.ts +16 -13
- package/src/components/select/dropdown.ts +43 -108
- package/src/components/select/option.ts +44 -25
- package/src/components/select/search.ts +158 -117
- package/src/components/select/select.css +99 -27
- package/src/components/select/select.ts +236 -128
- package/src/components/select/tags.ts +1 -27
- package/src/components/select/templates.ts +191 -132
- package/src/components/select/utils.ts +30 -166
- package/src/components/toast/toast.css +1 -1
- package/src/helpers/dom.ts +0 -30
- package/webpack.config.js +6 -1
- package/examples/select/combobox-icons.html +0 -58
- package/examples/select/icon-description.html +0 -56
- /package/examples/select/{combobox.html → combobox_.html} +0 -0
- /package/examples/select/{remote-data.html → remote-data_.html} +0 -0
- /package/examples/select/{tags-icons.html → tags-icons_.html} +0 -0
- /package/examples/select/{tags-selected.html → tags-selected_.html} +0 -0
- /package/examples/select/{tags.html → tags_.html} +0 -0
|
@@ -37,7 +37,6 @@ export class KTSelect extends KTComponent {
|
|
|
37
37
|
private _displayElement: HTMLElement;
|
|
38
38
|
private _dropdownContentElement: HTMLElement;
|
|
39
39
|
private _searchInputElement: HTMLInputElement | null;
|
|
40
|
-
private _valueDisplayElement: HTMLElement;
|
|
41
40
|
private _options: NodeListOf<HTMLElement>;
|
|
42
41
|
|
|
43
42
|
// State
|
|
@@ -52,6 +51,7 @@ export class KTSelect extends KTComponent {
|
|
|
52
51
|
private _focusManager: FocusManager;
|
|
53
52
|
private _eventManager: EventManager;
|
|
54
53
|
private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
|
|
54
|
+
private _mutationObserver: MutationObserver | null = null;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Constructor: Initializes the select component
|
|
@@ -167,21 +167,23 @@ export class KTSelect extends KTComponent {
|
|
|
167
167
|
);
|
|
168
168
|
if (!optionsContainer) return;
|
|
169
169
|
|
|
170
|
+
// Clear previous messages
|
|
171
|
+
optionsContainer.innerHTML = '';
|
|
172
|
+
|
|
170
173
|
switch (type) {
|
|
171
174
|
case 'error':
|
|
172
|
-
optionsContainer.
|
|
175
|
+
optionsContainer.appendChild(defaultTemplates.error({
|
|
173
176
|
...this._config,
|
|
174
177
|
errorMessage: message,
|
|
175
|
-
});
|
|
178
|
+
}));
|
|
176
179
|
break;
|
|
177
180
|
case 'loading':
|
|
178
|
-
optionsContainer.
|
|
181
|
+
optionsContainer.appendChild(defaultTemplates.loading(
|
|
179
182
|
this._config,
|
|
180
183
|
message || 'Loading...',
|
|
181
|
-
)
|
|
184
|
+
));
|
|
182
185
|
break;
|
|
183
186
|
case 'empty':
|
|
184
|
-
optionsContainer.innerHTML = '';
|
|
185
187
|
optionsContainer.appendChild(defaultTemplates.empty(this._config));
|
|
186
188
|
break;
|
|
187
189
|
}
|
|
@@ -397,6 +399,7 @@ export class KTSelect extends KTComponent {
|
|
|
397
399
|
this._displayElement,
|
|
398
400
|
this._dropdownContentElement,
|
|
399
401
|
this._config,
|
|
402
|
+
this, // Pass the KTSelect instance to KTSelectDropdown
|
|
400
403
|
);
|
|
401
404
|
|
|
402
405
|
// Update display and set ARIA attributes
|
|
@@ -406,14 +409,9 @@ export class KTSelect extends KTComponent {
|
|
|
406
409
|
|
|
407
410
|
// Attach event listeners after all modules are initialized
|
|
408
411
|
this._attachEventListeners();
|
|
409
|
-
}
|
|
410
412
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
*/
|
|
414
|
-
// private _initializeOptionsHtml() {
|
|
415
|
-
// this._generateOptionsHtml(this._element);
|
|
416
|
-
// }
|
|
413
|
+
this._observeNativeSelect();
|
|
414
|
+
}
|
|
417
415
|
|
|
418
416
|
/**
|
|
419
417
|
* Creates the HTML structure for the select component
|
|
@@ -431,7 +429,17 @@ export class KTSelect extends KTComponent {
|
|
|
431
429
|
|
|
432
430
|
// Move classes from original select to display element
|
|
433
431
|
if (this._element.classList.length > 0) {
|
|
434
|
-
|
|
432
|
+
// Exclude kt-select class from being added to the wrapper element
|
|
433
|
+
const classes = Array.from(this._element.classList).filter(
|
|
434
|
+
(className) => className !== 'kt-select',
|
|
435
|
+
);
|
|
436
|
+
wrapperElement.classList.add(...classes);
|
|
437
|
+
|
|
438
|
+
// If element has class kt-select, move it to display element
|
|
439
|
+
if (this._element.classList.contains('kt-select')) {
|
|
440
|
+
displayElement.classList.add('kt-select');
|
|
441
|
+
}
|
|
442
|
+
|
|
435
443
|
this._element.className = '';
|
|
436
444
|
}
|
|
437
445
|
|
|
@@ -480,7 +488,7 @@ export class KTSelect extends KTComponent {
|
|
|
480
488
|
|
|
481
489
|
// Insert after the original element
|
|
482
490
|
this._element.after(wrapperElement);
|
|
483
|
-
this._element.
|
|
491
|
+
this._element.classList.add('hidden');
|
|
484
492
|
}
|
|
485
493
|
|
|
486
494
|
/**
|
|
@@ -514,10 +522,6 @@ export class KTSelect extends KTComponent {
|
|
|
514
522
|
this._searchInputElement = this._displayElement as HTMLInputElement;
|
|
515
523
|
}
|
|
516
524
|
|
|
517
|
-
this._valueDisplayElement = this._wrapperElement.querySelector(
|
|
518
|
-
`[data-kt-select-value]`,
|
|
519
|
-
) as HTMLElement;
|
|
520
|
-
|
|
521
525
|
this._options = this._wrapperElement.querySelectorAll(
|
|
522
526
|
`[data-kt-select-option]`,
|
|
523
527
|
) as NodeListOf<HTMLElement>;
|
|
@@ -537,17 +541,10 @@ export class KTSelect extends KTComponent {
|
|
|
537
541
|
this._handleDropdownOptionClick.bind(this),
|
|
538
542
|
);
|
|
539
543
|
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
// this._handleDropdownClick.bind(this),
|
|
545
|
-
// );
|
|
546
|
-
|
|
547
|
-
// Attach centralized keyboard handler
|
|
548
|
-
const keyboardTarget = this._searchInputElement || this._wrapperElement;
|
|
549
|
-
if (keyboardTarget) {
|
|
550
|
-
keyboardTarget.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
|
|
544
|
+
// Attach centralized keyboard handler to the wrapper element.
|
|
545
|
+
// Events from focusable children like _displayElement or _searchInputElement (if present) will bubble up.
|
|
546
|
+
if (this._wrapperElement) {
|
|
547
|
+
this._wrapperElement.addEventListener('keydown', this._handleKeyboardEvent.bind(this));
|
|
551
548
|
}
|
|
552
549
|
}
|
|
553
550
|
|
|
@@ -729,28 +726,6 @@ export class KTSelect extends KTComponent {
|
|
|
729
726
|
* ========================================================================
|
|
730
727
|
*/
|
|
731
728
|
|
|
732
|
-
/**
|
|
733
|
-
* Toggle dropdown visibility
|
|
734
|
-
* @deprecated
|
|
735
|
-
*/
|
|
736
|
-
public toggleDropdown() {
|
|
737
|
-
if (this._config.disabled) {
|
|
738
|
-
if (this._config.debug) console.log('toggleDropdown: select is disabled, not opening');
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
if (this._config.debug) console.log('toggleDropdown called');
|
|
742
|
-
if (this._dropdownModule) {
|
|
743
|
-
// Always use the dropdown module's state to determine whether to open or close
|
|
744
|
-
if (this._dropdownModule.isOpen()) {
|
|
745
|
-
if (this._config.debug) console.log('Dropdown is open, closing...');
|
|
746
|
-
this.closeDropdown();
|
|
747
|
-
} else {
|
|
748
|
-
if (this._config.debug) console.log('Dropdown is closed, opening...');
|
|
749
|
-
this.openDropdown();
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
729
|
/**
|
|
755
730
|
* Open the dropdown
|
|
756
731
|
*/
|
|
@@ -791,17 +766,6 @@ export class KTSelect extends KTComponent {
|
|
|
791
766
|
this._dispatchEvent('show');
|
|
792
767
|
this._fireEvent('show');
|
|
793
768
|
|
|
794
|
-
// Focus search input if configured and exists
|
|
795
|
-
if (
|
|
796
|
-
this._config.enableSearch &&
|
|
797
|
-
this._config.searchAutofocus &&
|
|
798
|
-
this._searchInputElement
|
|
799
|
-
) {
|
|
800
|
-
setTimeout(() => {
|
|
801
|
-
this._searchInputElement.focus();
|
|
802
|
-
}, 50);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
769
|
// Update ARIA states
|
|
806
770
|
this._setAriaAttributes();
|
|
807
771
|
|
|
@@ -830,15 +794,15 @@ export class KTSelect extends KTComponent {
|
|
|
830
794
|
if (this._config.debug)
|
|
831
795
|
console.log('Closing dropdown via dropdownModule...');
|
|
832
796
|
|
|
833
|
-
// Clear search input
|
|
797
|
+
// Clear search input if the dropdown is closing
|
|
834
798
|
if (this._searchModule && this._searchInputElement) {
|
|
835
799
|
// Clear search input if configured to do so
|
|
836
800
|
if (this._config.clearSearchOnClose) {
|
|
837
801
|
this._searchInputElement.value = '';
|
|
838
802
|
}
|
|
839
803
|
|
|
840
|
-
//
|
|
841
|
-
this._searchModule.
|
|
804
|
+
// Clear search input when dropdown closes
|
|
805
|
+
this._searchModule.clearSearch();
|
|
842
806
|
}
|
|
843
807
|
|
|
844
808
|
// Set our internal flag to match what we're doing
|
|
@@ -878,11 +842,12 @@ export class KTSelect extends KTComponent {
|
|
|
878
842
|
const selectedOptions = this.getSelectedOptions();
|
|
879
843
|
if (selectedOptions.length === 0) return;
|
|
880
844
|
|
|
881
|
-
//
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
845
|
+
// Iterate through selected options and focus the first one that is visible
|
|
846
|
+
for (const value of selectedOptions) {
|
|
847
|
+
if (this._focusManager && this._focusManager.focusOptionByValue(value)) {
|
|
848
|
+
break; // Stop after focusing the first found selected and visible option
|
|
849
|
+
}
|
|
850
|
+
}
|
|
886
851
|
}
|
|
887
852
|
|
|
888
853
|
/**
|
|
@@ -954,38 +919,60 @@ export class KTSelect extends KTComponent {
|
|
|
954
919
|
*/
|
|
955
920
|
public updateSelectedOptionDisplay() {
|
|
956
921
|
const selectedOptions = this.getSelectedOptions();
|
|
922
|
+
const tagsEnabled = this._config.tags && this._tagsModule;
|
|
923
|
+
const valueDisplayEl = this.getValueDisplayElement();
|
|
957
924
|
|
|
958
|
-
|
|
959
|
-
|
|
925
|
+
if (tagsEnabled) {
|
|
926
|
+
// Tags module will render tags if selectedOptions > 0, or clear them if selectedOptions === 0.
|
|
960
927
|
this._tagsModule.updateTagsDisplay(selectedOptions);
|
|
961
|
-
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Guard against valueDisplayEl being null due to template modifications
|
|
931
|
+
if (!valueDisplayEl) {
|
|
932
|
+
if (this._config.debug) {
|
|
933
|
+
console.warn('KTSelect: Value display element is null. Cannot update display or placeholder. Check template for [data-kt-select-value].');
|
|
934
|
+
}
|
|
935
|
+
return; // Nothing to display on if the element is missing
|
|
962
936
|
}
|
|
963
937
|
|
|
964
938
|
if (typeof this._config.renderSelected === 'function') {
|
|
965
|
-
|
|
966
|
-
this._valueDisplayElement.innerHTML = this._config.renderSelected(selectedOptions);
|
|
939
|
+
valueDisplayEl.innerHTML = this._config.renderSelected(selectedOptions);
|
|
967
940
|
} else {
|
|
968
|
-
|
|
969
941
|
if (selectedOptions.length === 0) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
942
|
+
// No options selected: display placeholder.
|
|
943
|
+
// This runs if tags are off, OR if tags are on but no items are selected (tags module would have cleared tags).
|
|
944
|
+
const placeholderEl = defaultTemplates.placeholder(this._config);
|
|
945
|
+
valueDisplayEl.replaceChildren(placeholderEl);
|
|
973
946
|
} else {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
947
|
+
// Options are selected.
|
|
948
|
+
if (tagsEnabled) {
|
|
949
|
+
// Tags are enabled AND options are selected: tags module has rendered them.
|
|
950
|
+
// Clear valueDisplayEl as tags are the primary display.
|
|
951
|
+
valueDisplayEl.innerHTML = '';
|
|
979
952
|
} else {
|
|
980
|
-
//
|
|
981
|
-
content =
|
|
953
|
+
// Tags are not enabled AND options are selected: render normal text display.
|
|
954
|
+
let content = '';
|
|
955
|
+
if (this._config.displayTemplate) {
|
|
956
|
+
content = this.renderDisplayTemplateForSelected(this.getSelectedOptions());
|
|
957
|
+
} else {
|
|
958
|
+
content = this.getSelectedOptionsText();
|
|
959
|
+
}
|
|
960
|
+
valueDisplayEl.innerHTML = content;
|
|
982
961
|
}
|
|
983
|
-
|
|
984
|
-
this._valueDisplayElement.innerHTML = content;
|
|
985
962
|
}
|
|
986
963
|
}
|
|
987
964
|
}
|
|
988
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Check if an option was originally disabled in the HTML
|
|
968
|
+
*/
|
|
969
|
+
private _isOptionOriginallyDisabled(value: string): boolean {
|
|
970
|
+
const originalOption = Array.from(this._element.querySelectorAll('option')).find(
|
|
971
|
+
(opt) => opt.value === value
|
|
972
|
+
) as HTMLOptionElement;
|
|
973
|
+
return originalOption ? originalOption.disabled : false;
|
|
974
|
+
}
|
|
975
|
+
|
|
989
976
|
/**
|
|
990
977
|
* Update CSS classes for selected options
|
|
991
978
|
*/
|
|
@@ -1007,17 +994,23 @@ export class KTSelect extends KTComponent {
|
|
|
1007
994
|
allOptions.forEach((option) => {
|
|
1008
995
|
const optionValue = option.getAttribute('data-value');
|
|
1009
996
|
if (!optionValue) return;
|
|
997
|
+
|
|
1010
998
|
const isSelected = selectedValues.includes(optionValue);
|
|
999
|
+
const isOriginallyDisabled = this._isOptionOriginallyDisabled(optionValue);
|
|
1000
|
+
|
|
1011
1001
|
if (isSelected) {
|
|
1012
1002
|
option.classList.add('selected');
|
|
1013
1003
|
option.setAttribute('aria-selected', 'true');
|
|
1004
|
+
// Selected options should not be visually hidden or disabled by maxSelections logic
|
|
1014
1005
|
option.classList.remove('hidden');
|
|
1015
1006
|
option.classList.remove('disabled');
|
|
1016
1007
|
option.removeAttribute('aria-disabled');
|
|
1017
1008
|
} else {
|
|
1018
1009
|
option.classList.remove('selected');
|
|
1019
1010
|
option.setAttribute('aria-selected', 'false');
|
|
1020
|
-
|
|
1011
|
+
|
|
1012
|
+
// An option should be disabled if it was originally disabled OR if maxSelections is reached
|
|
1013
|
+
if (isOriginallyDisabled || maxReached) {
|
|
1021
1014
|
option.classList.add('disabled');
|
|
1022
1015
|
option.setAttribute('aria-disabled', 'true');
|
|
1023
1016
|
} else {
|
|
@@ -1085,18 +1078,6 @@ export class KTSelect extends KTComponent {
|
|
|
1085
1078
|
* ========================================================================
|
|
1086
1079
|
*/
|
|
1087
1080
|
|
|
1088
|
-
/**
|
|
1089
|
-
* Handle display element click
|
|
1090
|
-
* @deprecated
|
|
1091
|
-
*/
|
|
1092
|
-
private _handleDropdownClick(event: Event) {
|
|
1093
|
-
if (this._config.debug)
|
|
1094
|
-
console.log('Display element clicked', event.target);
|
|
1095
|
-
event.preventDefault();
|
|
1096
|
-
event.stopPropagation(); // Prevent event bubbling
|
|
1097
|
-
this.toggleDropdown();
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
1081
|
/**
|
|
1101
1082
|
* Handle click within the dropdown
|
|
1102
1083
|
*/
|
|
@@ -1145,6 +1126,13 @@ export class KTSelect extends KTComponent {
|
|
|
1145
1126
|
|
|
1146
1127
|
if (this._config.debug) console.log('Option clicked:', optionValue);
|
|
1147
1128
|
|
|
1129
|
+
// If in single-select mode and the clicked option is already selected, just close the dropdown.
|
|
1130
|
+
if (!this._config.multiple && this._state.isSelected(optionValue)) {
|
|
1131
|
+
if (this._config.debug) console.log('Single select mode: clicked already selected option. Closing dropdown.');
|
|
1132
|
+
this.closeDropdown();
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1148
1136
|
// Use toggleSelection instead of _selectOption to prevent re-rendering
|
|
1149
1137
|
this.toggleSelection(optionValue);
|
|
1150
1138
|
}
|
|
@@ -1221,7 +1209,14 @@ export class KTSelect extends KTComponent {
|
|
|
1221
1209
|
* Get value display element
|
|
1222
1210
|
*/
|
|
1223
1211
|
public getValueDisplayElement() {
|
|
1224
|
-
return this.
|
|
1212
|
+
return this._displayElement;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Get wrapper element
|
|
1217
|
+
*/
|
|
1218
|
+
public getWrapperElement(): HTMLElement {
|
|
1219
|
+
return this._wrapperElement;
|
|
1225
1220
|
}
|
|
1226
1221
|
|
|
1227
1222
|
/**
|
|
@@ -1253,7 +1248,7 @@ export class KTSelect extends KTComponent {
|
|
|
1253
1248
|
// Otherwise, remove just the display property
|
|
1254
1249
|
option.setAttribute(
|
|
1255
1250
|
'style',
|
|
1256
|
-
styleAttr
|
|
1251
|
+
styleAttr?.replace(/display:\s*[^;]+;?/gi, '')?.trim(),
|
|
1257
1252
|
);
|
|
1258
1253
|
}
|
|
1259
1254
|
}
|
|
@@ -1265,7 +1260,7 @@ export class KTSelect extends KTComponent {
|
|
|
1265
1260
|
this._searchInputElement.value = '';
|
|
1266
1261
|
// If we have a search module, clear any search filtering
|
|
1267
1262
|
if (this._searchModule) {
|
|
1268
|
-
this._searchModule.
|
|
1263
|
+
this._searchModule.clearSearch();
|
|
1269
1264
|
}
|
|
1270
1265
|
}
|
|
1271
1266
|
}
|
|
@@ -1297,7 +1292,7 @@ export class KTSelect extends KTComponent {
|
|
|
1297
1292
|
// Get current selection state
|
|
1298
1293
|
const isSelected = this._state.isSelected(value);
|
|
1299
1294
|
if (this._config.debug)
|
|
1300
|
-
console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}
|
|
1295
|
+
console.log(`toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}`);
|
|
1301
1296
|
|
|
1302
1297
|
// If already selected in single select mode, do nothing (can't deselect in single select)
|
|
1303
1298
|
if (isSelected && !this._config.multiple) {
|
|
@@ -1309,9 +1304,9 @@ export class KTSelect extends KTComponent {
|
|
|
1309
1304
|
if (this._config.debug)
|
|
1310
1305
|
console.log(`Toggling selection for option: ${value}, currently selected: ${isSelected}`);
|
|
1311
1306
|
|
|
1312
|
-
// Ensure any search
|
|
1307
|
+
// Ensure any search input is cleared when selection changes
|
|
1313
1308
|
if (this._searchModule) {
|
|
1314
|
-
this._searchModule.
|
|
1309
|
+
this._searchModule.clearSearch();
|
|
1315
1310
|
}
|
|
1316
1311
|
|
|
1317
1312
|
// Toggle the selection in the state
|
|
@@ -1341,14 +1336,13 @@ export class KTSelect extends KTComponent {
|
|
|
1341
1336
|
this._updateSelectedOptionClass();
|
|
1342
1337
|
|
|
1343
1338
|
// For single select mode, always close the dropdown after selection
|
|
1344
|
-
// For multiple select mode, only close if closeOnSelect is true
|
|
1345
1339
|
if (!this._config.multiple) {
|
|
1346
1340
|
if (this._config.debug)
|
|
1347
1341
|
console.log('About to call closeDropdown() for single select mode - always close after selection');
|
|
1348
1342
|
this.closeDropdown();
|
|
1349
|
-
} else
|
|
1343
|
+
} else {
|
|
1350
1344
|
if (this._config.debug)
|
|
1351
|
-
console.log('About to call closeDropdown() for multiple select
|
|
1345
|
+
console.log('About to call closeDropdown() for multiple select');
|
|
1352
1346
|
this.closeDropdown();
|
|
1353
1347
|
}
|
|
1354
1348
|
|
|
@@ -1476,9 +1470,9 @@ export class KTSelect extends KTComponent {
|
|
|
1476
1470
|
// Update options in the dropdown
|
|
1477
1471
|
this._updateSearchResults(items);
|
|
1478
1472
|
|
|
1479
|
-
// Refresh the search module
|
|
1480
|
-
if (this._searchModule
|
|
1481
|
-
this._searchModule.
|
|
1473
|
+
// Refresh the search module to update focus and cache
|
|
1474
|
+
if (this._searchModule) {
|
|
1475
|
+
this._searchModule.refreshAfterSearch();
|
|
1482
1476
|
}
|
|
1483
1477
|
})
|
|
1484
1478
|
.catch((error) => {
|
|
@@ -1606,20 +1600,49 @@ export class KTSelect extends KTComponent {
|
|
|
1606
1600
|
* Centralized keyboard event handler for all select modes
|
|
1607
1601
|
*/
|
|
1608
1602
|
private _handleKeyboardEvent(event: KeyboardEvent) {
|
|
1603
|
+
// If the event target is the search input and the event was already handled (defaultPrevented),
|
|
1604
|
+
// then return early to avoid duplicate processing by this broader handler.
|
|
1605
|
+
if (event.target === this._searchInputElement && event.defaultPrevented) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
1609
|
const isOpen = this._dropdownIsOpen;
|
|
1610
1610
|
const config = this._config;
|
|
1611
1611
|
const focusManager = this._focusManager;
|
|
1612
1612
|
const buffer = this._typeToSearchBuffer;
|
|
1613
1613
|
|
|
1614
|
-
//
|
|
1614
|
+
// If the event target is the search input, let it handle most typing keys naturally.
|
|
1615
|
+
if (event.target === this._searchInputElement) {
|
|
1616
|
+
// Allow navigation keys like ArrowDown, ArrowUp, Escape, Enter (for search/selection) to be handled by the logic below.
|
|
1617
|
+
// For other keys (characters, space, backspace, delete), let the input field process them.
|
|
1618
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' &&
|
|
1619
|
+
event.key !== 'Escape' && event.key !== 'Enter' && event.key !== 'Tab' &&
|
|
1620
|
+
event.key !== 'Home' && event.key !== 'End') {
|
|
1621
|
+
// If it's a character key and we are NOT type-to-searching (because search has focus)
|
|
1622
|
+
// then let the input field handle it for its own value.
|
|
1623
|
+
// The search module's 'input' event will handle filtering based on the input's value.
|
|
1624
|
+
buffer.clear(); // Clear type-to-search buffer when typing in search field
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
// For Enter specifically in search input, we might want to select the focused option or submit search.
|
|
1628
|
+
// This is handled later in the switch.
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Ignore modifier keys (except for specific combinations if added later)
|
|
1615
1632
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1616
1633
|
|
|
1617
|
-
// Type-to-search: only for single char keys
|
|
1618
|
-
if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/)) {
|
|
1634
|
+
// Type-to-search: only for single char keys, when search input does not have focus
|
|
1635
|
+
if (event.key.length === 1 && !event.repeat && !event.key.match(/\s/) && document.activeElement !== this._searchInputElement) {
|
|
1619
1636
|
buffer.push(event.key);
|
|
1620
1637
|
const str = buffer.getBuffer();
|
|
1638
|
+
if (isOpen) {
|
|
1621
1639
|
focusManager.focusByString(str);
|
|
1622
|
-
|
|
1640
|
+
} else {
|
|
1641
|
+
// If closed, type-to-search could potentially open and select.
|
|
1642
|
+
// For now, let's assume it only works when open or opens it first.
|
|
1643
|
+
// Or, we could find the matching option and set it directly without opening.
|
|
1644
|
+
}
|
|
1645
|
+
return; // Type-to-search handles the event
|
|
1623
1646
|
}
|
|
1624
1647
|
|
|
1625
1648
|
switch (event.key) {
|
|
@@ -1650,18 +1673,29 @@ export class KTSelect extends KTComponent {
|
|
|
1650
1673
|
case 'Enter':
|
|
1651
1674
|
case ' ': // Space
|
|
1652
1675
|
if (isOpen) {
|
|
1653
|
-
const
|
|
1654
|
-
if (
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
if (
|
|
1659
|
-
|
|
1660
|
-
|
|
1676
|
+
const focusedOptionEl = this._focusManager.getFocusedOption();
|
|
1677
|
+
if (focusedOptionEl) {
|
|
1678
|
+
const val = focusedOptionEl.dataset.value;
|
|
1679
|
+
// If single select, and the item is already selected, just close.
|
|
1680
|
+
if (val !== undefined && !this._config.multiple && this._state.isSelected(val)) {
|
|
1681
|
+
if (this._config.debug) console.log('Enter on already selected item in single-select mode. Closing.');
|
|
1682
|
+
this.closeDropdown();
|
|
1683
|
+
event.preventDefault();
|
|
1684
|
+
break;
|
|
1661
1685
|
}
|
|
1662
1686
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1687
|
+
|
|
1688
|
+
// Proceed with selection if not handled above
|
|
1689
|
+
this.selectFocusedOption();
|
|
1690
|
+
|
|
1691
|
+
// Close dropdown if configured to do so (for new selections)
|
|
1692
|
+
if (!this._config.multiple) {
|
|
1693
|
+
// This will also be true for the case handled above, but closeDropdown is idempotent.
|
|
1694
|
+
// However, the break above prevents this from being reached for that specific case.
|
|
1695
|
+
this.closeDropdown();
|
|
1696
|
+
}
|
|
1697
|
+
event.preventDefault(); // Prevent form submission or other default actions
|
|
1698
|
+
break;
|
|
1665
1699
|
} else {
|
|
1666
1700
|
this.openDropdown();
|
|
1667
1701
|
}
|
|
@@ -1707,4 +1741,78 @@ export class KTSelect extends KTComponent {
|
|
|
1707
1741
|
));
|
|
1708
1742
|
return contentArray.join(displaySeparator);
|
|
1709
1743
|
}
|
|
1744
|
+
|
|
1745
|
+
public getDisplayElement(): HTMLElement {
|
|
1746
|
+
return this._displayElement;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private _observeNativeSelect() {
|
|
1750
|
+
if (this._mutationObserver) return; // Prevent double observers
|
|
1751
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
1752
|
+
let needsRebuild = false;
|
|
1753
|
+
let needsSelectionSync = false;
|
|
1754
|
+
|
|
1755
|
+
for (const mutation of mutations) {
|
|
1756
|
+
if (mutation.type === 'childList') {
|
|
1757
|
+
// Option(s) added or removed
|
|
1758
|
+
needsRebuild = true;
|
|
1759
|
+
} else if (mutation.type === 'attributes' && mutation.target instanceof HTMLOptionElement) {
|
|
1760
|
+
if (mutation.attributeName === 'selected') {
|
|
1761
|
+
needsSelectionSync = true;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
if (needsRebuild) {
|
|
1767
|
+
// Rebuild the custom dropdown options
|
|
1768
|
+
this._rebuildOptionsFromNative();
|
|
1769
|
+
}
|
|
1770
|
+
if (needsSelectionSync) {
|
|
1771
|
+
this._syncSelectionFromNative();
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
this._mutationObserver.observe(this._element, {
|
|
1776
|
+
childList: true,
|
|
1777
|
+
attributes: true,
|
|
1778
|
+
subtree: true,
|
|
1779
|
+
attributeFilter: ['selected'],
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
private _rebuildOptionsFromNative() {
|
|
1784
|
+
// Remove and rebuild the custom dropdown options from the native select
|
|
1785
|
+
if (this._dropdownContentElement) {
|
|
1786
|
+
const optionsContainer = this._dropdownContentElement.querySelector('[data-kt-select-options]');
|
|
1787
|
+
if (optionsContainer) {
|
|
1788
|
+
optionsContainer.innerHTML = '';
|
|
1789
|
+
const options = Array.from(this._element.querySelectorAll('option'));
|
|
1790
|
+
options.forEach((optionElement) => {
|
|
1791
|
+
if (
|
|
1792
|
+
optionElement.value === '' &&
|
|
1793
|
+
optionElement.textContent.trim() === ''
|
|
1794
|
+
) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const selectOption = new KTSelectOption(optionElement, this._config);
|
|
1798
|
+
const renderedOption = selectOption.render();
|
|
1799
|
+
optionsContainer.appendChild(renderedOption);
|
|
1800
|
+
});
|
|
1801
|
+
// Update internal references
|
|
1802
|
+
this._options = this._wrapperElement.querySelectorAll('[data-kt-select-option]') as NodeListOf<HTMLElement>;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Sync selection after rebuilding
|
|
1806
|
+
this._syncSelectionFromNative();
|
|
1807
|
+
this.updateSelectedOptionDisplay();
|
|
1808
|
+
this._updateSelectedOptionClass();
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
private _syncSelectionFromNative() {
|
|
1812
|
+
// Sync internal state from the native select's selected options
|
|
1813
|
+
const selected = Array.from(this._element.querySelectorAll('option:checked')).map(opt => (opt as HTMLOptionElement).value);
|
|
1814
|
+
this._state.setSelectedOptions(this._config.multiple ? selected : selected[0] || '');
|
|
1815
|
+
this.updateSelectedOptionDisplay();
|
|
1816
|
+
this._updateSelectedOptionClass();
|
|
1817
|
+
}
|
|
1710
1818
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { KTSelectConfigInterface } from './config';
|
|
7
7
|
import { KTSelect } from './select';
|
|
8
8
|
import { defaultTemplates } from './templates';
|
|
9
|
-
import { EventManager
|
|
9
|
+
import { EventManager } from './utils';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* KTSelectTags - Handles tags-specific functionality for KTSelect
|
|
@@ -83,32 +83,6 @@ export class KTSelectTags {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
* Get the label/text for an option by its value
|
|
88
|
-
*/
|
|
89
|
-
private _getOptionLabel(optionValue: string): string {
|
|
90
|
-
// First look for an option element in the dropdown with matching value
|
|
91
|
-
const optionElements = this._select.getOptionsElement();
|
|
92
|
-
for (const option of Array.from(optionElements)) {
|
|
93
|
-
if ((option as HTMLElement).dataset.value === optionValue) {
|
|
94
|
-
return (option as HTMLElement).textContent?.trim() || optionValue;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// If not found in dropdown, look in original select element
|
|
99
|
-
const originalOptions = this._select
|
|
100
|
-
.getElement()
|
|
101
|
-
.querySelectorAll('option');
|
|
102
|
-
for (const option of Array.from(originalOptions)) {
|
|
103
|
-
if ((option as HTMLOptionElement).value === optionValue) {
|
|
104
|
-
return (option as HTMLOptionElement).textContent?.trim() || optionValue;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// If still not found, return the value itself
|
|
109
|
-
return optionValue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
86
|
/**
|
|
113
87
|
* Remove a tag and its selection
|
|
114
88
|
*/
|