+1
.gitignore
+1
.gitignore
···
1
+
.DS_Store
+14
index.html
+14
index.html
+168
index.js
+168
index.js
···
1
+
const template = document.createElement("template");
2
+
template.innerHTML = `
3
+
<slot></slot>
4
+
5
+
<ul class="menu" part="menu"></ul>
6
+
7
+
<style>
8
+
:host {
9
+
--color-background-inherited: var(--color-background, #ffffff);
10
+
--color-border-inherited: var(--color-border, #00000022);
11
+
--color-shadow-inherited: var(--color-shadow, #000000);
12
+
--color-hover-inherited: var(--color-hover, #eeeeee);
13
+
display: block;
14
+
position: relative;
15
+
font-family: system-ui;
16
+
}
17
+
18
+
*, *::before, *::after {
19
+
margin: 0;
20
+
padding: 0;
21
+
box-sizing: border-box;
22
+
}
23
+
24
+
.menu {
25
+
display: flex;
26
+
flex-direction: column;
27
+
position: absolute;
28
+
left: 0;
29
+
margin-top: 4px;
30
+
width: 100%;
31
+
list-style: none;
32
+
overflow: hidden;
33
+
background-color: var(--color-background-inherited);
34
+
background-clip: padding-box;
35
+
border: 1px solid var(--color-border-inherited);
36
+
border-radius: 8px;
37
+
box-shadow: 0 6px 6px -4px rgb(from var(--color-shadow-inherited) r g b / 20%);
38
+
padding: 4px;
39
+
}
40
+
41
+
.menu:empty {
42
+
display: none;
43
+
}
44
+
45
+
.user {
46
+
all: unset;
47
+
box-sizing: border-box;
48
+
display: flex;
49
+
align-items: center;
50
+
gap: 8px;
51
+
padding: 6px 8px;
52
+
width: 100%;
53
+
height: calc(1.5rem + 6px * 2);
54
+
border-radius: 4px;
55
+
}
56
+
57
+
.user:hover {
58
+
background-color: var(--color-hover-inherited);
59
+
}
60
+
61
+
.avatar {
62
+
width: 1.5rem;
63
+
border-radius: 50%;
64
+
}
65
+
66
+
.handle {
67
+
white-space: nowrap;
68
+
overflow: hidden;
69
+
text-overflow: ellipsis;
70
+
}
71
+
</style>
72
+
`;
73
+
74
+
const user = document.createElement("template");
75
+
user.innerHTML = `
76
+
<li>
77
+
<button class="user" part="user">
78
+
<img class="avatar" part="avatar">
79
+
<span class="handle" part="handle"></span>
80
+
</button>
81
+
</li>
82
+
`;
83
+
84
+
export default class ActorTypeahead extends HTMLElement {
85
+
static tag = "actor-typeahead";
86
+
87
+
static define(tag = this.tag) {
88
+
this.tag = tag;
89
+
90
+
const name = customElements.getName(this);
91
+
if (name && name !== tag) return console.warn(`${this.name} already defined as <${name}>!`);
92
+
93
+
const ce = customElements.get(tag);
94
+
if (Boolean(ce) && ce !== this) return console.warn(`<${tag}> already defined as ${ce.name}!`);
95
+
96
+
customElements.define(tag, this);
97
+
}
98
+
99
+
static {
100
+
const tag = new URL(import.meta.url).searchParams.get("define") || this.tag;
101
+
if (tag !== "false") this.define(tag);
102
+
}
103
+
104
+
#shadow = this.attachShadow({ mode: "closed" });
105
+
106
+
constructor() {
107
+
super();
108
+
109
+
this.#shadow.append(template.cloneNode(true).content);
110
+
this.#render([]);
111
+
this.addEventListener("input", this);
112
+
// this.addEventListener("focusout", this);
113
+
this.#shadow.addEventListener("click", this);
114
+
}
115
+
116
+
get #rows() {
117
+
const rows = Number.parseInt(this.getAttribute("rows"));
118
+
119
+
if (Number.isNaN(rows)) return 5;
120
+
return rows;
121
+
}
122
+
123
+
/** @param {Event} evt */
124
+
handleEvent(evt) {
125
+
switch (evt.type) {
126
+
case "input":
127
+
this.#search(evt.target.value);
128
+
break;
129
+
// case "focusout":
130
+
// this.#render([]);
131
+
// break;
132
+
case "click":
133
+
const button = evt.target.closest(".user");
134
+
const input = this.querySelector("input");
135
+
if (!input || !button) return;
136
+
137
+
input.value = button.dataset.handle;
138
+
this.#render([]);
139
+
}
140
+
}
141
+
142
+
#render(actors) {
143
+
const fragment = document.createDocumentFragment();
144
+
for (const actor of actors) {
145
+
const li = user.cloneNode(true).content;
146
+
li.querySelector(".user").dataset.handle = actor.handle;
147
+
li.querySelector(".avatar").src = actor.avatar;
148
+
li.querySelector(".handle").textContent = actor.handle;
149
+
fragment.append(li);
150
+
}
151
+
152
+
this.#shadow.querySelector(".menu").replaceChildren(...fragment.children);
153
+
}
154
+
155
+
/** @param {string} query */
156
+
async #search(query) {
157
+
if (!query) return this.#render([]);
158
+
159
+
const host = this.getAttribute("host") ?? "https://public.api.bsky.app";
160
+
const url = new URL("xrpc/app.bsky.actor.searchActorsTypeahead", host);
161
+
url.searchParams.set("q", query);
162
+
url.searchParams.set("limit", this.getAttribute("rows") ?? 5);
163
+
164
+
const res = await fetch(url);
165
+
const { actors } = await res.json();
166
+
this.#render(actors);
167
+
}
168
+
}