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