Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
4.1 kB · 100 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101/** * Handle autocomplete, for every field on the site that asks for one. * * `actor-typeahead` is a custom element that wraps a plain `<input>` and hangs * a list of matching accounts under it, backed by the public, unauthenticated * `app.bsky.actor.searchActorsTypeahead`. Wiring it up this way keeps the input * itself the real thing: it is created, styled, validated and submitted exactly * as before, and the element around it is decoration. If the chunk never * arrives, or the query endpoint is unreachable, the wrapper stays an inert * unknown tag and the field behaves like any other text input. * * That last part is not free, and the escape hatch below is why this file * exists rather than a bare `import`. */
/** Inputs we have wrapped, so the shared key handler ignores every other one. */const enhanced = new WeakSet<HTMLInputElement>();
/** Field value as it stood before the component got a look at an Enter press. */const beforeEnter = new WeakMap<HTMLInputElement, string>();
let loading: Promise<void> | undefined;let listening = false;
/** * Pull in the component. Failure is deliberately silent: a missing chunk costs * the visitor suggestions and nothing else, so it has no business putting an * error in front of them. Clearing the handle lets a later focus try again, * which is worth having on a flaky connection. */function load(): Promise<void> { loading ??= import("actor-typeahead").then( () => undefined, () => { loading = undefined; }, ); return loading;}
/** * The component calls preventDefault() on every Enter, whether or not a * suggestion is highlighted, so implicit form submission stops working the * moment it loads. These two handlers bracket its own listener to put that * back. They live on the document because the component attaches to the * wrapper whenever its chunk happens to land, so registration order cannot be * relied on any closer to the field than this. */function rememberValue(event: KeyboardEvent): void { const input = event.target; if (!(input instanceof HTMLInputElement) || !enhanced.has(input)) return; if (event.key === "Enter") beforeEnter.set(input, input.value);}
function submitIfSwallowed(event: KeyboardEvent): void { const input = event.target; if (!(input instanceof HTMLInputElement) || !enhanced.has(input)) return; if (event.key !== "Enter" || !event.defaultPrevented) return; // Enter mid-composition commits the text. It has never meant submit. if (event.isComposing) return; // Choosing a suggestion rewrites the field. An unchanged value means the key // was swallowed for nothing, so stand in for the submission it ate. if (beforeEnter.get(input) === input.value) input.form?.requestSubmit();}
/** * Wrap `input` so it offers handle suggestions, and return the element to put * where the input would have gone. The input keeps its own attributes, classes * and listeners; nothing about it changes. */export function withHandleTypeahead(input: HTMLInputElement): HTMLElement { if (!listening) { listening = true; document.addEventListener("keydown", rememberValue, { capture: true }); document.addEventListener("keydown", submitIfSwallowed); } enhanced.add(input);
const wrapper = document.createElement("actor-typeahead"); wrapper.className = "typeahead"; wrapper.append(input);
// Loaded on first contact rather than at startup. The sign-in field is on the // first screen a signed-out visitor sees, and suggestions are worth nothing // until they start typing, so this stays off the critical path. const arm = (): void => { void load().then(() => { // The component only reacts to input events, so a keystroke that landed // while the chunk was still in flight would otherwise show nothing until // the next one. if (input.value && document.activeElement === input) { input.dispatchEvent(new InputEvent("input", { bubbles: true })); } }); }; input.addEventListener("pointerdown", arm, { once: true }); input.addEventListener("focus", arm, { once: true });
return wrapper;}