+39
README.md
+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
+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
+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
+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
+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
+
}