@duskmoon-dev/el-autocomplete 0.4.0
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/cjs/index.js +452 -0
- package/dist/cjs/register.js +455 -0
- package/dist/esm/index.js +420 -0
- package/dist/esm/register.js +419 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/el-dm-autocomplete.d.ts +97 -0
- package/dist/types/el-dm-autocomplete.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/register.d.ts +2 -0
- package/dist/types/register.d.ts.map +1 -0
- package/package.json +59 -0
- package/src/duskmoon-core.d.ts +4 -0
- package/src/el-dm-autocomplete.ts +536 -0
- package/src/index.ts +25 -0
- package/src/register.ts +2 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @duskmoon-dev/el-autocomplete
|
|
3
|
+
*
|
|
4
|
+
* DuskMoon Autocomplete custom element
|
|
5
|
+
*/
|
|
6
|
+
import { ElDmAutocomplete } from './el-dm-autocomplete.js';
|
|
7
|
+
export { ElDmAutocomplete };
|
|
8
|
+
export type { AutocompleteSize, AutocompleteOption } from './el-dm-autocomplete.js';
|
|
9
|
+
/**
|
|
10
|
+
* Register the el-dm-autocomplete custom element
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { register } from '@duskmoon-dev/el-autocomplete';
|
|
15
|
+
* register();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function register(): void;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAEpF;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,IAAI,IAAI,CAI/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/register.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duskmoon-dev/el-autocomplete",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DuskMoon Autocomplete custom element",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/cjs/index.js",
|
|
13
|
+
"types": "./dist/types/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./register": {
|
|
16
|
+
"import": "./dist/esm/register.js",
|
|
17
|
+
"require": "./dist/cjs/register.js",
|
|
18
|
+
"types": "./dist/types/register.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun run build:esm && bun run build:cjs && bun run build:types",
|
|
27
|
+
"build:esm": "bun build ./src/index.ts ./src/register.ts --outdir ./dist/esm --format esm --target browser --external @duskmoon-dev/el-core --external @duskmoon-dev/core",
|
|
28
|
+
"build:cjs": "bun build ./src/index.ts ./src/register.ts --outdir ./dist/cjs --format cjs --target browser --external @duskmoon-dev/el-core --external @duskmoon-dev/core",
|
|
29
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
30
|
+
"clean": "rm -rf dist",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@duskmoon-dev/core": "^1.1.1",
|
|
35
|
+
"@duskmoon-dev/el-core": "0.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.8.3"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@duskmoon-dev/core": "^0.1.7"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/AkaraChen/duskmoon-elements.git",
|
|
49
|
+
"directory": "elements/autocomplete"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"web-components",
|
|
53
|
+
"custom-elements",
|
|
54
|
+
"autocomplete",
|
|
55
|
+
"duskmoon"
|
|
56
|
+
],
|
|
57
|
+
"author": "AkaraChen",
|
|
58
|
+
"license": "MIT"
|
|
59
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { BaseElement, css as cssTag } from '@duskmoon-dev/el-core';
|
|
2
|
+
import { css } from '@duskmoon-dev/core/components/autocomplete';
|
|
3
|
+
|
|
4
|
+
// Strip @layer components wrapper for Shadow DOM
|
|
5
|
+
const strippedCss = css
|
|
6
|
+
.replace(/@layer\s+components\s*\{/, '')
|
|
7
|
+
.replace(/\}[\s]*$/, '');
|
|
8
|
+
|
|
9
|
+
const styles = cssTag`
|
|
10
|
+
${strippedCss}
|
|
11
|
+
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:host([hidden]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.autocomplete {
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export type AutocompleteSize = 'sm' | 'md' | 'lg';
|
|
26
|
+
|
|
27
|
+
export interface AutocompleteOption {
|
|
28
|
+
value: string;
|
|
29
|
+
label: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
group?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ElDmAutocomplete extends BaseElement {
|
|
36
|
+
static properties = {
|
|
37
|
+
value: { type: String, reflect: true, default: '' },
|
|
38
|
+
options: { type: String, reflect: true, default: '[]' },
|
|
39
|
+
multiple: { type: Boolean, reflect: true, default: false },
|
|
40
|
+
disabled: { type: Boolean, reflect: true, default: false },
|
|
41
|
+
clearable: { type: Boolean, reflect: true, default: false },
|
|
42
|
+
placeholder: { type: String, reflect: true, default: '' },
|
|
43
|
+
size: { type: String, reflect: true, default: 'md' },
|
|
44
|
+
loading: { type: Boolean, reflect: true, default: false },
|
|
45
|
+
noResultsText: { type: String, reflect: true, default: 'No results found' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
value!: string;
|
|
49
|
+
options!: string;
|
|
50
|
+
multiple!: boolean;
|
|
51
|
+
disabled!: boolean;
|
|
52
|
+
clearable!: boolean;
|
|
53
|
+
placeholder!: string;
|
|
54
|
+
size!: AutocompleteSize;
|
|
55
|
+
loading!: boolean;
|
|
56
|
+
noResultsText!: string;
|
|
57
|
+
|
|
58
|
+
private _isOpen = false;
|
|
59
|
+
private _searchValue = '';
|
|
60
|
+
private _highlightedIndex = -1;
|
|
61
|
+
private _selectedValues: string[] = [];
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
super();
|
|
65
|
+
this.attachStyles(styles);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
super.connectedCallback();
|
|
70
|
+
this._parseValue();
|
|
71
|
+
document.addEventListener('click', this._handleOutsideClick);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
disconnectedCallback() {
|
|
75
|
+
super.disconnectedCallback();
|
|
76
|
+
document.removeEventListener('click', this._handleOutsideClick);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private _handleOutsideClick = (e: MouseEvent) => {
|
|
80
|
+
if (!this.contains(e.target as Node)) {
|
|
81
|
+
this._close();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
private _parseValue() {
|
|
86
|
+
if (this.multiple && this.value) {
|
|
87
|
+
try {
|
|
88
|
+
this._selectedValues = JSON.parse(this.value);
|
|
89
|
+
} catch {
|
|
90
|
+
this._selectedValues = this.value ? [this.value] : [];
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
this._selectedValues = this.value ? [this.value] : [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private _getOptions(): AutocompleteOption[] {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(this.options);
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private _getFilteredOptions(): AutocompleteOption[] {
|
|
106
|
+
const allOptions = this._getOptions();
|
|
107
|
+
if (!this._searchValue) return allOptions;
|
|
108
|
+
|
|
109
|
+
const search = this._searchValue.toLowerCase();
|
|
110
|
+
return allOptions.filter(
|
|
111
|
+
(opt) =>
|
|
112
|
+
opt.label.toLowerCase().includes(search) ||
|
|
113
|
+
opt.value.toLowerCase().includes(search) ||
|
|
114
|
+
opt.description?.toLowerCase().includes(search)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private _open() {
|
|
119
|
+
if (this.disabled) return;
|
|
120
|
+
this._isOpen = true;
|
|
121
|
+
this._highlightedIndex = -1;
|
|
122
|
+
this.update();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _close() {
|
|
126
|
+
this._isOpen = false;
|
|
127
|
+
this._searchValue = '';
|
|
128
|
+
this._highlightedIndex = -1;
|
|
129
|
+
this.update();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _toggle() {
|
|
133
|
+
if (this._isOpen) {
|
|
134
|
+
this._close();
|
|
135
|
+
} else {
|
|
136
|
+
this._open();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _handleInputChange(e: Event) {
|
|
141
|
+
const input = e.target as HTMLInputElement;
|
|
142
|
+
this._searchValue = input.value;
|
|
143
|
+
this._highlightedIndex = -1;
|
|
144
|
+
|
|
145
|
+
if (!this._isOpen) {
|
|
146
|
+
this._open();
|
|
147
|
+
} else {
|
|
148
|
+
this.update();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.emit('input', { searchValue: this._searchValue });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _handleKeyDown(e: KeyboardEvent) {
|
|
155
|
+
const filteredOptions = this._getFilteredOptions().filter(
|
|
156
|
+
(opt) => !opt.disabled
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
switch (e.key) {
|
|
160
|
+
case 'ArrowDown':
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
if (!this._isOpen) {
|
|
163
|
+
this._open();
|
|
164
|
+
} else {
|
|
165
|
+
this._highlightedIndex = Math.min(
|
|
166
|
+
this._highlightedIndex + 1,
|
|
167
|
+
filteredOptions.length - 1
|
|
168
|
+
);
|
|
169
|
+
this.update();
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'ArrowUp':
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (this._isOpen) {
|
|
176
|
+
this._highlightedIndex = Math.max(this._highlightedIndex - 1, 0);
|
|
177
|
+
this.update();
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'Enter':
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
if (
|
|
184
|
+
this._isOpen &&
|
|
185
|
+
this._highlightedIndex >= 0 &&
|
|
186
|
+
this._highlightedIndex < filteredOptions.length
|
|
187
|
+
) {
|
|
188
|
+
this._selectOption(filteredOptions[this._highlightedIndex]);
|
|
189
|
+
} else if (!this._isOpen) {
|
|
190
|
+
this._open();
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'Escape':
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
this._close();
|
|
197
|
+
break;
|
|
198
|
+
|
|
199
|
+
case 'Backspace':
|
|
200
|
+
if (
|
|
201
|
+
this.multiple &&
|
|
202
|
+
!this._searchValue &&
|
|
203
|
+
this._selectedValues.length > 0
|
|
204
|
+
) {
|
|
205
|
+
const lastValue = this._selectedValues[this._selectedValues.length - 1];
|
|
206
|
+
this._removeValue(lastValue);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _selectOption(option: AutocompleteOption) {
|
|
213
|
+
if (option.disabled) return;
|
|
214
|
+
|
|
215
|
+
if (this.multiple) {
|
|
216
|
+
const index = this._selectedValues.indexOf(option.value);
|
|
217
|
+
if (index === -1) {
|
|
218
|
+
this._selectedValues = [...this._selectedValues, option.value];
|
|
219
|
+
} else {
|
|
220
|
+
this._selectedValues = this._selectedValues.filter(
|
|
221
|
+
(v) => v !== option.value
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
this.value = JSON.stringify(this._selectedValues);
|
|
225
|
+
this._searchValue = '';
|
|
226
|
+
} else {
|
|
227
|
+
this._selectedValues = [option.value];
|
|
228
|
+
this.value = option.value;
|
|
229
|
+
this._close();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.emit('change', {
|
|
233
|
+
value: this.value,
|
|
234
|
+
selectedValues: this._selectedValues,
|
|
235
|
+
option,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
this.update();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private _removeValue(val: string) {
|
|
242
|
+
this._selectedValues = this._selectedValues.filter((v) => v !== val);
|
|
243
|
+
this.value = this.multiple
|
|
244
|
+
? JSON.stringify(this._selectedValues)
|
|
245
|
+
: this._selectedValues[0] || '';
|
|
246
|
+
|
|
247
|
+
this.emit('change', {
|
|
248
|
+
value: this.value,
|
|
249
|
+
selectedValues: this._selectedValues,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.update();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private _clear() {
|
|
256
|
+
this._selectedValues = [];
|
|
257
|
+
this._searchValue = '';
|
|
258
|
+
this.value = this.multiple ? '[]' : '';
|
|
259
|
+
|
|
260
|
+
this.emit('clear', { value: this.value });
|
|
261
|
+
this.emit('change', {
|
|
262
|
+
value: this.value,
|
|
263
|
+
selectedValues: this._selectedValues,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.update();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private _getDisplayValue(): string {
|
|
270
|
+
if (this._selectedValues.length === 0) return '';
|
|
271
|
+
|
|
272
|
+
const allOptions = this._getOptions();
|
|
273
|
+
if (this.multiple) {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const selectedOption = allOptions.find(
|
|
278
|
+
(opt) => opt.value === this._selectedValues[0]
|
|
279
|
+
);
|
|
280
|
+
return selectedOption?.label || this._selectedValues[0];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _highlightMatch(text: string): string {
|
|
284
|
+
if (!this._searchValue) return text;
|
|
285
|
+
|
|
286
|
+
const search = this._searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
287
|
+
const regex = new RegExp(`(${search})`, 'gi');
|
|
288
|
+
return text.replace(
|
|
289
|
+
regex,
|
|
290
|
+
'<span class="autocomplete-highlight">$1</span>'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private _renderTags(): string {
|
|
295
|
+
const allOptions = this._getOptions();
|
|
296
|
+
return this._selectedValues
|
|
297
|
+
.map((val) => {
|
|
298
|
+
const option = allOptions.find((opt) => opt.value === val);
|
|
299
|
+
const label = option?.label || val;
|
|
300
|
+
return `
|
|
301
|
+
<span class="autocomplete-tag">
|
|
302
|
+
<span>${label}</span>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
class="autocomplete-tag-remove"
|
|
306
|
+
data-value="${val}"
|
|
307
|
+
${this.disabled ? 'disabled' : ''}
|
|
308
|
+
>×</button>
|
|
309
|
+
</span>
|
|
310
|
+
`;
|
|
311
|
+
})
|
|
312
|
+
.join('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private _renderOptions(): string {
|
|
316
|
+
const filteredOptions = this._getFilteredOptions();
|
|
317
|
+
|
|
318
|
+
if (this.loading) {
|
|
319
|
+
return `
|
|
320
|
+
<div class="autocomplete-loading">
|
|
321
|
+
<span>Loading...</span>
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (filteredOptions.length === 0) {
|
|
327
|
+
return `
|
|
328
|
+
<div class="autocomplete-no-results">${this.noResultsText}</div>
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Group options if they have groups
|
|
333
|
+
const groups = new Map<string, AutocompleteOption[]>();
|
|
334
|
+
const ungrouped: AutocompleteOption[] = [];
|
|
335
|
+
|
|
336
|
+
for (const opt of filteredOptions) {
|
|
337
|
+
if (opt.group) {
|
|
338
|
+
const group = groups.get(opt.group) || [];
|
|
339
|
+
group.push(opt);
|
|
340
|
+
groups.set(opt.group, group);
|
|
341
|
+
} else {
|
|
342
|
+
ungrouped.push(opt);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let html = '';
|
|
347
|
+
let globalIndex = 0;
|
|
348
|
+
|
|
349
|
+
// Render grouped options
|
|
350
|
+
for (const [groupName, options] of groups) {
|
|
351
|
+
html += `<div class="autocomplete-group-header">${groupName}</div>`;
|
|
352
|
+
for (const opt of options) {
|
|
353
|
+
html += this._renderOption(opt, globalIndex++);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Render ungrouped options
|
|
358
|
+
for (const opt of ungrouped) {
|
|
359
|
+
html += this._renderOption(opt, globalIndex++);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return html;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private _renderOption(opt: AutocompleteOption, index: number): string {
|
|
366
|
+
const isSelected = this._selectedValues.includes(opt.value);
|
|
367
|
+
const isHighlighted = index === this._highlightedIndex;
|
|
368
|
+
|
|
369
|
+
const classes = [
|
|
370
|
+
'autocomplete-option',
|
|
371
|
+
isSelected ? 'selected' : '',
|
|
372
|
+
isHighlighted ? 'highlighted' : '',
|
|
373
|
+
opt.disabled ? 'disabled' : '',
|
|
374
|
+
]
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.join(' ');
|
|
377
|
+
|
|
378
|
+
return `
|
|
379
|
+
<div
|
|
380
|
+
class="${classes}"
|
|
381
|
+
data-value="${opt.value}"
|
|
382
|
+
data-index="${index}"
|
|
383
|
+
role="option"
|
|
384
|
+
aria-selected="${isSelected}"
|
|
385
|
+
${opt.disabled ? 'aria-disabled="true"' : ''}
|
|
386
|
+
>
|
|
387
|
+
${
|
|
388
|
+
this.multiple
|
|
389
|
+
? `
|
|
390
|
+
<span class="autocomplete-option-icon">
|
|
391
|
+
${isSelected ? '✓' : ''}
|
|
392
|
+
</span>
|
|
393
|
+
`
|
|
394
|
+
: ''
|
|
395
|
+
}
|
|
396
|
+
<div class="autocomplete-option-content">
|
|
397
|
+
<div class="autocomplete-option-label">${this._highlightMatch(opt.label)}</div>
|
|
398
|
+
${opt.description ? `<div class="autocomplete-option-description">${this._highlightMatch(opt.description)}</div>` : ''}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
render() {
|
|
405
|
+
const sizeClass =
|
|
406
|
+
this.size !== 'md' ? `autocomplete-${this.size}` : '';
|
|
407
|
+
const openClass = this._isOpen ? 'autocomplete-open' : '';
|
|
408
|
+
const clearableClass = this.clearable ? 'autocomplete-clearable' : '';
|
|
409
|
+
const showClear =
|
|
410
|
+
this.clearable && this._selectedValues.length > 0 && !this.disabled;
|
|
411
|
+
|
|
412
|
+
if (this.multiple) {
|
|
413
|
+
return `
|
|
414
|
+
<div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
|
|
415
|
+
<div class="autocomplete-tags">
|
|
416
|
+
${this._renderTags()}
|
|
417
|
+
<input
|
|
418
|
+
type="text"
|
|
419
|
+
class="autocomplete-tags-input"
|
|
420
|
+
placeholder="${this._selectedValues.length === 0 ? this.placeholder : ''}"
|
|
421
|
+
value="${this._searchValue}"
|
|
422
|
+
${this.disabled ? 'disabled' : ''}
|
|
423
|
+
role="combobox"
|
|
424
|
+
aria-expanded="${this._isOpen}"
|
|
425
|
+
aria-haspopup="listbox"
|
|
426
|
+
autocomplete="off"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
${
|
|
430
|
+
showClear
|
|
431
|
+
? `
|
|
432
|
+
<button type="button" class="autocomplete-clear" aria-label="Clear selection">×</button>
|
|
433
|
+
`
|
|
434
|
+
: ''
|
|
435
|
+
}
|
|
436
|
+
<div class="autocomplete-dropdown" role="listbox">
|
|
437
|
+
${this._renderOptions()}
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return `
|
|
444
|
+
<div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
|
|
445
|
+
<input
|
|
446
|
+
type="text"
|
|
447
|
+
class="autocomplete-input"
|
|
448
|
+
placeholder="${this.placeholder}"
|
|
449
|
+
value="${this._isOpen ? this._searchValue : this._getDisplayValue()}"
|
|
450
|
+
${this.disabled ? 'disabled' : ''}
|
|
451
|
+
role="combobox"
|
|
452
|
+
aria-expanded="${this._isOpen}"
|
|
453
|
+
aria-haspopup="listbox"
|
|
454
|
+
autocomplete="off"
|
|
455
|
+
/>
|
|
456
|
+
${
|
|
457
|
+
showClear
|
|
458
|
+
? `
|
|
459
|
+
<button type="button" class="autocomplete-clear" aria-label="Clear selection">×</button>
|
|
460
|
+
`
|
|
461
|
+
: ''
|
|
462
|
+
}
|
|
463
|
+
<div class="autocomplete-dropdown" role="listbox">
|
|
464
|
+
${this._renderOptions()}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
update() {
|
|
471
|
+
super.update();
|
|
472
|
+
this._attachEventListeners();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private _attachEventListeners() {
|
|
476
|
+
const input = this.shadowRoot?.querySelector(
|
|
477
|
+
'.autocomplete-input, .autocomplete-tags-input'
|
|
478
|
+
) as HTMLInputElement;
|
|
479
|
+
|
|
480
|
+
if (input) {
|
|
481
|
+
input.removeEventListener('input', this._handleInputChange.bind(this));
|
|
482
|
+
input.removeEventListener('keydown', this._handleKeyDown.bind(this));
|
|
483
|
+
input.removeEventListener('focus', this._open.bind(this));
|
|
484
|
+
|
|
485
|
+
input.addEventListener('input', this._handleInputChange.bind(this));
|
|
486
|
+
input.addEventListener('keydown', this._handleKeyDown.bind(this));
|
|
487
|
+
input.addEventListener('focus', this._open.bind(this));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const clearBtn = this.shadowRoot?.querySelector('.autocomplete-clear');
|
|
491
|
+
if (clearBtn) {
|
|
492
|
+
clearBtn.removeEventListener('click', this._clear.bind(this));
|
|
493
|
+
clearBtn.addEventListener('click', this._clear.bind(this));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Option click handlers
|
|
497
|
+
const options = this.shadowRoot?.querySelectorAll('.autocomplete-option');
|
|
498
|
+
options?.forEach((opt) => {
|
|
499
|
+
opt.removeEventListener('click', this._handleOptionClick);
|
|
500
|
+
opt.addEventListener('click', this._handleOptionClick);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Tag remove handlers
|
|
504
|
+
const tagRemoves = this.shadowRoot?.querySelectorAll(
|
|
505
|
+
'.autocomplete-tag-remove'
|
|
506
|
+
);
|
|
507
|
+
tagRemoves?.forEach((btn) => {
|
|
508
|
+
btn.removeEventListener('click', this._handleTagRemove);
|
|
509
|
+
btn.addEventListener('click', this._handleTagRemove);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private _handleOptionClick = (e: Event) => {
|
|
514
|
+
e.stopPropagation();
|
|
515
|
+
const target = e.currentTarget as HTMLElement;
|
|
516
|
+
const value = target.dataset.value;
|
|
517
|
+
|
|
518
|
+
if (value) {
|
|
519
|
+
const allOptions = this._getOptions();
|
|
520
|
+
const option = allOptions.find((opt) => opt.value === value);
|
|
521
|
+
if (option) {
|
|
522
|
+
this._selectOption(option);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
private _handleTagRemove = (e: Event) => {
|
|
528
|
+
e.stopPropagation();
|
|
529
|
+
const target = e.currentTarget as HTMLElement;
|
|
530
|
+
const value = target.dataset.value;
|
|
531
|
+
|
|
532
|
+
if (value) {
|
|
533
|
+
this._removeValue(value);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @duskmoon-dev/el-autocomplete
|
|
3
|
+
*
|
|
4
|
+
* DuskMoon Autocomplete custom element
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ElDmAutocomplete } from './el-dm-autocomplete.js';
|
|
8
|
+
|
|
9
|
+
export { ElDmAutocomplete };
|
|
10
|
+
export type { AutocompleteSize, AutocompleteOption } from './el-dm-autocomplete.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register the el-dm-autocomplete custom element
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { register } from '@duskmoon-dev/el-autocomplete';
|
|
18
|
+
* register();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function register(): void {
|
|
22
|
+
if (!customElements.get('el-dm-autocomplete')) {
|
|
23
|
+
customElements.define('el-dm-autocomplete', ElDmAutocomplete);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/register.ts
ADDED