nostr-web-components
Version:
collection of web components that provide quick access to basic nostr things
190 lines (188 loc) • 6.13 kB
JavaScript
import { normalizeURL } from "@nostr/tools/utils";
import { decode } from "@nostr/tools/nip19";
import { nostrUserFromEvent } from "@nostr/gadgets/metadata";
import { debounce, handleImageError, splitComma, transparentPixel } from "./utils.js";
import { pool } from "./nostr.js";
class NostrUserSearch extends HTMLElement {
static observedAttributes = ["relays", "limit", "placeholder", "value"];
root;
limit = 10;
value;
relays = ["wss://nostr.wine", "wss://search.nos.today", "wss://relay.nostr.band"].map(normalizeURL);
constructor() {
super();
this.attachShadow({ mode: "open" });
const { shadowRoot } = this;
this.root = document.createElement("div");
shadowRoot.appendChild(this.root);
}
connectedCallback() {
this.style.display = "inline-block";
this.style.width = "fit-content";
this.style.height = "fit-content";
setTimeout(() => {
let template = this.getAttribute("template") ? document.getElementById(this.getAttribute("template")) : null;
if (template) {
this.root.appendChild(template.content.cloneNode(true));
} else {
this.root.innerHTML = `
<style>
.wrapper {
position: relative;
}
ul {
margin: 0;
margin-top: -1px;
padding: 0;
background-color: white;
position: absolute;
left: 0;
border-bottom: 0;
display: none;
}
ul > li {
display: flex;
align-items: center;
cursor: pointer;
}
ul > li:hover {
background-color: rgba(0, 0, 0, 0.05);
}
ul > li > img {
height: 2rem;
width: 2rem;
}
ul > li > span {
margin: 0 0.5rem;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<div class="wrapper" part="wrapper">
<input type="search" part="input" />
<ul part="results">
<template>
<li part="item">
<img part="picture" />
<span part="name"></span>
<span part="nip05"></span>
</li>
</template>
</ul>
</div>
`;
}
for (let input of this.queryPart("input")) {
input.oninput = (ev) => this.search(ev.target.value.trim());
if (this.hasAttribute("placeholder")) {
input.setAttribute("placeholder", this.getAttribute("placeholder"));
}
if (this.hasAttribute("value")) {
input.value = this.getAttribute("value");
this.search(input.value.trim());
}
}
}, 1);
}
attributeChangedCallback(name, _old, value) {
if (name === "relays") {
this.relays = splitComma(value).map(normalizeURL);
} else if (name === "limit") {
this.limit = parseInt(value) || 10;
} else if (name === "placeholder") {
for (let input of this.queryPart("input")) {
input.setAttribute("placeholder", value);
}
} else if (name === "value") {
for (let input of this.queryPart("input")) {
input.value = value;
}
this.search(value.trim());
}
}
search = debounce(async (q) => {
for (let results of this.queryPart("results")) {
if (q.length < 2) {
results.style.display = "none";
return;
}
for (let i = 0; i < results.children.length; i++) {
if (results.children[i].tagName === "TEMPLATE")
continue;
results.removeChild(results.children[i]);
}
results.style.display = "block";
}
pool.subscribeManyEose(
this.relays,
{ search: q, limit: this.limit, kinds: [0] },
{
onevent: (evt) => {
let nu = nostrUserFromEvent(evt);
for (let results of this.queryPart("results")) {
const template = results.querySelector("template");
if (!template)
continue;
const item = template.content.cloneNode(true);
const itemElement = item.querySelector('[part="item"]');
if (itemElement) {
itemElement.title = nu.npub;
itemElement.onclick = this.handleItemClicked.bind(this);
itemElement.dataset.metadata = evt.content;
const pic = item.querySelector('[part="picture"]');
if (pic) {
pic.onerror = handleImageError;
pic.src = nu.image || transparentPixel;
}
const name = item.querySelector('[part="name"]');
if (name) {
name.textContent = nu.shortName;
}
const nip05 = item.querySelector('[part="nip05"]');
if (nip05 && nu.metadata.nip05) {
nip05.textContent = nu.metadata.nip05;
}
results.appendChild(item);
}
}
}
}
);
}, 450);
handleItemClicked(ev) {
let item = ev.currentTarget;
let npub = item.title;
let pubkey = decode(npub).data;
for (let results of this.queryPart("results")) {
for (let i = 0; i < results.children.length; i++) {
if (results.children[i].tagName === "TEMPLATE")
continue;
results.removeChild(results.children[i]);
}
results.style.display = "none";
}
this.value = pubkey;
this.dispatchEvent(
new CustomEvent("selected", {
bubbles: true,
detail: { pubkey, npub, metadata: JSON.parse(item.dataset.metadata) }
})
);
}
*queryPart(name) {
let slotted = this.querySelectorAll(`[part="${name}"]`);
for (let i = 0; i < slotted.length; i++) {
yield slotted[i];
}
let templated = this.root.querySelectorAll(`[part="${name}"]`);
for (let i = 0; i < templated.length; i++) {
yield templated[i];
}
}
}
window.customElements.define("nostr-user-search", NostrUserSearch);
export {
NostrUserSearch as default
};