Autocomplete
A free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.
Demo
Pick one, or type a city that is not on the list.
Type a tag; any text is kept.
Tell us a city.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/autocomplete.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/autocomplete.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/autocomplete.qwe
let autocomplete_demo_cities = ["Amsterdam", "Athens", "Auckland", "Barcelona", "Berlin", "Bordeaux", "Dublin", "Kyoto", "Lisbon", "London", "Los Angeles", "Lyon", "Madrid", "New York", "Porto", "Prague", "Rome", "Vienna"]component AutocompleteDemoLive() { state picked = null state text = "" <div.demo-stack> <Autocomplete name="ac-demo-tag" label="Tag" mode="any" limit={4} options={["design", "databases", "devops", "documentation", "testing", "type-systems", "web"]} on_change={(t) => { text = t }} on_select={(t) => { picked = t }}/> <p.demo-note>{text == "" ? "Type a tag; any text is kept." : `Text: ${text}${picked != null ? ` · last picked: ${picked}` : ""}`}</p> </div>}view AutocompleteDemo() { <div.demo-stack.demo-narrow> <Autocomplete name="ac-demo-city" label="City" options={autocomplete_demo_cities} placeholder="Where to?" hint="Pick one, or type a city that is not on the list."/> <AutocompleteDemoLive/> <Autocomplete name="ac-demo-sm" label="Small" size="sm" min_chars={2} options={autocomplete_demo_cities} error="Tell us a city."/> </div>}Source
ui/autocomplete.qwe
// Autocomplete: a text box that offers matches from a list as one types (a city,// a tag, a product), where any text may stay: picking a suggestion is a shortcut,// not a must. The box is a real <input> named `name`, so a plain <form> sends// what is in it. The arrow keys move through the suggestions, Enter takes one,// Escape closes them.//// <Autocomplete name="city" label="City" options={["Lisbon", "London", "Lyon"]}/>// <Autocomplete name="tag" label="Tag" options={tags} min_chars={2} limit={5} on_change={(t) => { tag = t }}/>//// options: the words offered, strings. min_chars: letters typed before anything// is offered (1). limit: at most this many suggestions (8). mode: "start" (the// suggestion begins with the text; the default) or "any" (it has the text anywhere).// value, placeholder, autocomplete (the browser's own, "off"), name, label, hint,// error, required, disabled, field (the box's id, "autocomplete-" + name by// default) and size ("sm" or "md") as for every field.// on_change(text): inside one of your components, given the box's text as it is// typed, and the suggestion when one is taken. on_select(text): given only the// suggestion taken.import ui/theme.qwe use ui_clslet autocomplete_css = css`.ui-autocomplete { position: relative; display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-autocomplete-label { font-size: 13px; font-weight: 500; }.ui-autocomplete-req { margin-left: 2px; color: var(--ui-danger); }.ui-autocomplete-box { position: relative; }.ui-autocomplete-box input { box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); transition: border-color .15s, box-shadow .15s;}.ui-autocomplete-box input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-autocomplete-box input:hover { border-color: var(--ui-strong); }.ui-autocomplete-box input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-autocomplete-box input:disabled { opacity: .55; cursor: not-allowed; }.ui-autocomplete-pop { position: absolute; z-index: 20; top: 100%; left: 0; right: 0; margin: 4px 0 0; padding: 4px; list-style: none; max-height: 240px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); box-shadow: var(--ui-shadow);}.ui-autocomplete-pop[hidden] { display: none; }.ui-autocomplete-opt { padding: 7px 10px; border-radius: 4px; cursor: pointer; }.ui-autocomplete-opt[aria-selected="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-autocomplete-opt mark { background: none; color: inherit; font-weight: 600; }.ui-autocomplete-sm .ui-autocomplete-box input { height: 30px; padding: 0 10px; font-size: 13px; }.ui-autocomplete-sm .ui-autocomplete-opt { padding: 5px 8px; font-size: 13px; }.ui-autocomplete-invalid .ui-autocomplete-box input { border-color: var(--ui-danger); }.ui-autocomplete-hint, .ui-autocomplete-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-autocomplete-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-autocomplete-box input { transition: none; } }`// The options matching `text`, at most `limit`, those that start with it first.fn autocomplete_matches(options: list, text: str, mode: str, limit: int) -> list { let needle = lower(trim(text)) if needle == "" { return [] } let starts = options | filter((o) => starts_with(lower(o), needle)) let within = mode == "any" ? options | filter((o) => !starts_with(lower(o), needle) && contains(lower(o), needle)) : [] take(starts + within, limit)}component Autocomplete(name: str? = null, label: str? = null, value: str = "", options: list = [], min_chars: int = 1, limit: int = 8, mode: str = "start", placeholder: str? = null, autocomplete: str = "off", hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_change = null, on_select = null, class: str? = null) { style global autocomplete_css state text = value state open = false state active = 0 let id = field ?? `autocomplete-${name ?? re.replace(lower(label ?? "field"), "[^a-z0-9]+", "-")}` let shown = len(trim(text)) >= min_chars ? autocomplete_matches(options, text, mode, limit) : [] let listed = open && len(shown) > 0 let act = active < len(shown) ? active : 0 let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let needle = lower(trim(text)) let pick = (o: str) => { text = o open = false on_change && on_change(o) on_select && on_select(o) } <div class={ui_cls(["ui-autocomplete", size == "sm" && "ui-autocomplete-sm", error != null && "ui-autocomplete-invalid", class])}> {label != null && <label.ui-autocomplete-label for={id}> {label}{required && <span.ui-autocomplete-req aria-hidden="true">*</span>} </label>} <div.ui-autocomplete-box> <input type="text" id={id} name={name} value={text} placeholder={placeholder} autocomplete={autocomplete} required={required} disabled={disabled} role="combobox" aria-autocomplete="list" aria-expanded={listed ? "true" : "false"} aria-controls={`${id}-list`} aria-activedescendant={listed ? `${id}-opt-${act}` : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @input={(e) => { text = e.value open = true active = 0 on_change && on_change(e.value) }} @focus={() => { open = true }} @blur={() => { open = false }} @keydown={(e) => { let n = len(shown) if (e.key == "ArrowDown" || e.key == "ArrowUp") && n > 0 { e.preventDefault() if !open { open = true } else { active = e.key == "ArrowDown" ? (act + 1) % n : (act + n - 1) % n } } if e.key == "Enter" && listed { e.preventDefault() pick(shown[act]) } if e.key == "Escape" && open { e.preventDefault() open = false } }}/> <ul.ui-autocomplete-pop id={`${id}-list`} role="listbox" aria-label={label ?? "Suggestions"} hidden={!listed}> {for [i, o] in enumerate(shown) => <li.ui-autocomplete-opt key={o} id={`${id}-opt-${i}`} role="option" aria-selected={i == act ? "true" : "false"} @mousedown={(e) => { e.preventDefault() }} @click={() => { pick(o) }}> {starts_with(lower(o), needle) ? [<mark>{slice(o, 0, len(needle))}</mark>, slice(o, len(needle))] : o} </li>} </ul> </div> {hint != null && <p.ui-autocomplete-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-autocomplete-error id={`${id}-error`} role="alert">{error}</p>} </div>}Related
CodeEditorA code textarea with line numbers, where Tab puts spaces in, Shift+Tab takes them out and Enter keeps the indent.5 views · 8 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views · 6 downloadsSearchInputA search box with a magnifier and a clear button; Escape clears, Enter submits its form.6 views · 5 downloadsNavSearchA search box for a navbar that grows when focused, with / to reach it and Escape to clear; a real form sends it.5 views · 5 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloads
Thread
Thread 0 posts
On Autocomplete: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.