Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
3.6 kB · 120 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import { esc } from '../utils.js';import { createElement, Bird, Bone, Bug, Cat, Dog, Fish, Panda, PawPrint, Rabbit, Rat, Shell, Snail, Shrimp, Squirrel, Turtle, Worm, Cherry, Star, Moon, Sprout,} from 'lucide';
const ENDPOINT = 'https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead';
const PLACEHOLDER_ICONS = [ Bird, Bone, Bug, Cat, Dog, Fish, Panda, PawPrint, Rabbit, Rat, Shell, Snail, Shrimp, Squirrel, Turtle, Worm, Cherry, Star, Moon, Sprout,];
function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h);}
function placeholderIconHtml(key) { const icon = PLACEHOLDER_ICONS[hashStr(key) % PLACEHOLDER_ICONS.length]; return createElement(icon, { width: 14, height: 14, 'stroke-width': 2 }).outerHTML;}
export function attachTypeahead(input, list, { onSubmit }) { let actors = []; let cursor = -1; let seq = 0;
const close = () => { list.style.display = 'none'; list.innerHTML = ''; actors = []; cursor = -1; };
const render = () => { if (!actors.length) return close(); list.style.display = 'block'; list.innerHTML = actors.map((a, i) => ` <li data-i="${i}" class="flex items-center gap-2 px-2 py-1 cursor-pointer ${i === cursor ? 'bg-gray-200 dark:bg-stone-800' : ''}"> <span class="shrink-0 w-6 h-6 border border-gray-400 dark:border-gray-600 flex items-center justify-center overflow-hidden"> ${a.avatar ? `<img src="${esc(a.avatar)}" class="w-6 h-6 object-cover" />` : placeholderIconHtml(a.did || a.handle)} </span> <span class="truncate"> ${a.displayName ? `<span class="mr-1">${esc(a.displayName)}</span>` : ''} <span class="text-gray-600 dark:text-gray-400">@${esc(a.handle)}</span> </span> </li> `).join(''); };
const select = (i) => { const a = actors[i]; if (!a) return; input.value = a.handle; close(); };
const fetchResults = async (q) => { const mySeq = ++seq; try { const res = await fetch(`${ENDPOINT}?q=${encodeURIComponent(q)}&limit=6`, { headers: { 'X-Client': 'followtunnel' }, }); if (mySeq !== seq) return; const data = await res.json(); actors = data.actors || []; cursor = -1; render(); } catch { if (mySeq !== seq) return; close(); } };
let timer; input.addEventListener('input', () => { const q = input.value.trim().replace(/^@/, ''); clearTimeout(timer); if (q.length < 2) return close(); timer = setTimeout(() => fetchResults(q), 200); });
input.addEventListener('keydown', e => { if (list.style.display === 'none' || !actors.length) { if (e.key === 'Enter') onSubmit(input.value.trim()); return; } if (e.key === 'ArrowDown') { e.preventDefault(); cursor = (cursor + 1) % actors.length; render(); } else if (e.key === 'ArrowUp') { e.preventDefault(); cursor = (cursor - 1 + actors.length) % actors.length; render(); } else if (e.key === 'Enter') { e.preventDefault(); if (cursor >= 0) { select(cursor); onSubmit(input.value.trim()); } else { onSubmit(input.value.trim()); } } else if (e.key === 'Escape') { close(); } });
list.addEventListener('mousedown', e => { const li = e.target.closest('li[data-i]'); if (!li) return; e.preventDefault(); select(Number(li.dataset.i)); });
input.addEventListener('blur', () => setTimeout(close, 150));}