a tiny atproto handle typeahead web component

Add deps and docs

+1
.gitignore
··· 1 1 .DS_Store 2 + /node_modules
+39
README.md
··· 1 + # actor-typeahead 2 + 3 + A small web component that progressively enhances an `<input>` element into an autocomplete for ATProto handles! 4 + 5 + It works with any web stack and any JavaScript framework. 6 + 7 + ## Installation 8 + 9 + The easiest way to install is to just copy `actor-typeahead.js` into your project and reference it in a script tag: 10 + 11 + ```html 12 + <script type="module" src="actor-typeahead.js"></script> 13 + ``` 14 + 15 + It will automatically register the `<actor-typeahead>` tag as a custom element. 16 + 17 + If you'd like to use a different tag, you can import the file with a query string: 18 + 19 + ```html 20 + <script type="module" src="actor-typeahead.js?tag=some-other-tag"></script> 21 + ``` 22 + 23 + If you'd prefer to manually register the custom element, you can use the query string `?tag=none` and call the static method `define`: 24 + 25 + <script type="module"> 26 + import ActorTypeahead from "actor-typeahead.js?tag=none"; 27 + ActorTypeahead.define("some-other-tag"); 28 + </script> 29 + 30 + 31 + ## Usage 32 + 33 + Simply wrap your `<input>` with `<actor-typeahead>`: 34 + 35 + ```html 36 + <actor-typeahead> 37 + <input> 38 + </actor-typeahead> 39 + ```
+14 -12
actor-typeahead.js
··· 107 107 if (name && name !== tag) return console.warn(`${this.name} already defined as <${name}>!`); 108 108 109 109 const ce = customElements.get(tag); 110 - if (Boolean(ce) && ce !== this) return console.warn(`<${tag}> already defined as ${ce.name}!`); 110 + if (ce && ce !== this) return console.warn(`<${tag}> already defined as ${ce.name}!`); 111 111 112 112 customElements.define(tag, this); 113 113 } 114 114 115 115 static { 116 - const tag = new URL(import.meta.url).searchParams.get("define") || this.tag; 117 - if (tag !== "false") this.define(tag); 116 + const tag = new URL(import.meta.url).searchParams.get("tag") || this.tag; 117 + if (tag !== "none") this.define(tag); 118 118 } 119 119 120 120 #shadow = this.attachShadow({ mode: "closed" }); ··· 138 138 } 139 139 140 140 get #rows() { 141 - const rows = Number.parseInt(this.getAttribute("rows")); 141 + const rows = Number.parseInt(this.getAttribute("rows") ?? ""); 142 142 143 143 if (Number.isNaN(rows)) return 5; 144 144 return rows; ··· 164 164 break; 165 165 166 166 case "pointerdown": 167 - this.#onpointerdown(evt); 167 + this.#onpointerdown(/** @type {PointerEvent} */ (evt)); 168 168 break; 169 169 170 170 case "pointerup": 171 - this.#onpointerup(evt); 171 + this.#onpointerup(/** @type {PointerEvent} */ (evt)); 172 172 break; 173 173 } 174 174 } ··· 183 183 break; 184 184 185 185 case "PageDown": 186 - event.preventDefault(); 186 + evt.preventDefault(); 187 187 this.#index = this.#rows - 1; 188 188 this.#render(); 189 189 break; ··· 195 195 break; 196 196 197 197 case "PageUp": 198 - event.preventDefault(); 198 + evt.preventDefault(); 199 199 this.#index = 0; 200 200 this.#render(); 201 201 break; ··· 216 216 217 217 /** @param {PointerEvent} evt */ 218 218 #onclick(evt) { 219 - const button = evt.target.closest(".user"); 219 + const button = evt.target?.closest(".user"); 220 220 const input = this.querySelector("input"); 221 221 if (!input || !button) return; 222 222 ··· 227 227 228 228 /** @param {InputEvent} evt */ 229 229 async #oninput(evt) { 230 - const query = evt.target.value; 230 + const query = evt.target?.value; 231 231 if (!query) { 232 232 this.#actors = []; 233 233 this.#render(); ··· 237 237 const host = this.getAttribute("host") ?? "https://public.api.bsky.app"; 238 238 const url = new URL("xrpc/app.bsky.actor.searchActorsTypeahead", host); 239 239 url.searchParams.set("q", query); 240 - url.searchParams.set("limit", this.getAttribute("rows") ?? 5); 240 + url.searchParams.set("limit", `${this.#rows}`); 241 241 242 242 const res = await fetch(url); 243 243 const json = await res.json(); ··· 273 273 fragment.append(li); 274 274 } 275 275 276 - this.#shadow.querySelector(".menu").replaceChildren(...fragment.children); 276 + this.#shadow.querySelector(".menu")?.replaceChildren(...fragment.children); 277 277 } 278 278 279 279 /** @param {PointerEvent} evt */ 280 280 #onpointerdown(evt) { 281 281 const menu = this.#shadow.querySelector(".menu"); 282 + if (!menu) return; 282 283 283 284 if (!menu.contains(evt.target)) return; 284 285 menu.setPointerCapture(evt.pointerId); ··· 288 289 /** @param {PointerEvent} evt */ 289 290 #onpointerup(evt) { 290 291 const menu = this.#shadow.querySelector(".menu"); 292 + if (!menu) return; 291 293 292 294 if (!menu.hasPointerCapture(evt.pointerId)) return; 293 295 menu.releasePointerCapture(evt.pointerId);
+14
jsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "esnext", 4 + "module": "esnext", 5 + "checkJs": true, 6 + "strict": true, 7 + "verbatimModuleSyntax": true, 8 + "isolatedModules": true, 9 + "noUncheckedSideEffectImports": true, 10 + "moduleDetection": "force", 11 + "skipLibCheck": true 12 + }, 13 + "include": ["./*.js"] 14 + }
+30
package-lock.json
··· 1 + { 2 + "name": "actor-typeahead", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "actor-typeahead", 9 + "version": "1.0.0", 10 + "license": "MPL-2.0", 11 + "devDependencies": { 12 + "typescript": "^5.9.3" 13 + } 14 + }, 15 + "node_modules/typescript": { 16 + "version": "5.9.3", 17 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 18 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 19 + "dev": true, 20 + "license": "Apache-2.0", 21 + "bin": { 22 + "tsc": "bin/tsc", 23 + "tsserver": "bin/tsserver" 24 + }, 25 + "engines": { 26 + "node": ">=14.17" 27 + } 28 + } 29 + } 30 + }
+18
package.json
··· 1 + { 2 + "name": "actor-typeahead", 3 + "version": "1.0.0", 4 + "description": "", 5 + "main": "actor-typeahead.js", 6 + "scripts": { 7 + "typecheck": "tsc -p jsconfig.json" 8 + }, 9 + "repository": { 10 + "type": "git", 11 + "url": "git@tangled.sh:jakelazaroff.com/actor-typeahead" 12 + }, 13 + "author": "", 14 + "license": "MPL-2.0", 15 + "devDependencies": { 16 + "typescript": "^5.9.3" 17 + } 18 + }