UNPKG

nostr-web-components

Version:

collection of web components that provide quick access to basic nostr things

190 lines (188 loc) 6.13 kB
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 };