@samline/autocomplete 0.0.0-stage → 2.0.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +314 -2
  4. package/dist/browser/global.global.js +668 -0
  5. package/dist/browser/global.global.js.map +1 -0
  6. package/dist/browser/index.cjs +663 -0
  7. package/dist/browser/index.cjs.map +1 -0
  8. package/dist/browser/index.d.cts +31 -0
  9. package/dist/browser/index.d.ts +31 -0
  10. package/dist/browser/index.js +24 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/chunk-4IPBCQDZ.js +218 -0
  13. package/dist/chunk-4IPBCQDZ.js.map +1 -0
  14. package/dist/chunk-5UZVFHB4.js +70 -0
  15. package/dist/chunk-5UZVFHB4.js.map +1 -0
  16. package/dist/chunk-AD7T42HJ.js +1 -0
  17. package/dist/chunk-AD7T42HJ.js.map +1 -0
  18. package/dist/chunk-PPDQPLPE.js +1 -0
  19. package/dist/chunk-PPDQPLPE.js.map +1 -0
  20. package/dist/chunk-U5JYDGKW.js +403 -0
  21. package/dist/chunk-U5JYDGKW.js.map +1 -0
  22. package/dist/core/index.cjs +181 -0
  23. package/dist/core/index.cjs.map +1 -0
  24. package/dist/core/index.d.cts +31 -0
  25. package/dist/core/index.d.ts +31 -0
  26. package/dist/core/index.js +22 -0
  27. package/dist/core/index.js.map +1 -0
  28. package/dist/index.cjs +697 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +22 -0
  31. package/dist/index.d.ts +22 -0
  32. package/dist/index.js +61 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/render-B38UXYCT.d.ts +7 -0
  35. package/dist/render-WD69VMUs.d.cts +7 -0
  36. package/dist/styles.css +61 -0
  37. package/dist/types-CPvjhqjb.d.cts +118 -0
  38. package/dist/types-CPvjhqjb.d.ts +118 -0
  39. package/dist/vanilla/index.cjs +598 -0
  40. package/dist/vanilla/index.cjs.map +1 -0
  41. package/dist/vanilla/index.d.cts +11 -0
  42. package/dist/vanilla/index.d.ts +11 -0
  43. package/dist/vanilla/index.js +13 -0
  44. package/dist/vanilla/index.js.map +1 -0
  45. package/package.json +127 -4
@@ -0,0 +1,663 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/browser/index.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ Autocomplete: () => Autocomplete,
24
+ available: () => available,
25
+ browser: () => browser,
26
+ default: () => Autocomplete,
27
+ destroyAutocomplete: () => destroyAutocomplete,
28
+ destroyAutocompletes: () => destroyAutocompletes,
29
+ getAutocomplete: () => getAutocomplete,
30
+ getAutocompletes: () => getAutocompletes,
31
+ newAutocomplete: () => newAutocomplete
32
+ });
33
+ module.exports = __toCommonJS(browser_exports);
34
+
35
+ // src/constants.ts
36
+ var DEFAULT_QUERY_PARAM = "q";
37
+ var DEFAULT_MIN_LENGTH = 2;
38
+ var DEFAULT_DEBOUNCE = 250;
39
+ var DEFAULT_MAX_RESULTS = 8;
40
+ var DEFAULT_CACHE_SIZE = 50;
41
+ var DEFAULT_CACHE_TTL = 3e5;
42
+ var POPUP_ATTRIBUTE = "data-autocomplete-popup";
43
+ var LIST_ATTRIBUTE = "data-autocomplete-list";
44
+ var OPTION_ATTRIBUTE = "data-autocomplete-option";
45
+ var STATUS_ATTRIBUTE = "data-autocomplete-status";
46
+ var LIVE_ATTRIBUTE = "data-autocomplete-live";
47
+ var HIGHLIGHT_ATTRIBUTE = "data-autocomplete-highlight";
48
+
49
+ // src/core/cache.ts
50
+ var createCache = ({ max = DEFAULT_CACHE_SIZE, ttl = DEFAULT_CACHE_TTL } = {}, now = Date.now) => {
51
+ const entries = /* @__PURE__ */ new Map();
52
+ const get = (key) => {
53
+ const entry = entries.get(key);
54
+ if (!entry) return void 0;
55
+ entries.delete(key);
56
+ if (entry.expires <= now()) return void 0;
57
+ entries.set(key, entry);
58
+ return entry.value;
59
+ };
60
+ const set = (key, value) => {
61
+ entries.delete(key);
62
+ entries.set(key, { value, expires: now() + ttl });
63
+ while (entries.size > max) {
64
+ const oldest = entries.keys().next();
65
+ if (oldest.done) break;
66
+ entries.delete(oldest.value);
67
+ }
68
+ };
69
+ return {
70
+ get,
71
+ set,
72
+ clear: () => entries.clear(),
73
+ get size() {
74
+ return entries.size;
75
+ }
76
+ };
77
+ };
78
+
79
+ // src/core/source.ts
80
+ var parseJsonResponse = async (response) => response.json();
81
+ var invalidPayloadError = () => new TypeError(
82
+ "Autocomplete response does not contain an array. Configure mapResponse(payload, response, query)."
83
+ );
84
+ var mapCommonResponse = (payload) => {
85
+ if (Array.isArray(payload)) return payload;
86
+ if (!payload || typeof payload !== "object") throw invalidPayloadError();
87
+ const record = payload;
88
+ if (Array.isArray(record.suggestions)) return record.suggestions;
89
+ if (Array.isArray(record.items)) return record.items;
90
+ if (Array.isArray(record.data)) return record.data;
91
+ throw invalidPayloadError();
92
+ };
93
+ var fromEndpoint = ({
94
+ endpoint,
95
+ queryParam = DEFAULT_QUERY_PARAM,
96
+ requestInit = {},
97
+ parseResponse = parseJsonResponse,
98
+ mapResponse = mapCommonResponse
99
+ }) => {
100
+ if (!endpoint) throw new TypeError("Autocomplete requires an endpoint.");
101
+ return async (query, { signal, input }) => {
102
+ const resolved = typeof endpoint === "function" ? endpoint(query, input) : endpoint;
103
+ const url = new URL(resolved, document.baseURI);
104
+ if (queryParam !== null) url.searchParams.set(queryParam, query);
105
+ const init = typeof requestInit === "function" ? requestInit(query, input) : requestInit;
106
+ const response = await fetch(url, { ...init, signal });
107
+ if (!response.ok) throw new Error(`Autocomplete request failed with HTTP ${response.status}.`);
108
+ const payload = await parseResponse(response, query);
109
+ const result = await mapResponse(payload, response, query);
110
+ if (!Array.isArray(result))
111
+ throw new TypeError("Autocomplete mapResponse must return an array.");
112
+ return result;
113
+ };
114
+ };
115
+
116
+ // src/core/state.ts
117
+ var DEFAULT_MESSAGES = {
118
+ loading: "Searching...",
119
+ empty: "No results",
120
+ error: "Search failed",
121
+ results: ({ count }) => count === 1 ? "1 result available" : `${count} results available`
122
+ };
123
+ var resolveMessage = (message, context) => typeof message === "function" ? message(context) : message;
124
+ var readField = (suggestion, keys) => {
125
+ if (typeof suggestion === "string") return suggestion;
126
+ if (!suggestion || typeof suggestion !== "object") return String(suggestion ?? "");
127
+ const record = suggestion;
128
+ const key = keys.find((name) => record[name] !== void 0 && record[name] !== null);
129
+ return key === void 0 ? "" : String(record[key]);
130
+ };
131
+ var getDefaultLabel = (suggestion) => readField(suggestion, ["label", "value", "title", "name"]);
132
+ var getDefaultValue = (suggestion) => readField(suggestion, ["value", "label", "title", "name"]);
133
+ var getDefaultId = (suggestion, index) => {
134
+ if (!suggestion || typeof suggestion !== "object") return index;
135
+ return suggestion.id ?? index;
136
+ };
137
+ var assertNonNegativeInteger = (value, name) => {
138
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
139
+ throw new TypeError(`Autocomplete ${name} must be a non-negative integer.`);
140
+ }
141
+ };
142
+ var assertPositiveInteger = (value, name) => {
143
+ if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
144
+ throw new TypeError(`Autocomplete ${name} must be an integer greater than zero.`);
145
+ }
146
+ };
147
+ var validateOptions = (options) => {
148
+ const hasSource = options.source !== void 0;
149
+ const hasEndpoint = options.endpoint !== void 0;
150
+ if (hasSource && hasEndpoint) {
151
+ throw new TypeError("Autocomplete accepts either a source or an endpoint, not both.");
152
+ }
153
+ if (!hasSource && !options.endpoint) {
154
+ throw new TypeError("Autocomplete requires a source or an endpoint.");
155
+ }
156
+ if (hasSource && !Array.isArray(options.source) && typeof options.source !== "function") {
157
+ throw new TypeError("Autocomplete source must be an array or a function.");
158
+ }
159
+ assertNonNegativeInteger(options.minLength, "minLength");
160
+ assertNonNegativeInteger(options.debounce, "debounce");
161
+ assertPositiveInteger(options.maxResults, "maxResults");
162
+ if (typeof options.cache === "object") {
163
+ assertPositiveInteger(options.cache.max, "cache.max");
164
+ assertNonNegativeInteger(options.cache.ttl, "cache.ttl");
165
+ }
166
+ };
167
+
168
+ // src/core/text.ts
169
+ var COMBINING_MARKS = /\p{M}/gu;
170
+ var WHITESPACE = /\s+/;
171
+ var normalizeText = (value) => value.normalize("NFD").replace(COMBINING_MARKS, "").toLowerCase();
172
+ var getTerms = (query, normalize) => Array.from(new Set(normalize(query).split(WHITESPACE).filter(Boolean)));
173
+ var filterSuggestions = (suggestions, query, { getText, normalize = normalizeText }) => {
174
+ const terms = getTerms(query, normalize);
175
+ if (terms.length === 0) return [...suggestions];
176
+ return suggestions.filter((suggestion) => {
177
+ const text = normalize(getText(suggestion));
178
+ return terms.every((term) => text.includes(term));
179
+ });
180
+ };
181
+ var highlight = (text, query, normalize = normalizeText) => {
182
+ const terms = getTerms(query, normalize);
183
+ if (terms.length === 0 || text === "") return text === "" ? [] : [{ text, match: false }];
184
+ let normalized = "";
185
+ const starts = [];
186
+ const ends = [];
187
+ let offset = 0;
188
+ for (const char of text) {
189
+ const piece = normalize(char);
190
+ if (piece === "" && ends.length > 0) ends[ends.length - 1] = offset + char.length;
191
+ normalized += piece;
192
+ for (let index = 0; index < piece.length; index += 1) {
193
+ starts.push(offset);
194
+ ends.push(offset + char.length);
195
+ }
196
+ offset += char.length;
197
+ }
198
+ const ranges = [];
199
+ for (const term of terms) {
200
+ let found = normalized.indexOf(term);
201
+ while (found !== -1) {
202
+ ranges.push([starts[found], ends[found + term.length - 1]]);
203
+ found = normalized.indexOf(term, found + 1);
204
+ }
205
+ }
206
+ if (ranges.length === 0) return [{ text, match: false }];
207
+ ranges.sort((a, b) => a[0] - b[0]);
208
+ const merged = [];
209
+ for (const range of ranges) {
210
+ const last = merged[merged.length - 1];
211
+ if (last && range[0] <= last[1]) last[1] = Math.max(last[1], range[1]);
212
+ else merged.push([range[0], range[1]]);
213
+ }
214
+ const parts = [];
215
+ let cursor = 0;
216
+ for (const [start, end] of merged) {
217
+ if (start > cursor) parts.push({ text: text.slice(cursor, start), match: false });
218
+ parts.push({ text: text.slice(start, end), match: true });
219
+ cursor = end;
220
+ }
221
+ if (cursor < text.length) parts.push({ text: text.slice(cursor), match: false });
222
+ return parts;
223
+ };
224
+
225
+ // src/vanilla/dom.ts
226
+ var resolveInput = (target) => {
227
+ const element = typeof target === "string" ? document.querySelector(target) : target;
228
+ if (!(element instanceof HTMLInputElement)) {
229
+ throw new TypeError("Autocomplete requires an HTMLInputElement or a valid selector.");
230
+ }
231
+ return element;
232
+ };
233
+ var sanitizeId = (value) => String(value).replace(/[^a-zA-Z0-9_-]/g, "-");
234
+ var restoreAttribute = (element, name, value) => {
235
+ if (value === null) element.removeAttribute(name);
236
+ else element.setAttribute(name, value);
237
+ };
238
+ var renderHighlight = (element, parts) => {
239
+ for (const part of parts) {
240
+ if (!part.match) {
241
+ element.append(document.createTextNode(part.text));
242
+ continue;
243
+ }
244
+ const mark = document.createElement("mark");
245
+ mark.setAttribute(HIGHLIGHT_ATTRIBUTE, "");
246
+ mark.textContent = part.text;
247
+ element.append(mark);
248
+ }
249
+ };
250
+ var VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0";
251
+
252
+ // src/vanilla/render.ts
253
+ var RESTORED_ATTRIBUTES = [
254
+ "role",
255
+ "autocomplete",
256
+ "aria-autocomplete",
257
+ "aria-controls",
258
+ "aria-expanded",
259
+ "aria-activedescendant"
260
+ ];
261
+ var instanceCount = 0;
262
+ var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
263
+ var autocomplete = (target, options) => {
264
+ validateOptions(options);
265
+ const input = resolveInput(target);
266
+ const instanceId = `autocomplete-${++instanceCount}`;
267
+ const messages = { ...DEFAULT_MESSAGES, ...options.messages };
268
+ const localSuggestions = Array.isArray(options.source) ? options.source : null;
269
+ const getLabel = options.getLabel ?? getDefaultLabel;
270
+ const getValue = options.getValue ?? getDefaultValue;
271
+ const getId = options.getId ?? getDefaultId;
272
+ const normalize = options.normalize ?? normalizeText;
273
+ const minLength = options.minLength ?? (localSuggestions ? 1 : DEFAULT_MIN_LENGTH);
274
+ const debounce = options.debounce ?? (localSuggestions ? 0 : DEFAULT_DEBOUNCE);
275
+ const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
276
+ const searchOnFocus = options.searchOnFocus ?? true;
277
+ const highlightEnabled = options.highlight ?? true;
278
+ const source = localSuggestions ? null : typeof options.source === "function" ? options.source : fromEndpoint(options);
279
+ const cache = source && options.cache !== false ? createCache(typeof options.cache === "object" ? options.cache : {}) : null;
280
+ const popup = document.createElement("div");
281
+ const list = document.createElement("div");
282
+ const status = document.createElement("div");
283
+ const live = document.createElement("div");
284
+ const originalAttributes = /* @__PURE__ */ new Map();
285
+ const label = input.labels?.[0] ?? null;
286
+ const originalLabelId = label?.getAttribute("id") ?? null;
287
+ let suggestions = [];
288
+ let activeIndex = -1;
289
+ let selected = null;
290
+ let debounceTimer;
291
+ let abortController = null;
292
+ let requestSequence = 0;
293
+ let destroyed = false;
294
+ let suppressFocusSearch = false;
295
+ popup.hidden = true;
296
+ popup.setAttribute(POPUP_ATTRIBUTE, "");
297
+ list.id = `${instanceId}-listbox`;
298
+ list.hidden = true;
299
+ list.role = "listbox";
300
+ list.setAttribute(LIST_ATTRIBUTE, "");
301
+ status.hidden = true;
302
+ status.setAttribute(STATUS_ATTRIBUTE, "");
303
+ status.setAttribute("aria-hidden", "true");
304
+ live.role = "status";
305
+ live.setAttribute("aria-live", "polite");
306
+ live.setAttribute("aria-atomic", "true");
307
+ live.setAttribute(LIVE_ATTRIBUTE, "");
308
+ live.style.cssText = VISUALLY_HIDDEN;
309
+ if (label) {
310
+ if (!label.id) label.id = `${instanceId}-label`;
311
+ list.setAttribute("aria-labelledby", label.id);
312
+ }
313
+ for (const name of RESTORED_ATTRIBUTES) originalAttributes.set(name, input.getAttribute(name));
314
+ input.role = "combobox";
315
+ input.autocomplete = "off";
316
+ input.setAttribute("aria-autocomplete", "list");
317
+ input.setAttribute("aria-controls", list.id);
318
+ input.setAttribute("aria-expanded", "false");
319
+ popup.append(list, status);
320
+ input.insertAdjacentElement("afterend", popup);
321
+ popup.insertAdjacentElement("afterend", live);
322
+ const dispatch = (name, detail) => {
323
+ input.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
324
+ };
325
+ const announce = (message) => {
326
+ live.textContent = message;
327
+ };
328
+ const cancel = () => {
329
+ clearTimeout(debounceTimer);
330
+ debounceTimer = void 0;
331
+ requestSequence += 1;
332
+ abortController?.abort();
333
+ abortController = null;
334
+ };
335
+ const updateActiveOption = () => {
336
+ const elements = list.querySelectorAll(`[${OPTION_ATTRIBUTE}]`);
337
+ elements.forEach((element, index) => {
338
+ const active2 = index === activeIndex;
339
+ element.dataset.active = String(active2);
340
+ element.setAttribute("aria-selected", String(active2));
341
+ });
342
+ const active = elements.item(activeIndex);
343
+ if (!active) {
344
+ input.removeAttribute("aria-activedescendant");
345
+ return;
346
+ }
347
+ input.setAttribute("aria-activedescendant", active.id);
348
+ active.scrollIntoView({ block: "nearest" });
349
+ };
350
+ const close = () => {
351
+ activeIndex = -1;
352
+ popup.hidden = true;
353
+ list.hidden = true;
354
+ status.hidden = true;
355
+ input.setAttribute("aria-expanded", "false");
356
+ input.removeAttribute("aria-activedescendant");
357
+ updateActiveOption();
358
+ };
359
+ const dismiss = () => {
360
+ cancel();
361
+ close();
362
+ };
363
+ const renderStatus = (state, query) => {
364
+ if (destroyed) return;
365
+ const message = resolveMessage(messages[state], { query, count: 0 });
366
+ list.replaceChildren();
367
+ list.hidden = true;
368
+ status.textContent = message;
369
+ status.setAttribute(STATUS_ATTRIBUTE, state);
370
+ status.hidden = false;
371
+ popup.hidden = false;
372
+ input.setAttribute("aria-expanded", "false");
373
+ input.removeAttribute("aria-activedescendant");
374
+ if (state !== "loading") announce(message);
375
+ };
376
+ const select = (index) => {
377
+ const suggestion = suggestions[index];
378
+ if (!suggestion) return;
379
+ selected = suggestion.data;
380
+ input.value = suggestion.value;
381
+ dismiss();
382
+ dispatch("autocomplete:select", {
383
+ suggestion: suggestion.data,
384
+ label: suggestion.label,
385
+ value: suggestion.value
386
+ });
387
+ options.onSelect?.(suggestion.data, input);
388
+ };
389
+ const handleOptionPointerDown = (event) => {
390
+ event.preventDefault();
391
+ select(Number(event.currentTarget.dataset.index));
392
+ if (document.activeElement !== input) {
393
+ suppressFocusSearch = true;
394
+ input.focus({ preventScroll: true });
395
+ suppressFocusSearch = false;
396
+ }
397
+ };
398
+ const handleOptionPointerEnter = (event) => {
399
+ activeIndex = Number(event.currentTarget.dataset.index);
400
+ updateActiveOption();
401
+ };
402
+ const renderSuggestions = (query) => {
403
+ const fragment = document.createDocumentFragment();
404
+ suggestions.forEach((suggestion, index) => {
405
+ const option = document.createElement("div");
406
+ const parts = highlightEnabled ? highlight(suggestion.label, query, normalize) : [{ text: suggestion.label, match: false }];
407
+ option.id = suggestion.id;
408
+ option.role = "option";
409
+ option.setAttribute(OPTION_ATTRIBUTE, "");
410
+ option.dataset.index = String(index);
411
+ option.setAttribute("aria-selected", "false");
412
+ renderHighlight(option, parts);
413
+ options.renderOption?.(option, suggestion.data, {
414
+ query,
415
+ index,
416
+ label: suggestion.label,
417
+ highlight: parts
418
+ });
419
+ option.addEventListener("pointerdown", handleOptionPointerDown);
420
+ option.addEventListener("pointerenter", handleOptionPointerEnter);
421
+ fragment.append(option);
422
+ });
423
+ list.replaceChildren(fragment);
424
+ list.hidden = false;
425
+ status.hidden = true;
426
+ popup.hidden = false;
427
+ input.setAttribute("aria-expanded", "true");
428
+ };
429
+ const normalizeSuggestion = (suggestion, index) => ({
430
+ id: `${instanceId}-option-${sanitizeId(getId(suggestion, index))}`,
431
+ label: String(getLabel(suggestion)),
432
+ value: String(getValue(suggestion)),
433
+ data: suggestion
434
+ });
435
+ const showResults = (query, result, cached) => {
436
+ if (destroyed) return;
437
+ suggestions = result.slice(0, maxResults).map(normalizeSuggestion);
438
+ activeIndex = -1;
439
+ if (suggestions.length === 0) renderStatus("empty", query);
440
+ else {
441
+ renderSuggestions(query);
442
+ announce(resolveMessage(messages.results, { query, count: suggestions.length }));
443
+ }
444
+ dispatch("autocomplete:load", {
445
+ query,
446
+ suggestions: suggestions.map((suggestion) => suggestion.data),
447
+ cached
448
+ });
449
+ };
450
+ const filterLocal = (items, query) => {
451
+ const filter = options.filter;
452
+ return filter ? items.filter((item) => filter(item, query)) : filterSuggestions(items, query, { getText: getLabel, normalize });
453
+ };
454
+ const search = async (query = input.value.trim()) => {
455
+ if (destroyed || query.length < minLength) {
456
+ close();
457
+ return;
458
+ }
459
+ cancel();
460
+ const sequence = ++requestSequence;
461
+ if (localSuggestions) {
462
+ showResults(query, filterLocal(localSuggestions, query), false);
463
+ return;
464
+ }
465
+ const hit = cache?.get(query);
466
+ if (hit) {
467
+ showResults(query, hit, true);
468
+ return;
469
+ }
470
+ const controller = new AbortController();
471
+ abortController = controller;
472
+ renderStatus("loading", query);
473
+ dispatch("autocomplete:loadstart", { query });
474
+ try {
475
+ const result = await source(query, { signal: controller.signal, input });
476
+ if (!Array.isArray(result)) throw new TypeError("Autocomplete source must return an array.");
477
+ if (sequence !== requestSequence) return;
478
+ cache?.set(query, result);
479
+ showResults(query, result, false);
480
+ } catch (error) {
481
+ if (isAbortError(error) || sequence !== requestSequence || destroyed) return;
482
+ suggestions = [];
483
+ activeIndex = -1;
484
+ renderStatus("error", query);
485
+ dispatch("autocomplete:error", { query, error });
486
+ options.onError?.(error, input);
487
+ } finally {
488
+ if (sequence === requestSequence) abortController = null;
489
+ }
490
+ };
491
+ const scheduleSearch = () => {
492
+ clearTimeout(debounceTimer);
493
+ const query = input.value.trim();
494
+ if (selected !== null) {
495
+ selected = null;
496
+ dispatch("autocomplete:clear", { query });
497
+ }
498
+ if (query.length < minLength) {
499
+ dismiss();
500
+ return;
501
+ }
502
+ if (debounce === 0) {
503
+ void search(query);
504
+ return;
505
+ }
506
+ debounceTimer = setTimeout(() => void search(query), debounce);
507
+ };
508
+ const clear = () => {
509
+ const hadSelection = selected !== null;
510
+ cancel();
511
+ suggestions = [];
512
+ activeIndex = -1;
513
+ selected = null;
514
+ input.value = "";
515
+ close();
516
+ announce("");
517
+ if (hadSelection) dispatch("autocomplete:clear", { query: "" });
518
+ };
519
+ const moveActiveOption = (direction) => {
520
+ if (list.hidden || suggestions.length === 0) return;
521
+ activeIndex = activeIndex === -1 ? direction > 0 ? 0 : suggestions.length - 1 : (activeIndex + direction + suggestions.length) % suggestions.length;
522
+ updateActiveOption();
523
+ };
524
+ const handleInput = (event) => {
525
+ if (event.isComposing) return;
526
+ scheduleSearch();
527
+ };
528
+ const handleFocus = () => {
529
+ if (suppressFocusSearch) return;
530
+ if (searchOnFocus && input.value.trim().length >= minLength) scheduleSearch();
531
+ };
532
+ const handleKeydown = (event) => {
533
+ const listOpen = !list.hidden && suggestions.length > 0;
534
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
535
+ if (!listOpen) return;
536
+ event.preventDefault();
537
+ moveActiveOption(event.key === "ArrowDown" ? 1 : -1);
538
+ return;
539
+ }
540
+ if (event.key === "Enter" && listOpen && activeIndex >= 0) {
541
+ event.preventDefault();
542
+ select(activeIndex);
543
+ return;
544
+ }
545
+ if ((event.key === "Home" || event.key === "End") && listOpen) {
546
+ event.preventDefault();
547
+ activeIndex = event.key === "Home" ? 0 : suggestions.length - 1;
548
+ updateActiveOption();
549
+ return;
550
+ }
551
+ if (event.key === "Escape") {
552
+ event.preventDefault();
553
+ dismiss();
554
+ return;
555
+ }
556
+ if (event.key === "Tab") dismiss();
557
+ };
558
+ const handleDocumentPointerDown = (event) => {
559
+ if (event.target !== input && !popup.contains(event.target)) dismiss();
560
+ };
561
+ const destroy = () => {
562
+ if (destroyed) return;
563
+ destroyed = true;
564
+ cancel();
565
+ input.removeEventListener("input", handleInput);
566
+ input.removeEventListener("compositionend", scheduleSearch);
567
+ input.removeEventListener("focus", handleFocus);
568
+ input.removeEventListener("keydown", handleKeydown);
569
+ document.removeEventListener("pointerdown", handleDocumentPointerDown);
570
+ popup.remove();
571
+ live.remove();
572
+ cache?.clear();
573
+ originalAttributes.forEach((value, name) => restoreAttribute(input, name, value));
574
+ if (label) restoreAttribute(label, "id", originalLabelId);
575
+ };
576
+ input.addEventListener("input", handleInput);
577
+ input.addEventListener("compositionend", scheduleSearch);
578
+ input.addEventListener("focus", handleFocus);
579
+ input.addEventListener("keydown", handleKeydown);
580
+ document.addEventListener("pointerdown", handleDocumentPointerDown);
581
+ return {
582
+ element: input,
583
+ popupElement: popup,
584
+ listElement: list,
585
+ statusElement: status,
586
+ liveElement: live,
587
+ options,
588
+ search,
589
+ close: dismiss,
590
+ clear,
591
+ cancel,
592
+ clearCache: () => cache?.clear(),
593
+ getSelected: () => selected,
594
+ destroy
595
+ };
596
+ };
597
+
598
+ // src/browser/registry.ts
599
+ var available = /* @__PURE__ */ Object.create(null);
600
+ var removeRegistration = (id) => {
601
+ const entry = available[id];
602
+ if (!entry) return;
603
+ delete available[id];
604
+ entry.destroy();
605
+ };
606
+ var newAutocomplete = ({
607
+ id,
608
+ options
609
+ }) => {
610
+ if (!id) {
611
+ console.error("Autocomplete ID is required");
612
+ return null;
613
+ }
614
+ const input = document.getElementById(id);
615
+ if (!(input instanceof HTMLInputElement)) {
616
+ console.warn(`Autocomplete input with ID ${id} not found`);
617
+ return null;
618
+ }
619
+ removeRegistration(id);
620
+ const controller = autocomplete(input, options);
621
+ const destroy = controller.destroy;
622
+ const registered = {
623
+ ...controller,
624
+ destroy: () => {
625
+ if (available[id] === registered) delete available[id];
626
+ destroy();
627
+ }
628
+ };
629
+ available[id] = registered;
630
+ return registered;
631
+ };
632
+ var getAutocomplete = (id) => available[id] ?? null;
633
+ var getAutocompletes = () => ({
634
+ ...available
635
+ });
636
+ var destroyAutocomplete = (target) => {
637
+ if (typeof target === "string") return removeRegistration(target);
638
+ const entry = Object.entries(available).find(([, controller]) => controller.element === target);
639
+ if (entry) removeRegistration(entry[0]);
640
+ };
641
+ var destroyAutocompletes = () => Object.keys(available).forEach(removeRegistration);
642
+ var Autocomplete = {
643
+ autocomplete,
644
+ newAutocomplete,
645
+ getAutocomplete,
646
+ getAutocompletes,
647
+ destroyAutocomplete,
648
+ destroyAutocompletes,
649
+ available
650
+ };
651
+ var browser = Autocomplete;
652
+ // Annotate the CommonJS export names for ESM import in node:
653
+ 0 && (module.exports = {
654
+ Autocomplete,
655
+ available,
656
+ browser,
657
+ destroyAutocomplete,
658
+ destroyAutocompletes,
659
+ getAutocomplete,
660
+ getAutocompletes,
661
+ newAutocomplete
662
+ });
663
+ //# sourceMappingURL=index.cjs.map