···11+# actor-typeahead
22+33+A small web component that progressively enhances an `<input>` element into an autocomplete for ATProto handles!
44+55+It works with any web stack and any JavaScript framework.
66+77+## Installation
88+99+The easiest way to install is to just copy `actor-typeahead.js` into your project and reference it in a script tag:
1010+1111+```html
1212+<script type="module" src="actor-typeahead.js"></script>
1313+```
1414+1515+It will automatically register the `<actor-typeahead>` tag as a custom element.
1616+1717+If you'd like to use a different tag, you can import the file with a query string:
1818+1919+```html
2020+<script type="module" src="actor-typeahead.js?tag=some-other-tag"></script>
2121+```
2222+2323+If you'd prefer to manually register the custom element, you can use the query string `?tag=none` and call the static method `define`:
2424+2525+<script type="module">
2626+ import ActorTypeahead from "actor-typeahead.js?tag=none";
2727+ ActorTypeahead.define("some-other-tag");
2828+</script>
2929+3030+3131+## Usage
3232+3333+Simply wrap your `<input>` with `<actor-typeahead>`:
3434+3535+```html
3636+<actor-typeahead>
3737+ <input>
3838+</actor-typeahead>
3939+```
+14-12
actor-typeahead.js
···107107 if (name && name !== tag) return console.warn(`${this.name} already defined as <${name}>!`);
108108109109 const ce = customElements.get(tag);
110110- if (Boolean(ce) && ce !== this) return console.warn(`<${tag}> already defined as ${ce.name}!`);
110110+ if (ce && ce !== this) return console.warn(`<${tag}> already defined as ${ce.name}!`);
111111112112 customElements.define(tag, this);
113113 }
114114115115 static {
116116- const tag = new URL(import.meta.url).searchParams.get("define") || this.tag;
117117- if (tag !== "false") this.define(tag);
116116+ const tag = new URL(import.meta.url).searchParams.get("tag") || this.tag;
117117+ if (tag !== "none") this.define(tag);
118118 }
119119120120 #shadow = this.attachShadow({ mode: "closed" });
···138138 }
139139140140 get #rows() {
141141- const rows = Number.parseInt(this.getAttribute("rows"));
141141+ const rows = Number.parseInt(this.getAttribute("rows") ?? "");
142142143143 if (Number.isNaN(rows)) return 5;
144144 return rows;
···164164 break;
165165166166 case "pointerdown":
167167- this.#onpointerdown(evt);
167167+ this.#onpointerdown(/** @type {PointerEvent} */ (evt));
168168 break;
169169170170 case "pointerup":
171171- this.#onpointerup(evt);
171171+ this.#onpointerup(/** @type {PointerEvent} */ (evt));
172172 break;
173173 }
174174 }
···183183 break;
184184185185 case "PageDown":
186186- event.preventDefault();
186186+ evt.preventDefault();
187187 this.#index = this.#rows - 1;
188188 this.#render();
189189 break;
···195195 break;
196196197197 case "PageUp":
198198- event.preventDefault();
198198+ evt.preventDefault();
199199 this.#index = 0;
200200 this.#render();
201201 break;
···216216217217 /** @param {PointerEvent} evt */
218218 #onclick(evt) {
219219- const button = evt.target.closest(".user");
219219+ const button = evt.target?.closest(".user");
220220 const input = this.querySelector("input");
221221 if (!input || !button) return;
222222···227227228228 /** @param {InputEvent} evt */
229229 async #oninput(evt) {
230230- const query = evt.target.value;
230230+ const query = evt.target?.value;
231231 if (!query) {
232232 this.#actors = [];
233233 this.#render();
···237237 const host = this.getAttribute("host") ?? "https://public.api.bsky.app";
238238 const url = new URL("xrpc/app.bsky.actor.searchActorsTypeahead", host);
239239 url.searchParams.set("q", query);
240240- url.searchParams.set("limit", this.getAttribute("rows") ?? 5);
240240+ url.searchParams.set("limit", `${this.#rows}`);
241241242242 const res = await fetch(url);
243243 const json = await res.json();
···273273 fragment.append(li);
274274 }
275275276276- this.#shadow.querySelector(".menu").replaceChildren(...fragment.children);
276276+ this.#shadow.querySelector(".menu")?.replaceChildren(...fragment.children);
277277 }
278278279279 /** @param {PointerEvent} evt */
280280 #onpointerdown(evt) {
281281 const menu = this.#shadow.querySelector(".menu");
282282+ if (!menu) return;
282283283284 if (!menu.contains(evt.target)) return;
284285 menu.setPointerCapture(evt.pointerId);
···288289 /** @param {PointerEvent} evt */
289290 #onpointerup(evt) {
290291 const menu = this.#shadow.querySelector(".menu");
292292+ if (!menu) return;
291293292294 if (!menu.hasPointerCapture(evt.pointerId)) return;
293295 menu.releasePointerCapture(evt.pointerId);