@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.
@@ -0,0 +1,452 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/index.ts
30
+ var exports_src = {};
31
+ __export(exports_src, {
32
+ register: () => register,
33
+ ElDmAutocomplete: () => ElDmAutocomplete
34
+ });
35
+ module.exports = __toCommonJS(exports_src);
36
+
37
+ // src/el-dm-autocomplete.ts
38
+ var import_el_core = require("@duskmoon-dev/el-core");
39
+ var import_autocomplete = require("@duskmoon-dev/core/components/autocomplete");
40
+ var strippedCss = import_autocomplete.css.replace(/@layer\s+components\s*\{/, "").replace(/\}[\s]*$/, "");
41
+ var styles = import_el_core.css`
42
+ ${strippedCss}
43
+
44
+ :host {
45
+ display: block;
46
+ }
47
+
48
+ :host([hidden]) {
49
+ display: none;
50
+ }
51
+
52
+ .autocomplete {
53
+ width: 100%;
54
+ }
55
+ `;
56
+
57
+ class ElDmAutocomplete extends import_el_core.BaseElement {
58
+ static properties = {
59
+ value: { type: String, reflect: true, default: "" },
60
+ options: { type: String, reflect: true, default: "[]" },
61
+ multiple: { type: Boolean, reflect: true, default: false },
62
+ disabled: { type: Boolean, reflect: true, default: false },
63
+ clearable: { type: Boolean, reflect: true, default: false },
64
+ placeholder: { type: String, reflect: true, default: "" },
65
+ size: { type: String, reflect: true, default: "md" },
66
+ loading: { type: Boolean, reflect: true, default: false },
67
+ noResultsText: { type: String, reflect: true, default: "No results found" }
68
+ };
69
+ value;
70
+ options;
71
+ multiple;
72
+ disabled;
73
+ clearable;
74
+ placeholder;
75
+ size;
76
+ loading;
77
+ noResultsText;
78
+ _isOpen = false;
79
+ _searchValue = "";
80
+ _highlightedIndex = -1;
81
+ _selectedValues = [];
82
+ constructor() {
83
+ super();
84
+ this.attachStyles(styles);
85
+ }
86
+ connectedCallback() {
87
+ super.connectedCallback();
88
+ this._parseValue();
89
+ document.addEventListener("click", this._handleOutsideClick);
90
+ }
91
+ disconnectedCallback() {
92
+ super.disconnectedCallback();
93
+ document.removeEventListener("click", this._handleOutsideClick);
94
+ }
95
+ _handleOutsideClick = (e) => {
96
+ if (!this.contains(e.target)) {
97
+ this._close();
98
+ }
99
+ };
100
+ _parseValue() {
101
+ if (this.multiple && this.value) {
102
+ try {
103
+ this._selectedValues = JSON.parse(this.value);
104
+ } catch {
105
+ this._selectedValues = this.value ? [this.value] : [];
106
+ }
107
+ } else {
108
+ this._selectedValues = this.value ? [this.value] : [];
109
+ }
110
+ }
111
+ _getOptions() {
112
+ try {
113
+ return JSON.parse(this.options);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+ _getFilteredOptions() {
119
+ const allOptions = this._getOptions();
120
+ if (!this._searchValue)
121
+ return allOptions;
122
+ const search = this._searchValue.toLowerCase();
123
+ return allOptions.filter((opt) => opt.label.toLowerCase().includes(search) || opt.value.toLowerCase().includes(search) || opt.description?.toLowerCase().includes(search));
124
+ }
125
+ _open() {
126
+ if (this.disabled)
127
+ return;
128
+ this._isOpen = true;
129
+ this._highlightedIndex = -1;
130
+ this.update();
131
+ }
132
+ _close() {
133
+ this._isOpen = false;
134
+ this._searchValue = "";
135
+ this._highlightedIndex = -1;
136
+ this.update();
137
+ }
138
+ _toggle() {
139
+ if (this._isOpen) {
140
+ this._close();
141
+ } else {
142
+ this._open();
143
+ }
144
+ }
145
+ _handleInputChange(e) {
146
+ const input = e.target;
147
+ this._searchValue = input.value;
148
+ this._highlightedIndex = -1;
149
+ if (!this._isOpen) {
150
+ this._open();
151
+ } else {
152
+ this.update();
153
+ }
154
+ this.emit("input", { searchValue: this._searchValue });
155
+ }
156
+ _handleKeyDown(e) {
157
+ const filteredOptions = this._getFilteredOptions().filter((opt) => !opt.disabled);
158
+ switch (e.key) {
159
+ case "ArrowDown":
160
+ e.preventDefault();
161
+ if (!this._isOpen) {
162
+ this._open();
163
+ } else {
164
+ this._highlightedIndex = Math.min(this._highlightedIndex + 1, filteredOptions.length - 1);
165
+ this.update();
166
+ }
167
+ break;
168
+ case "ArrowUp":
169
+ e.preventDefault();
170
+ if (this._isOpen) {
171
+ this._highlightedIndex = Math.max(this._highlightedIndex - 1, 0);
172
+ this.update();
173
+ }
174
+ break;
175
+ case "Enter":
176
+ e.preventDefault();
177
+ if (this._isOpen && this._highlightedIndex >= 0 && this._highlightedIndex < filteredOptions.length) {
178
+ this._selectOption(filteredOptions[this._highlightedIndex]);
179
+ } else if (!this._isOpen) {
180
+ this._open();
181
+ }
182
+ break;
183
+ case "Escape":
184
+ e.preventDefault();
185
+ this._close();
186
+ break;
187
+ case "Backspace":
188
+ if (this.multiple && !this._searchValue && this._selectedValues.length > 0) {
189
+ const lastValue = this._selectedValues[this._selectedValues.length - 1];
190
+ this._removeValue(lastValue);
191
+ }
192
+ break;
193
+ }
194
+ }
195
+ _selectOption(option) {
196
+ if (option.disabled)
197
+ return;
198
+ if (this.multiple) {
199
+ const index = this._selectedValues.indexOf(option.value);
200
+ if (index === -1) {
201
+ this._selectedValues = [...this._selectedValues, option.value];
202
+ } else {
203
+ this._selectedValues = this._selectedValues.filter((v) => v !== option.value);
204
+ }
205
+ this.value = JSON.stringify(this._selectedValues);
206
+ this._searchValue = "";
207
+ } else {
208
+ this._selectedValues = [option.value];
209
+ this.value = option.value;
210
+ this._close();
211
+ }
212
+ this.emit("change", {
213
+ value: this.value,
214
+ selectedValues: this._selectedValues,
215
+ option
216
+ });
217
+ this.update();
218
+ }
219
+ _removeValue(val) {
220
+ this._selectedValues = this._selectedValues.filter((v) => v !== val);
221
+ this.value = this.multiple ? JSON.stringify(this._selectedValues) : this._selectedValues[0] || "";
222
+ this.emit("change", {
223
+ value: this.value,
224
+ selectedValues: this._selectedValues
225
+ });
226
+ this.update();
227
+ }
228
+ _clear() {
229
+ this._selectedValues = [];
230
+ this._searchValue = "";
231
+ this.value = this.multiple ? "[]" : "";
232
+ this.emit("clear", { value: this.value });
233
+ this.emit("change", {
234
+ value: this.value,
235
+ selectedValues: this._selectedValues
236
+ });
237
+ this.update();
238
+ }
239
+ _getDisplayValue() {
240
+ if (this._selectedValues.length === 0)
241
+ return "";
242
+ const allOptions = this._getOptions();
243
+ if (this.multiple) {
244
+ return "";
245
+ }
246
+ const selectedOption = allOptions.find((opt) => opt.value === this._selectedValues[0]);
247
+ return selectedOption?.label || this._selectedValues[0];
248
+ }
249
+ _highlightMatch(text) {
250
+ if (!this._searchValue)
251
+ return text;
252
+ const search = this._searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
253
+ const regex = new RegExp(`(${search})`, "gi");
254
+ return text.replace(regex, '<span class="autocomplete-highlight">$1</span>');
255
+ }
256
+ _renderTags() {
257
+ const allOptions = this._getOptions();
258
+ return this._selectedValues.map((val) => {
259
+ const option = allOptions.find((opt) => opt.value === val);
260
+ const label = option?.label || val;
261
+ return `
262
+ <span class="autocomplete-tag">
263
+ <span>${label}</span>
264
+ <button
265
+ type="button"
266
+ class="autocomplete-tag-remove"
267
+ data-value="${val}"
268
+ ${this.disabled ? "disabled" : ""}
269
+ >&times;</button>
270
+ </span>
271
+ `;
272
+ }).join("");
273
+ }
274
+ _renderOptions() {
275
+ const filteredOptions = this._getFilteredOptions();
276
+ if (this.loading) {
277
+ return `
278
+ <div class="autocomplete-loading">
279
+ <span>Loading...</span>
280
+ </div>
281
+ `;
282
+ }
283
+ if (filteredOptions.length === 0) {
284
+ return `
285
+ <div class="autocomplete-no-results">${this.noResultsText}</div>
286
+ `;
287
+ }
288
+ const groups = new Map;
289
+ const ungrouped = [];
290
+ for (const opt of filteredOptions) {
291
+ if (opt.group) {
292
+ const group = groups.get(opt.group) || [];
293
+ group.push(opt);
294
+ groups.set(opt.group, group);
295
+ } else {
296
+ ungrouped.push(opt);
297
+ }
298
+ }
299
+ let html = "";
300
+ let globalIndex = 0;
301
+ for (const [groupName, options] of groups) {
302
+ html += `<div class="autocomplete-group-header">${groupName}</div>`;
303
+ for (const opt of options) {
304
+ html += this._renderOption(opt, globalIndex++);
305
+ }
306
+ }
307
+ for (const opt of ungrouped) {
308
+ html += this._renderOption(opt, globalIndex++);
309
+ }
310
+ return html;
311
+ }
312
+ _renderOption(opt, index) {
313
+ const isSelected = this._selectedValues.includes(opt.value);
314
+ const isHighlighted = index === this._highlightedIndex;
315
+ const classes = [
316
+ "autocomplete-option",
317
+ isSelected ? "selected" : "",
318
+ isHighlighted ? "highlighted" : "",
319
+ opt.disabled ? "disabled" : ""
320
+ ].filter(Boolean).join(" ");
321
+ return `
322
+ <div
323
+ class="${classes}"
324
+ data-value="${opt.value}"
325
+ data-index="${index}"
326
+ role="option"
327
+ aria-selected="${isSelected}"
328
+ ${opt.disabled ? 'aria-disabled="true"' : ""}
329
+ >
330
+ ${this.multiple ? `
331
+ <span class="autocomplete-option-icon">
332
+ ${isSelected ? "✓" : ""}
333
+ </span>
334
+ ` : ""}
335
+ <div class="autocomplete-option-content">
336
+ <div class="autocomplete-option-label">${this._highlightMatch(opt.label)}</div>
337
+ ${opt.description ? `<div class="autocomplete-option-description">${this._highlightMatch(opt.description)}</div>` : ""}
338
+ </div>
339
+ </div>
340
+ `;
341
+ }
342
+ render() {
343
+ const sizeClass = this.size !== "md" ? `autocomplete-${this.size}` : "";
344
+ const openClass = this._isOpen ? "autocomplete-open" : "";
345
+ const clearableClass = this.clearable ? "autocomplete-clearable" : "";
346
+ const showClear = this.clearable && this._selectedValues.length > 0 && !this.disabled;
347
+ if (this.multiple) {
348
+ return `
349
+ <div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
350
+ <div class="autocomplete-tags">
351
+ ${this._renderTags()}
352
+ <input
353
+ type="text"
354
+ class="autocomplete-tags-input"
355
+ placeholder="${this._selectedValues.length === 0 ? this.placeholder : ""}"
356
+ value="${this._searchValue}"
357
+ ${this.disabled ? "disabled" : ""}
358
+ role="combobox"
359
+ aria-expanded="${this._isOpen}"
360
+ aria-haspopup="listbox"
361
+ autocomplete="off"
362
+ />
363
+ </div>
364
+ ${showClear ? `
365
+ <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
366
+ ` : ""}
367
+ <div class="autocomplete-dropdown" role="listbox">
368
+ ${this._renderOptions()}
369
+ </div>
370
+ </div>
371
+ `;
372
+ }
373
+ return `
374
+ <div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
375
+ <input
376
+ type="text"
377
+ class="autocomplete-input"
378
+ placeholder="${this.placeholder}"
379
+ value="${this._isOpen ? this._searchValue : this._getDisplayValue()}"
380
+ ${this.disabled ? "disabled" : ""}
381
+ role="combobox"
382
+ aria-expanded="${this._isOpen}"
383
+ aria-haspopup="listbox"
384
+ autocomplete="off"
385
+ />
386
+ ${showClear ? `
387
+ <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
388
+ ` : ""}
389
+ <div class="autocomplete-dropdown" role="listbox">
390
+ ${this._renderOptions()}
391
+ </div>
392
+ </div>
393
+ `;
394
+ }
395
+ update() {
396
+ super.update();
397
+ this._attachEventListeners();
398
+ }
399
+ _attachEventListeners() {
400
+ const input = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags-input");
401
+ if (input) {
402
+ input.removeEventListener("input", this._handleInputChange.bind(this));
403
+ input.removeEventListener("keydown", this._handleKeyDown.bind(this));
404
+ input.removeEventListener("focus", this._open.bind(this));
405
+ input.addEventListener("input", this._handleInputChange.bind(this));
406
+ input.addEventListener("keydown", this._handleKeyDown.bind(this));
407
+ input.addEventListener("focus", this._open.bind(this));
408
+ }
409
+ const clearBtn = this.shadowRoot?.querySelector(".autocomplete-clear");
410
+ if (clearBtn) {
411
+ clearBtn.removeEventListener("click", this._clear.bind(this));
412
+ clearBtn.addEventListener("click", this._clear.bind(this));
413
+ }
414
+ const options = this.shadowRoot?.querySelectorAll(".autocomplete-option");
415
+ options?.forEach((opt) => {
416
+ opt.removeEventListener("click", this._handleOptionClick);
417
+ opt.addEventListener("click", this._handleOptionClick);
418
+ });
419
+ const tagRemoves = this.shadowRoot?.querySelectorAll(".autocomplete-tag-remove");
420
+ tagRemoves?.forEach((btn) => {
421
+ btn.removeEventListener("click", this._handleTagRemove);
422
+ btn.addEventListener("click", this._handleTagRemove);
423
+ });
424
+ }
425
+ _handleOptionClick = (e) => {
426
+ e.stopPropagation();
427
+ const target = e.currentTarget;
428
+ const value = target.dataset.value;
429
+ if (value) {
430
+ const allOptions = this._getOptions();
431
+ const option = allOptions.find((opt) => opt.value === value);
432
+ if (option) {
433
+ this._selectOption(option);
434
+ }
435
+ }
436
+ };
437
+ _handleTagRemove = (e) => {
438
+ e.stopPropagation();
439
+ const target = e.currentTarget;
440
+ const value = target.dataset.value;
441
+ if (value) {
442
+ this._removeValue(value);
443
+ }
444
+ };
445
+ }
446
+
447
+ // src/index.ts
448
+ function register() {
449
+ if (!customElements.get("el-dm-autocomplete")) {
450
+ customElements.define("el-dm-autocomplete", ElDmAutocomplete);
451
+ }
452
+ }