@samline/autocomplete 0.0.0-stage → 1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@samline/autocomplete` are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [1.0.0] - 2026-11-13
6
+
7
+ First public release.
8
+
9
+ ### Added
10
+
11
+ - `createAutocomplete(input, options)` (alias `autocomplete`) turns an existing text input into an accessible remote combobox: debounce, abortable requests, stale-response protection, listbox rendering, keyboard and pointer selection, events and cleanup.
12
+ - Options: `endpoint`, `queryParam`, `requestInit`, `parseResponse`, `mapResponse`, `getId`, `getLabel`, `getValue`, `renderOption`, `minLength`, `debounce`, `maxResults`, `searchOnFocus`, `messages`, `onSelect` and `onError`.
13
+ - Events `autocomplete:loadstart`, `autocomplete:load`, `autocomplete:error`, `autocomplete:select` and `autocomplete:clear`.
14
+ - Keyboard: ArrowUp, ArrowDown, Home, End, Enter, Escape and Tab. Searches wait for IME composition to finish.
15
+ - Browser global `window.Autocomplete` with `create`, `destroy`, `available` and `autocomplete`, and an optional stylesheet themed with `--autocomplete-*` custom properties.
16
+ - Website with examples and documentation at https://samline.github.io/autocomplete/.
17
+
18
+ ### Fixed (since the unpublished 0.1.0)
19
+
20
+ - Escape, Tab, clicking outside, selecting a suggestion and `close()` cancel the pending debounced search and abort the request in flight. Before, a late response reopened the list after it was dismissed.
21
+ - The standalone script defines `window.Autocomplete` with its documented members. Before, the global was wrapped as `{ default }`.
22
+
23
+ [1.0.0]: https://github.com/samline/autocomplete/releases/tag/v1.0.0
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 samline
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,139 @@
1
- # Temporary Holding Version
1
+ # @samline/autocomplete
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An accessible, framework-agnostic remote autocomplete (combobox) for vanilla JavaScript, TypeScript and plain HTML. Point an input at your endpoint; debounce, cancellation, keyboard navigation, events and cleanup are handled for you.
4
+
5
+ [Demo](https://samline.github.io/autocomplete/) · [Examples](https://samline.github.io/autocomplete/examples/) · [Documentation](https://samline.github.io/autocomplete/docs/) · [Changelog](CHANGELOG.md)
6
+
7
+ ## Table of contents
8
+
9
+ - [Installation](#installation)
10
+ - [Quick start](#quick-start)
11
+ - [HTML / CDN](#html--cdn)
12
+ - [Capabilities](#capabilities)
13
+ - [API at a glance](#api-at-a-glance)
14
+ - [Documentation](#documentation)
15
+ - [Changelog](#changelog)
16
+ - [AI agent skill](#ai-agent-skill)
17
+ - [License](#license)
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ npm install @samline/autocomplete
23
+ ```
24
+
25
+ ```sh
26
+ pnpm add @samline/autocomplete
27
+ ```
28
+
29
+ ```sh
30
+ bun add @samline/autocomplete
31
+ ```
32
+
33
+ Works in modern browsers with `fetch` and `AbortController`, and imports in Node 20 or newer. No runtime dependencies; the stylesheet is optional.
34
+
35
+ ## Quick start
36
+
37
+ ```html
38
+ <div class="field" style="position: relative">
39
+ <label for="city">City</label>
40
+ <input id="city" type="text" />
41
+ </div>
42
+ ```
43
+
44
+ ```js
45
+ import { createAutocomplete } from '@samline/autocomplete'
46
+ import '@samline/autocomplete/styles.css'
47
+
48
+ const controller = createAutocomplete('#city', {
49
+ endpoint: '/api/cities', // GET /api/cities?q=<query>
50
+ getId: city => city.id,
51
+ getLabel: city => city.name,
52
+ onSelect: city => console.log('Selected', city),
53
+ })
54
+
55
+ controller.destroy() // removes the list and restores the input
56
+ ```
57
+
58
+ ## HTML / CDN
59
+
60
+ For Shopify, WordPress or plain HTML pages without a build step, load the stylesheet and the browser bundle. Pin the version in production.
61
+
62
+ ```html
63
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@samline/autocomplete@1.0.0/dist/styles.css" />
64
+ <script src="https://cdn.jsdelivr.net/npm/@samline/autocomplete@1.0.0/dist/browser/global.global.js"></script>
65
+ <script>
66
+ window.Autocomplete.create('#city', { endpoint: '/api/cities' })
67
+ </script>
68
+ ```
69
+
70
+ `window.Autocomplete` has `create(target, options)` (requires an input id and registers the controller), `destroy(id)`, `available` and `autocomplete(target, options)`.
71
+
72
+ ## Capabilities
73
+
74
+ ### Any endpoint, any response
75
+
76
+ `endpoint` is a URL or a function of the query. `requestInit` adds headers or credentials, `parseResponse` replaces JSON parsing, and `mapResponse`, `getId`, `getLabel` and `getValue` adapt whatever your API returns.
77
+
78
+ ### An accessible combobox
79
+
80
+ ARIA `combobox` and `listbox` roles, `aria-expanded` and `aria-activedescendant`, status and error messages, and ArrowUp, ArrowDown, Home, End, Enter, Escape and Tab. Searches wait for IME composition to finish.
81
+
82
+ ### No stale results
83
+
84
+ Searches are debounced, superseded requests are aborted and late responses ignored. Closing the list (Escape, Tab, clicking outside, selecting or `close()`) cancels the pending search.
85
+
86
+ ### Events and styling
87
+
88
+ `autocomplete:select`, `autocomplete:clear`, `autocomplete:load`, `autocomplete:loadstart` and `autocomplete:error` bubble from the input. The optional stylesheet is themed with `--autocomplete-*` custom properties, and `renderOption` adds your own content to each option.
89
+
90
+ ## API at a glance
91
+
92
+ | API | Purpose |
93
+ | --- | --- |
94
+ | `createAutocomplete(input, options)` | Attach the combobox and return a controller (`autocomplete` is an alias). |
95
+ | `controller.search(query?)` / `close()` / `clear()` / `cancel()` | Run a search now, close the list, clear everything or cancel pending work. |
96
+ | `controller.getSelected()` / `destroy()` | Read the selected suggestion or remove the combobox. |
97
+ | `mapCommonResponse`, `parseJsonResponse` | The default response mapper and parser. |
98
+
99
+ ### Entrypoints
100
+
101
+ | Entrypoint | When to use |
102
+ | --- | --- |
103
+ | `@samline/autocomplete` | ESM or CommonJS: `createAutocomplete`, helpers, `browser` registry and types. |
104
+ | `@samline/autocomplete/browser` | Classic script that defines `window.Autocomplete`. |
105
+ | `@samline/autocomplete/styles.css` | Optional base stylesheet. |
106
+
107
+ ## Documentation
108
+
109
+ Read the [documentation](https://samline.github.io/autocomplete/docs/) for the full guides and API contracts, and explore [working examples](https://samline.github.io/autocomplete/examples/) with complete and separate source files.
110
+
111
+ | Guide | What it covers |
112
+ | --- | --- |
113
+ | [Installation](https://samline.github.io/autocomplete/docs/getting-started/) | Package / ESM, HTML / CDN and working starter files. |
114
+ | [Options](https://samline.github.io/autocomplete/docs/reference/configuration/) | Every option and its default. |
115
+ | [API reference](https://samline.github.io/autocomplete/docs/reference/api/) | The controller, helpers, browser global and events. |
116
+ | [Accessibility](https://samline.github.io/autocomplete/docs/guides/accessibility/) | What the combobox provides and what your page provides. |
117
+ | [Styling](https://samline.github.io/autocomplete/docs/guides/styling/) | The DOM contract and the custom properties. |
118
+
119
+ ## Changelog
120
+
121
+ Read [CHANGELOG.md](CHANGELOG.md) or the [published changelog](https://samline.github.io/autocomplete/docs/changelog/) for release history and explicitly marked breaking and behavior changes. Review the notes before upgrading.
122
+
123
+ ## AI agent skill
124
+
125
+ The [Autocomplete skill](https://samline.github.io/autocomplete/skills/autocomplete/SKILL.md) teaches verified package integration. Its maintained [repository source](skills/autocomplete/SKILL.md) and website download are the same file. Verify the installed package version before applying its guidance.
126
+
127
+ Download [SKILL.md](https://samline.github.io/autocomplete/skills/autocomplete/SKILL.md) into an `autocomplete/` folder in your agent's supported skill directory. For agents that discover project skills from `.agents/skills/`:
128
+
129
+ ```sh
130
+ mkdir -p .agents/skills/autocomplete
131
+ curl --fail --location https://samline.github.io/autocomplete/skills/autocomplete/SKILL.md \
132
+ --output .agents/skills/autocomplete/SKILL.md
133
+ ```
134
+
135
+ When working from this repository, copy `skills/autocomplete/SKILL.md` instead. Keep the filename `SKILL.md`; discovery paths and invocation syntax depend on the agent.
136
+
137
+ ## License
138
+
139
+ MIT — [LICENSE.md](LICENSE.md).
@@ -0,0 +1,61 @@
1
+ type AutocompleteTarget = string | HTMLInputElement;
2
+ type AutocompleteEndpoint = string | URL | ((query: string, input: HTMLInputElement) => string | URL);
3
+ type AutocompleteRequestInit = RequestInit | ((query: string, input: HTMLInputElement) => RequestInit);
4
+ type AutocompleteParser = (response: Response, query: string) => Promise<unknown>;
5
+ type AutocompleteResponseMapper<T> = (payload: unknown, response: Response, query: string) => readonly T[] | Promise<readonly T[]>;
6
+ interface AutocompleteMessages {
7
+ loading: string;
8
+ empty: string;
9
+ error: string;
10
+ }
11
+ interface AutocompleteOptions<T = unknown> {
12
+ endpoint: AutocompleteEndpoint;
13
+ queryParam?: string | null;
14
+ requestInit?: AutocompleteRequestInit;
15
+ parseResponse?: AutocompleteParser;
16
+ mapResponse?: AutocompleteResponseMapper<T>;
17
+ getId?: (suggestion: T, index: number) => string | number;
18
+ getLabel?: (suggestion: T) => string;
19
+ getValue?: (suggestion: T) => string;
20
+ renderOption?: (element: HTMLButtonElement, suggestion: T) => void;
21
+ minLength?: number;
22
+ debounce?: number;
23
+ maxResults?: number;
24
+ searchOnFocus?: boolean;
25
+ messages?: Partial<AutocompleteMessages>;
26
+ onSelect?: (suggestion: T, input: HTMLInputElement) => void;
27
+ onError?: (error: unknown, input: HTMLInputElement) => void;
28
+ }
29
+ interface AutocompleteController<T = unknown> {
30
+ readonly element: HTMLInputElement;
31
+ readonly listElement: HTMLDivElement;
32
+ readonly options: Readonly<AutocompleteOptions<T>>;
33
+ search(query?: string): Promise<void>;
34
+ close(): void;
35
+ clear(): void;
36
+ cancel(): void;
37
+ getSelected(): T | null;
38
+ destroy(): void;
39
+ }
40
+
41
+ declare function createAutocomplete<T = unknown>(target: AutocompleteTarget, options: AutocompleteOptions<T>): AutocompleteController<T>;
42
+
43
+ type AutocompleteAvailable = Record<string, AutocompleteController<unknown>>;
44
+ interface AutocompleteApi {
45
+ autocomplete: typeof createAutocomplete;
46
+ create: <T>(target: AutocompleteTarget, options: AutocompleteOptions<T>) => AutocompleteController<T>;
47
+ destroy: (id: string) => void;
48
+ available: AutocompleteAvailable;
49
+ }
50
+
51
+ declare global {
52
+ interface Window {
53
+ Autocomplete?: AutocompleteApi;
54
+ }
55
+ }
56
+ declare const autocomplete: typeof createAutocomplete;
57
+ declare const create: <T>(target: AutocompleteTarget, options: AutocompleteOptions<T>) => AutocompleteController<T>;
58
+ declare const destroy: (id: string) => void;
59
+ declare const available: AutocompleteAvailable;
60
+
61
+ export { autocomplete, available, create, destroy };
@@ -0,0 +1,437 @@
1
+ "use strict";
2
+ var Autocomplete = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/browser/global.ts
22
+ var global_exports = {};
23
+ __export(global_exports, {
24
+ autocomplete: () => autocomplete,
25
+ available: () => available2,
26
+ create: () => create2,
27
+ destroy: () => destroy2
28
+ });
29
+
30
+ // src/core/defaults.ts
31
+ var DEFAULT_MESSAGES = {
32
+ loading: "Searching...",
33
+ empty: "No results",
34
+ error: "Search failed"
35
+ };
36
+ var parseJsonResponse = async (response) => response.json();
37
+ var mapCommonResponse = (payload) => {
38
+ if (Array.isArray(payload)) return payload;
39
+ if (!payload || typeof payload !== "object") throw invalidPayloadError();
40
+ const record = payload;
41
+ if (Array.isArray(record.suggestions)) return record.suggestions;
42
+ if (Array.isArray(record.items)) return record.items;
43
+ if (Array.isArray(record.data)) return record.data;
44
+ throw invalidPayloadError();
45
+ };
46
+ function getDefaultLabel(suggestion) {
47
+ if (typeof suggestion === "string") return suggestion;
48
+ if (!suggestion || typeof suggestion !== "object") return String(suggestion ?? "");
49
+ const record = suggestion;
50
+ return String(record.label ?? record.value ?? record.title ?? "");
51
+ }
52
+ function getDefaultValue(suggestion) {
53
+ if (typeof suggestion === "string") return suggestion;
54
+ if (!suggestion || typeof suggestion !== "object") return String(suggestion ?? "");
55
+ const record = suggestion;
56
+ return String(record.value ?? record.label ?? record.title ?? "");
57
+ }
58
+ function getDefaultId(suggestion, index) {
59
+ if (!suggestion || typeof suggestion !== "object") return index;
60
+ return suggestion.id ?? index;
61
+ }
62
+ function validateOptions(options) {
63
+ if (!options.endpoint) throw new TypeError("Autocomplete requires an endpoint.");
64
+ assertNonNegativeInteger(options.minLength, "minLength");
65
+ assertNonNegativeInteger(options.debounce, "debounce");
66
+ if (options.maxResults !== void 0 && (!Number.isInteger(options.maxResults) || options.maxResults < 1)) {
67
+ throw new TypeError("Autocomplete maxResults must be an integer greater than zero.");
68
+ }
69
+ }
70
+ function assertNonNegativeInteger(value, name) {
71
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
72
+ throw new TypeError(`Autocomplete ${name} must be a non-negative integer.`);
73
+ }
74
+ }
75
+ function invalidPayloadError() {
76
+ return new TypeError(
77
+ "Autocomplete response does not contain an array. Configure mapResponse(payload, response, query)."
78
+ );
79
+ }
80
+
81
+ // src/core/dom.ts
82
+ function resolveInput(target) {
83
+ const element = typeof target === "string" ? document.querySelector(target) : target;
84
+ if (!(element instanceof HTMLInputElement)) {
85
+ throw new TypeError("Autocomplete requires an HTMLInputElement or a valid selector.");
86
+ }
87
+ return element;
88
+ }
89
+ function sanitizeId(value) {
90
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, "-");
91
+ }
92
+ function restoreAttribute(element, name, value) {
93
+ if (value === null) {
94
+ element.removeAttribute(name);
95
+ return;
96
+ }
97
+ element.setAttribute(name, value);
98
+ }
99
+
100
+ // src/core/controller.ts
101
+ var instanceCount = 0;
102
+ function createAutocompleteController(target, options) {
103
+ validateOptions(options);
104
+ const input = resolveInput(target);
105
+ const instanceId = `autocomplete-${++instanceCount}`;
106
+ const messages = { ...DEFAULT_MESSAGES, ...options.messages };
107
+ const mapResponse = options.mapResponse ?? mapCommonResponse;
108
+ const settings = {
109
+ queryParam: options.queryParam === void 0 ? "q" : options.queryParam,
110
+ requestInit: options.requestInit ?? {},
111
+ parseResponse: options.parseResponse ?? parseJsonResponse,
112
+ mapResponse,
113
+ getId: options.getId ?? getDefaultId,
114
+ getLabel: options.getLabel ?? getDefaultLabel,
115
+ getValue: options.getValue ?? getDefaultValue,
116
+ renderOption: options.renderOption,
117
+ minLength: options.minLength ?? 2,
118
+ debounce: options.debounce ?? 250,
119
+ maxResults: options.maxResults ?? 8,
120
+ searchOnFocus: options.searchOnFocus ?? true,
121
+ onSelect: options.onSelect,
122
+ onError: options.onError
123
+ };
124
+ const list = document.createElement("div");
125
+ const originalAttributes = /* @__PURE__ */ new Map();
126
+ let suggestions = [];
127
+ let activeIndex = -1;
128
+ let selected = null;
129
+ let debounceTimer;
130
+ let abortController = null;
131
+ let requestSequence = 0;
132
+ let destroyed = false;
133
+ let suppressFocusSearch = false;
134
+ list.id = `${instanceId}-listbox`;
135
+ list.hidden = true;
136
+ list.role = "listbox";
137
+ list.dataset.autocompleteList = "";
138
+ for (const name of [
139
+ "role",
140
+ "autocomplete",
141
+ "aria-autocomplete",
142
+ "aria-controls",
143
+ "aria-expanded",
144
+ "aria-activedescendant"
145
+ ]) {
146
+ originalAttributes.set(name, input.getAttribute(name));
147
+ }
148
+ input.role = "combobox";
149
+ input.autocomplete = "off";
150
+ input.setAttribute("aria-autocomplete", "list");
151
+ input.setAttribute("aria-controls", list.id);
152
+ input.setAttribute("aria-expanded", "false");
153
+ input.insertAdjacentElement("afterend", list);
154
+ function buildUrl(query) {
155
+ const endpoint = typeof options.endpoint === "function" ? options.endpoint(query, input) : options.endpoint;
156
+ const url = new URL(endpoint, document.baseURI);
157
+ if (settings.queryParam !== null) url.searchParams.set(settings.queryParam, query);
158
+ return url;
159
+ }
160
+ function buildRequestInit(query, signal) {
161
+ const init = typeof settings.requestInit === "function" ? settings.requestInit(query, input) : settings.requestInit;
162
+ return { ...init, signal };
163
+ }
164
+ function handleInput(event) {
165
+ if (event.isComposing) return;
166
+ scheduleSearch();
167
+ }
168
+ function scheduleSearch() {
169
+ window.clearTimeout(debounceTimer);
170
+ const query = input.value.trim();
171
+ if (selected !== null) {
172
+ selected = null;
173
+ dispatch("autocomplete:clear", { query });
174
+ }
175
+ if (query.length < settings.minLength) {
176
+ cancel();
177
+ close();
178
+ return;
179
+ }
180
+ debounceTimer = window.setTimeout(() => void search(query), settings.debounce);
181
+ }
182
+ async function search(query = input.value.trim()) {
183
+ if (destroyed || query.length < settings.minLength) {
184
+ close();
185
+ return;
186
+ }
187
+ cancel();
188
+ const sequence = ++requestSequence;
189
+ const controller = new AbortController();
190
+ abortController = controller;
191
+ renderStatus(messages.loading, "status", "loading");
192
+ dispatch("autocomplete:loadstart", { query });
193
+ try {
194
+ const response = await fetch(buildUrl(query), buildRequestInit(query, controller.signal));
195
+ if (!response.ok) throw new Error(`Autocomplete request failed with HTTP ${response.status}.`);
196
+ const payload = await settings.parseResponse(response, query);
197
+ const result = await settings.mapResponse(payload, response, query);
198
+ if (!Array.isArray(result))
199
+ throw new TypeError("Autocomplete mapResponse must return an array.");
200
+ if (sequence !== requestSequence) return;
201
+ suggestions = result.slice(0, settings.maxResults).map(normalizeSuggestion);
202
+ activeIndex = -1;
203
+ if (suggestions.length === 0) renderStatus(messages.empty, "status", "empty");
204
+ else renderSuggestions();
205
+ dispatch("autocomplete:load", {
206
+ query,
207
+ suggestions: suggestions.map((suggestion) => suggestion.data)
208
+ });
209
+ } catch (error) {
210
+ if (isAbortError(error) || sequence !== requestSequence) return;
211
+ suggestions = [];
212
+ activeIndex = -1;
213
+ renderStatus(messages.error, "alert", "error");
214
+ dispatch("autocomplete:error", { query, error });
215
+ settings.onError?.(error, input);
216
+ } finally {
217
+ if (sequence === requestSequence) abortController = null;
218
+ }
219
+ }
220
+ function normalizeSuggestion(suggestion, index) {
221
+ return {
222
+ id: `${instanceId}-option-${sanitizeId(settings.getId(suggestion, index))}`,
223
+ label: String(settings.getLabel(suggestion)),
224
+ value: String(settings.getValue(suggestion)),
225
+ data: suggestion
226
+ };
227
+ }
228
+ function renderSuggestions() {
229
+ const fragment = document.createDocumentFragment();
230
+ list.replaceChildren();
231
+ suggestions.forEach((suggestion, index) => {
232
+ const option = document.createElement("button");
233
+ option.id = suggestion.id;
234
+ option.type = "button";
235
+ option.role = "option";
236
+ option.textContent = suggestion.label;
237
+ option.dataset.autocompleteOption = "";
238
+ option.dataset.index = String(index);
239
+ option.setAttribute("aria-selected", "false");
240
+ settings.renderOption?.(option, suggestion.data);
241
+ option.addEventListener("pointerdown", handleOptionPointerDown);
242
+ option.addEventListener("pointerenter", handleOptionPointerEnter);
243
+ fragment.append(option);
244
+ });
245
+ list.append(fragment);
246
+ open();
247
+ }
248
+ function renderStatus(message, role, state) {
249
+ const element = document.createElement("div");
250
+ element.role = role;
251
+ element.textContent = message;
252
+ element.dataset.autocompleteStatus = state;
253
+ list.replaceChildren(element);
254
+ open(true);
255
+ }
256
+ function open(allowStatus = false) {
257
+ if (destroyed || !allowStatus && suggestions.length === 0) return;
258
+ list.hidden = false;
259
+ input.setAttribute("aria-expanded", "true");
260
+ }
261
+ function close() {
262
+ activeIndex = -1;
263
+ list.hidden = true;
264
+ input.setAttribute("aria-expanded", "false");
265
+ input.removeAttribute("aria-activedescendant");
266
+ updateActiveOption();
267
+ }
268
+ function dismiss() {
269
+ cancel();
270
+ close();
271
+ }
272
+ function clear() {
273
+ const hadSelection = selected !== null;
274
+ cancel();
275
+ suggestions = [];
276
+ activeIndex = -1;
277
+ selected = null;
278
+ input.value = "";
279
+ close();
280
+ if (hadSelection) dispatch("autocomplete:clear", { query: "" });
281
+ }
282
+ function moveActiveOption(direction) {
283
+ if (list.hidden || suggestions.length === 0) return;
284
+ activeIndex = activeIndex === -1 ? direction > 0 ? 0 : suggestions.length - 1 : (activeIndex + direction + suggestions.length) % suggestions.length;
285
+ updateActiveOption();
286
+ }
287
+ function updateActiveOption() {
288
+ const options2 = list.querySelectorAll("[data-autocomplete-option]");
289
+ options2.forEach((option, index) => {
290
+ const active2 = index === activeIndex;
291
+ option.dataset.active = String(active2);
292
+ option.setAttribute("aria-selected", String(active2));
293
+ });
294
+ const active = options2.item(activeIndex);
295
+ if (!active) {
296
+ input.removeAttribute("aria-activedescendant");
297
+ return;
298
+ }
299
+ input.setAttribute("aria-activedescendant", active.id);
300
+ active.scrollIntoView({ block: "nearest" });
301
+ }
302
+ function select(index) {
303
+ const suggestion = suggestions[index];
304
+ if (!suggestion) return;
305
+ selected = suggestion.data;
306
+ input.value = suggestion.value;
307
+ dismiss();
308
+ dispatch("autocomplete:select", {
309
+ suggestion: suggestion.data,
310
+ label: suggestion.label,
311
+ value: suggestion.value
312
+ });
313
+ settings.onSelect?.(suggestion.data, input);
314
+ }
315
+ function dispatch(name, detail) {
316
+ input.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
317
+ }
318
+ function cancel() {
319
+ window.clearTimeout(debounceTimer);
320
+ debounceTimer = void 0;
321
+ requestSequence += 1;
322
+ abortController?.abort();
323
+ abortController = null;
324
+ }
325
+ function handleFocus() {
326
+ if (suppressFocusSearch) return;
327
+ if (settings.searchOnFocus && input.value.trim().length >= settings.minLength) {
328
+ scheduleSearch();
329
+ }
330
+ }
331
+ function handleKeydown(event) {
332
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
333
+ if (suggestions.length === 0 || list.hidden) return;
334
+ event.preventDefault();
335
+ moveActiveOption(event.key === "ArrowDown" ? 1 : -1);
336
+ return;
337
+ }
338
+ if (event.key === "Enter" && !list.hidden && activeIndex >= 0) {
339
+ event.preventDefault();
340
+ select(activeIndex);
341
+ return;
342
+ }
343
+ if ((event.key === "Home" || event.key === "End") && !list.hidden && suggestions.length > 0) {
344
+ event.preventDefault();
345
+ activeIndex = event.key === "Home" ? 0 : suggestions.length - 1;
346
+ updateActiveOption();
347
+ return;
348
+ }
349
+ if (event.key === "Escape") {
350
+ event.preventDefault();
351
+ dismiss();
352
+ return;
353
+ }
354
+ if (event.key === "Tab") dismiss();
355
+ }
356
+ function handleOptionPointerDown(event) {
357
+ event.preventDefault();
358
+ const element = event.currentTarget;
359
+ select(Number(element.dataset.index));
360
+ if (document.activeElement !== input) {
361
+ suppressFocusSearch = true;
362
+ input.focus({ preventScroll: true });
363
+ suppressFocusSearch = false;
364
+ }
365
+ }
366
+ function handleOptionPointerEnter(event) {
367
+ const element = event.currentTarget;
368
+ activeIndex = Number(element.dataset.index);
369
+ updateActiveOption();
370
+ }
371
+ function handleDocumentPointerDown(event) {
372
+ if (event.target !== input && !list.contains(event.target)) dismiss();
373
+ }
374
+ function destroy3() {
375
+ if (destroyed) return;
376
+ destroyed = true;
377
+ cancel();
378
+ input.removeEventListener("input", handleInput);
379
+ input.removeEventListener("compositionend", scheduleSearch);
380
+ input.removeEventListener("focus", handleFocus);
381
+ input.removeEventListener("keydown", handleKeydown);
382
+ document.removeEventListener("pointerdown", handleDocumentPointerDown);
383
+ list.remove();
384
+ originalAttributes.forEach((value, name) => restoreAttribute(input, name, value));
385
+ }
386
+ input.addEventListener("input", handleInput);
387
+ input.addEventListener("compositionend", scheduleSearch);
388
+ input.addEventListener("focus", handleFocus);
389
+ input.addEventListener("keydown", handleKeydown);
390
+ document.addEventListener("pointerdown", handleDocumentPointerDown);
391
+ return {
392
+ element: input,
393
+ listElement: list,
394
+ options,
395
+ search,
396
+ close: dismiss,
397
+ clear,
398
+ cancel,
399
+ getSelected: () => selected,
400
+ destroy: destroy3
401
+ };
402
+ }
403
+ function isAbortError(error) {
404
+ return error instanceof DOMException && error.name === "AbortError";
405
+ }
406
+
407
+ // src/api/create-autocomplete.ts
408
+ function createAutocomplete(target, options) {
409
+ return createAutocompleteController(target, options);
410
+ }
411
+
412
+ // src/browser/registry.ts
413
+ var available = /* @__PURE__ */ Object.create(null);
414
+ function create(target, options) {
415
+ const input = resolveInput(target);
416
+ if (!input.id) throw new TypeError("Autocomplete.create requires an input with an id.");
417
+ available[input.id]?.destroy();
418
+ const controller = createAutocomplete(input, options);
419
+ available[input.id] = controller;
420
+ return controller;
421
+ }
422
+ function destroy(id) {
423
+ available[id]?.destroy();
424
+ delete available[id];
425
+ }
426
+ var browser = {
427
+ autocomplete: createAutocomplete,
428
+ create,
429
+ destroy,
430
+ available
431
+ };
432
+
433
+ // src/browser/global.ts
434
+ var { autocomplete, create: create2, destroy: destroy2, available: available2 } = browser;
435
+ return __toCommonJS(global_exports);
436
+ })();
437
+ //# sourceMappingURL=global.global.js.map