rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1/*! Fiesta 1.2.0 | MPL-2.0 | https://fiesta.rastrillo.org */
2(function () {
3 "use strict";
4
5 const fold = (s) =>
6 String(s || "").normalize("NFD").replace(/\p{M}/gu, "").replace(/[\u2018\u2019]/g, "'").toLowerCase().trim();
7
8 const plain = (t) => t.replace(/^\+/, "");
9 const folded = (o) => {
10 if (o.folded) return o.folded;
11 const name = fold(o.name);
12 const terms = (o.terms || []).map(fold).filter(Boolean);
13 return {
14 name,
15 text: fold(o.text),
16 terms,
17 bareName: name.replace(/\s*\([^()]*\)$/, ""),
18 words: name.split(/[\s\-’'(),.]+/).filter(Boolean),
19 plainTerms: terms.map(plain),
20 };
21 };
22
23 const ranked = (o, q) => {
24 if (!q) return 0;
25 const bare = q.replace(/^\+/, "");
26 const f = folded(o);
27 const name = f.name;
28 const terms = f.terms;
29 const plains = f.plainTerms;
30 if (name === q || f.bareName === q || terms.includes(q) || (bare && plains.includes(bare))) return 0;
31 if (name.startsWith(q)) return 1;
32 if (f.words.some((w) => w.startsWith(q))) return 2;
33 if (bare && plains.some((t) => t.startsWith(bare))) return 2;
34 if (f.text.includes(q)) return 3;
35 return -1;
36 };
37
38 const rank = (o, query) => ranked(o, fold(query));
39
40 const order = (opts, query) => {
41 const q = fold(query);
42 return opts
43 .map((o, i) => ({ o, i, r: ranked(o, q) }))
44 .filter((x) => x.r >= 0)
45 .sort((a, b) => a.r - b.r || (a.r === 0 ? (b.o.first ? 1 : 0) - (a.o.first ? 1 : 0) : 0) || a.i - b.i)
46 .map((x) => x.o);
47 };
48
49 const settle = (hits, query) => {
50 if (hits.length === 1) return hits[0];
51 if (!hits.length) return null;
52 const q = fold(query);
53 const bare = q.replace(/^\+/, "");
54 const r0 = rank(hits[0], query);
55 if (r0 === 0 && /^\d+$/.test(bare) && hits[0].first) return hits[0];
56 if (bare.length < 3 || /^\d+$/.test(bare)) return null;
57 if (r0 === 0) {
58 const ties = hits.filter((o) => ranked(o, q) === 0);
59 if (ties.length === 1) return hits[0];
60 return hits[0].first && ties.filter((o) => o.first).length === 1 ? hits[0] : null;
61 }
62 return r0 < rank(hits[1], query) ? hits[0] : null;
63 };
64
65 const isPrompt = (value, marked, required, disabled) =>
66 marked || (value === "" && (required || !!disabled));
67
68 const offered = (value, marked, required, searching, disabled) =>
69 value !== "" || (marked ? !searching : !(required || !!disabled));
70
71
72 function fiestaPickLang(tag, registry) {
73 var want = String(tag || "").toLowerCase();
74 if (!want) return null;
75 var keys = Object.keys(registry || {}), i;
76 for (i = 0; i < keys.length; i++) {
77 if (keys[i].toLowerCase() === want) return keys[i];
78 }
79 var primary = want.split("-")[0];
80 for (i = 0; i < keys.length; i++) {
81 if (keys[i].toLowerCase().split("-")[0] === primary) return keys[i];
82 }
83 return null;
84 }
85
86 function fiestaWord(el, key, fallback) {
87 var v = el.getAttribute("data-rst-" + key);
88 if (v !== null && v !== "") return v;
89 var registry = (typeof self !== "undefined" && self.fiestaStrings) || {};
90 var lang = fiestaPickLang(document.documentElement.lang, registry);
91 var table = lang ? registry[lang] : null;
92 return table && table[key] !== undefined && table[key] !== "" ? table[key] : fallback;
93 }
94
95 if (typeof module !== "undefined" && module && module.exports) {
96 module.exports = { fold: fold, rank: rank, order: order, settle: settle, isPrompt: isPrompt, offered: offered, fiestaPickLang: fiestaPickLang, fiestaWord: fiestaWord };
97 }
98 if (typeof document === "undefined") return;
99
100 let told = null;
101 document.addEventListener("rst:select-viewport", (e) => {
102 const d = e.detail;
103 told = d && isFinite(d.top) && isFinite(d.bottom) ? { top: +d.top, bottom: +d.bottom } : null;
104 });
105
106 const holds = (cs) => {
107 const d = cs.display, b = d !== "contents", cv = cs.contentVisibility;
108 const o = b && !/^(inline|run-in)( list-item)?$|^(ruby$|table-col|ruby-)/.test(d), c = o && !/^table-(row|head|foot)/.test(d);
109 const w = (cs.willChange || "").toLowerCase().split(/,\s*/).filter((v) => CSS.supports(v, "initial")).map((v) => v.replace(/^-webkit-/, ""));
110 const X = (s, v) => s.split(" ").some((k) => w.includes(k) || (v && !/^(none|)$/.test(cs.getPropertyValue(k))));
111 const p = c && (/paint|strict|content/.test(cs.contain) || cv === "auto" || cv === "hidden");
112 const f = p || (c && (/layout/.test(cs.contain) || X("contain content-visibility"))) ||
113 (o && (cs.transformStyle === "preserve-3d" || /\d/.test(cs.offsetPosition) || X("transform translate rotate scale perspective offset-path", 1) || X("transform-style offset offset-position"))) ||
114 (b && X("filter backdrop-filter -webkit-backdrop-filter", 1));
115 return { f, a: f || (b && (cs.position !== "static" || X("position"))), p, o, b };
116 };
117 const onTop = (a) => [":modal", ":popover-open"].some((q) => { try { return a.matches(q); } catch (e) { return false; } });
118
119 const clippers = (list, pos) => {
120 const out = [];
121 for (let a = list; (a = a.assignedSlot || a.parentElement || (a.parentNode && a.parentNode.host)) && a !== document.body; ) {
122 const cs = getComputedStyle(a), h = holds(cs);
123 if (h.b && (pos === "fixed" ? h.f : pos !== "absolute" || h.a)) {
124 if (h.p || (h.o && /auto|scroll|hidden|clip/.test(cs.overflowY))) out.push(a);
125 pos = cs.position;
126 }
127 if (onTop(a)) break;
128 }
129 return out;
130 };
131
132 const ACTS = new Set(["ArrowDown", "ArrowUp", "Home", "End", "Enter", "Tab"]);
133
134 const el = (tag, attr) => {
135 const e = document.createElement(tag);
136 if (attr) e.setAttribute(attr, "");
137 return e;
138 };
139
140 const enhanced = new WeakSet();
141 function combo(native, resume) {
142 if (enhanced.has(native)) return;
143 enhanced.add(native);
144 native.dataset.rstEnhanced = "true";
145
146 const id = native.id || "rst-select-" + Math.random().toString(36).slice(2);
147 const listId = id + "-listbox";
148 const say = (key, fallback) => fiestaWord(native, "select-" + key, fallback);
149 const manyFmt = say("results", "{n} results");
150 const oneFmt = say("result-one", "1 result");
151
152 const groups = [];
153 const offLimits = (o) => o.disabled || !!(o.parentElement && o.parentElement.tagName === "OPTGROUP" && o.parentElement.disabled);
154 const locked = () => native.matches(":disabled");
155 const opts = Array.from(native.options).map((o, i) => {
156 const d = o.dataset;
157 const g = o.parentElement.tagName === "OPTGROUP" ? o.parentElement : null;
158 if (g && groups[groups.length - 1] !== g) groups.push(g);
159 return {
160 text: o.text,
161 name: d.rstName || o.text,
162 desc: d.rstDesc || "",
163 short: d.rstShort || "",
164 lead: d.rstLead || "",
165 classes: (d.rstClass || "").split(/\s+/).filter((c) => c && c !== "is-active"),
166 terms: (d.rstTerms || "").split(/\s+/).filter(Boolean),
167 marked: d.rstPrompt !== undefined,
168 prompt: isPrompt(o.value, d.rstPrompt !== undefined, native.required, offLimits(o)),
169 first: d.rstFirst !== undefined,
170 sepBefore: !!(o.previousElementSibling && o.previousElementSibling.tagName === "HR"),
171 disabled: offLimits(o),
172 blank: o.value === "",
173 index: i,
174 group: g,
175 el: o,
176 li: null,
177 };
178 });
179 for (const o of opts) o.folded = folded(o);
180 const compact = opts.some((o) => o.short);
181
182 const wrap = el("div", "rst-combo");
183 if (compact) wrap.setAttribute("rst-combo-compact", "");
184 const lead = el("span", "rst-combo-lead");
185 lead.setAttribute("aria-hidden", "true");
186 const said = el("span");
187 said.id = id + "-said";
188 said.hidden = true;
189
190 const input = el("input", "rst-input");
191 input.type = "text";
192 input.id = id + "-combo";
193 input.autocomplete = "off";
194 input.spellcheck = false;
195 input.disabled = native.disabled;
196 input.placeholder = compact && native.getAttribute("aria-label")
197 ? native.getAttribute("aria-label")
198 : say("filter", "Type to filter");
199 input.setAttribute("role", "combobox");
200 input.setAttribute("aria-expanded", "false");
201 input.setAttribute("aria-controls", listId);
202 input.setAttribute("aria-autocomplete", "list");
203 const described = native.getAttribute("aria-describedby");
204 if (described) input.setAttribute("aria-describedby", described);
205 if (compact) input.setAttribute("aria-describedby", described ? described + " " + said.id : said.id);
206
207 const list = el("ul", "rst-combo-list");
208 list.id = listId;
209 list.hidden = true;
210 list.setAttribute("role", "listbox");
211
212 const status = el("p");
213 status.className = "rst-sr-only";
214 status.setAttribute("role", "status");
215 status.setAttribute("aria-live", "polite");
216
217 const label = native.id && document.querySelector('label[for="' + CSS.escape(native.id) + '"]');
218 if (label) {
219 if (!label.id) label.id = id + "-label";
220 label.htmlFor = input.id;
221 list.setAttribute("aria-labelledby", label.id);
222 } else if (native.getAttribute("aria-label")) {
223 input.setAttribute("aria-label", native.getAttribute("aria-label"));
224 list.setAttribute("aria-label", native.getAttribute("aria-label"));
225 }
226
227 const boxes = new Map();
228 for (const g of groups) {
229 const box = el("li");
230 box.setAttribute("role", "group");
231 box.setAttribute("aria-label", g.label);
232 const rows = el("ul", "rst-combo-rows");
233 rows.setAttribute("role", "none");
234 const head = el("li", "rst-select-group");
235 head.setAttribute("aria-hidden", "true");
236 head.textContent = g.label;
237 rows.appendChild(head);
238 box.appendChild(rows);
239 boxes.set(g, { box, rows });
240 }
241 const home = (o) => (o.group ? boxes.get(o.group).rows : list);
242
243 for (const o of opts) {
244 const li = el("li", "rst-combo-option");
245 li.id = listId + "-" + o.index;
246 li.setAttribute("role", "option");
247 li.setAttribute("aria-selected", "false");
248 if (o.disabled) li.setAttribute("aria-disabled", "true");
249 if (o.classes.length) li.classList.add(...o.classes);
250 if (o.lead) {
251 const glyph = el("span", "rst-combo-lead");
252 glyph.setAttribute("aria-hidden", "true");
253 glyph.textContent = o.lead;
254 li.append(glyph);
255 }
256 if (o.desc) {
257 const name = el("span", "rst-combo-name");
258 name.textContent = o.name;
259 const desc = el("span", "rst-combo-desc");
260 desc.textContent = o.desc;
261 li.append(name, desc);
262 } else {
263 li.append(o.text);
264 }
265 o.li = li;
266 if (o.group && !list.contains(boxes.get(o.group).box)) list.appendChild(boxes.get(o.group).box);
267 if (o.sepBefore) {
268 o.sep = el("li", "rst-combo-sep");
269 o.sep.setAttribute("aria-hidden", "true");
270 home(o).appendChild(o.sep);
271 }
272 home(o).appendChild(li);
273 }
274 const empty = el("li", "rst-combo-empty");
275 empty.style.display = "none";
276 empty.textContent = say("no-matches", "No matches");
277 list.appendChild(empty);
278
279 native.parentNode.insertBefore(wrap, native);
280 wrap.append(lead, input, said, list, status);
281 const inputAttr = native.getAttribute("rst-input");
282 native.removeAttribute("rst-input");
283 native.classList.add("rst-sr-only");
284 native.setAttribute("tabindex", "-1");
285 native.setAttribute("aria-hidden", "true");
286
287 let open = false;
288 let active = null;
289 let extras = 0;
290
291 const selectedEl = () => native.options[native.selectedIndex] || null;
292 const picked = () => opts.find((o) => o.el && o.el === selectedEl()) || null;
293 const currentText = () => {
294 const o = picked();
295 return o && !o.prompt ? o.short || o.name : "";
296 };
297 let showingPick = true;
298 const showText = () => {
299 input.value = currentText();
300 showingPick = true;
301 };
302 const query = () => (showingPick ? "" : input.value);
303 let worn = [];
304 const wear = (classes) => {
305 input.classList.remove(...worn);
306 worn = classes;
307 input.classList.add(...worn);
308 };
309 const showPick = () => {
310 const o = picked();
311 wear(o && !o.prompt ? o.classes : []);
312 lead.textContent = (o && o.lead) || "";
313 lead.hidden = !lead.textContent;
314 if (wrap.hasAttribute("rst-combo-has-lead") === lead.hidden) wrap.toggleAttribute("rst-combo-has-lead", !lead.hidden);
315 said.textContent = o && o.short ? [o.name, o.desc].filter(Boolean).join(" ") : "";
316 };
317 showText();
318 showPick();
319
320 let steered = false;
321 const settledPick = () => {
322 if (!open || !active || active.run) return null;
323 if (steered) return active.el === selectedEl() ? null : active;
324 if (input.value === currentText()) return null;
325 const pick = settle(lastHits, input.value);
326 return pick && pick === (lastHits.length === 1 ? pick : active) ? pick : null;
327 };
328
329 let shown = opts;
330 const isShown = (e) => e.style.display !== "none";
331 const visible = () => shown.filter((o) => isShown(o.li) && !o.disabled);
332 const firstReal = () => visible().find((o) => !o.run && !o.prompt) || null;
333 const unanswered = () => {
334 const sel = picked();
335 return !sel || sel.prompt;
336 };
337 const opening = () => {
338 if (unanswered()) return null;
339 const sel = picked();
340 return sel && isShown(sel.li) && !sel.disabled ? sel : firstReal();
341 };
342 const steps = () => visible().filter((o) => !o.prompt);
343
344 let layout = 0;
345 let placeDue = false;
346 let scrollTo = null;
347 let scrollTop = false;
348 const onLayout = () => {
349 layout = 0;
350 if (!open) return;
351 if (placeDue) place();
352 placeDue = false;
353 if (scrollTop) list.scrollTop = 0;
354 else if (scrollTo && isShown(scrollTo.li)) scrollTo.li.scrollIntoView({ block: "nearest" });
355 scrollTo = null;
356 scrollTop = false;
357 };
358 const soon = () => {
359 if (!layout) layout = requestAnimationFrame(onLayout);
360 };
361 let preferred = 0;
362 let gap = 0;
363 let chrome = 0;
364 let scrollers = [];
365 let floors = [];
366 const place = () => {
367 if (!preferred) {
368 if (wrap.hasAttribute("rst-combo-up")) wrap.removeAttribute("rst-combo-up");
369 list.style.maxBlockSize = "";
370 const cs = getComputedStyle(list);
371 scrollers = clippers(list, cs.position);
372 preferred = parseFloat(cs.maxBlockSize) || 240;
373 gap = parseFloat(cs.marginTop) || 0;
374 chrome = cs.boxSizing === "border-box" ? 0 : ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth"].reduce((n, k) => n + (parseFloat(cs[k]) || 0), 0);
375 }
376 const vv = window.visualViewport;
377 let top = vv ? vv.offsetTop : 0;
378 let bottom = vv ? vv.offsetTop + vv.height : window.innerHeight;
379 if (told) {
380 top = Math.max(top, told.top);
381 bottom = Math.min(bottom, told.bottom);
382 }
383 for (const a of scrollers) {
384 const r = a.getBoundingClientRect();
385 top = Math.max(top, r.top);
386 bottom = Math.min(bottom, r.bottom);
387 }
388 const box = wrap.getBoundingClientRect();
389 for (const f of floors) {
390 const r = f.getBoundingClientRect();
391 if (!r.height) continue;
392 if (!f.hasAttribute("data-rst-select-ceiling")) { if (r.top >= box.bottom - 1) bottom = Math.min(bottom, r.top); }
393 else if (r.bottom <= box.top + 1) top = Math.max(top, r.bottom);
394 }
395 const below = bottom - box.bottom - gap;
396 const above = box.top - top - gap;
397 const up = below < preferred + chrome && above > below;
398 const h = Math.floor(Math.max(0, Math.min(preferred + chrome, up ? above : below) - chrome)) + "px";
399 if (list.style.maxBlockSize !== h) list.style.maxBlockSize = h;
400 if (up !== wrap.hasAttribute("rst-combo-up")) wrap.toggleAttribute("rst-combo-up", up);
401 };
402 const replace = (e) => {
403 if (!wrap.isConnected) return closeList(false);
404 if (e && e.type === "resize" && e.currentTarget === window) preferred = 0;
405 placeDue = true;
406 soon();
407 };
408 const scrolled = (e) => {
409 if (e.target !== list) replace();
410 };
411 const setActive = (o, atTop) => {
412 if (active && active !== o) active.li.classList.remove("is-active");
413 if (o && !o.li.classList.contains("is-active")) o.li.classList.add("is-active");
414 active = o || null;
415 if (o) {
416 if (input.getAttribute("aria-activedescendant") !== o.li.id) input.setAttribute("aria-activedescendant", o.li.id);
417 scrollTop = !!(atTop && visible()[0] === o);
418 scrollTo = o;
419 soon();
420 } else if (input.hasAttribute("aria-activedescendant")) {
421 input.removeAttribute("aria-activedescendant");
422 }
423 };
424
425 const markSelected = () => {
426 for (const o of opts) {
427 const v = o.el && o.el === selectedEl() && !o.prompt ? "true" : "false";
428 if (o.li.getAttribute("aria-selected") !== v) o.li.setAttribute("aria-selected", v);
429 }
430 };
431
432 let pending = false;
433 let frame = 0;
434 let lastQuery = null;
435 let lastHits = [];
436 const lifted = new Set();
437 const anchorAfter = (o) => {
438 const next = opts[o.index + 1];
439 if (next && !next.run && next.group === o.group) return next.sep || next.li;
440 if (o.group) return null;
441 if (next && !next.run) return next.group ? boxes.get(next.group).box : next.sep || next.li;
442 return empty;
443 };
444 const show = (e, on) => {
445 if (isShown(e) !== on) e.style.display = on ? "" : "none";
446 };
447 const filter = (query) => {
448 pending = false;
449 const own = opts.filter((o) => !o.run);
450 const searching = fold(query) !== "";
451 const answerable = (o) =>
452 !o.blank || offered(o.el.value, o.marked, native.required, searching, o.disabled);
453 const hits = order(own.filter(answerable), query);
454 lastHits = hits.filter((o) => !o.disabled);
455 const hit = new Set(hits);
456 const extra = opts.filter((o) => o.run);
457 shown = searching ? [...hits, ...own.filter((o) => !hit.has(o)), ...extra] : opts;
458 const tail = extra.length ? extra[0].li : null;
459 if (empty.nextSibling !== tail || empty.parentNode !== list) list.insertBefore(empty, tail);
460 if (searching) {
461 const hitRow = new Set(hits.map((o) => o.li));
462 let ref = list.firstChild;
463 for (const o of hits) {
464 while (ref && ref !== empty && ref !== o.li && !hitRow.has(ref)) ref = ref.nextSibling;
465 if (ref === o.li) {
466 ref = ref.nextSibling;
467 } else {
468 list.insertBefore(o.li, ref);
469 lifted.add(o);
470 }
471 }
472 } else if (lifted.size) {
473 for (const o of [...lifted].sort((a, b) => b.index - a.index)) home(o).insertBefore(o.li, anchorAfter(o));
474 lifted.clear();
475 }
476 for (const o of own) {
477 show(o.li, hit.has(o));
478 if (o.sep) show(o.sep, !searching);
479 }
480 for (const { box } of boxes.values()) show(box, !searching);
481 for (const o of extra) show(o.li, true);
482 const found = hits.filter((o) => !o.disabled).length;
483 show(empty, !found);
484 const text = found === 1 ? oneFmt : manyFmt.replace("{n}", String(found));
485 if (status.textContent !== text) status.textContent = text;
486 const requeried = query !== lastQuery;
487 if (requeried) steered = false;
488 lastQuery = query;
489 if (!searching && !steered && (requeried || unanswered())) setActive(opening());
490 else if ((searching && requeried) || !active || !shown.includes(active) || !isShown(active.li) || active.disabled) {
491 setActive(firstReal(), true);
492 }
493 };
494 const flush = () => {
495 if (pending && open) filter(input.value);
496 };
497 const onFrame = () => {
498 frame = 0;
499 flush();
500 };
501 const searchTyping = () => {
502 if (frame) {
503 pending = true;
504 return;
505 }
506 frame = requestAnimationFrame(onFrame);
507 filter(input.value);
508 };
509
510 const openList = (showAll) => {
511 if (open || locked()) return;
512 open = true;
513 list.hidden = false;
514 input.setAttribute("aria-expanded", "true");
515 floors = [...document.querySelectorAll("[data-rst-select-floor],[data-rst-select-ceiling]")];
516 preferred = 0;
517 replace();
518 window.addEventListener("resize", replace);
519 document.addEventListener("rst:select-viewport", replace);
520 document.addEventListener("scroll", scrolled, true);
521 if (window.visualViewport) {
522 window.visualViewport.addEventListener("resize", replace);
523 window.visualViewport.addEventListener("scroll", replace);
524 }
525 if (showAll) filter("");
526 markSelected();
527 setActive(opening());
528 };
529
530 const closeList = (revert) => {
531 if (!open) return;
532 open = false;
533 list.hidden = true;
534 window.removeEventListener("resize", replace);
535 document.removeEventListener("rst:select-viewport", replace);
536 document.removeEventListener("scroll", scrolled, true);
537 if (window.visualViewport) {
538 window.visualViewport.removeEventListener("resize", replace);
539 window.visualViewport.removeEventListener("scroll", replace);
540 }
541 input.setAttribute("aria-expanded", "false");
542 input.removeAttribute("aria-activedescendant");
543 pending = false;
544 cancelAnimationFrame(frame);
545 frame = 0;
546 lastQuery = null;
547 if (active) active.li.classList.remove("is-active");
548 if (revert) {
549 showText();
550 showPick();
551 }
552 };
553
554 const choose = (o) => {
555 if (!o || o.disabled || locked()) return;
556 if (o.run) {
557 o.run();
558 return;
559 }
560 if (!native.contains(o.el)) {
561 closeList(true);
562 return;
563 }
564 o.el.selected = true;
565 native.dispatchEvent(new Event("change", { bubbles: true }));
566 showText();
567 showPick();
568 markSelected();
569 closeList(false);
570 };
571
572 let resuming = false;
573 let pointerFocus = "";
574 const selectPick = () => {
575 if (document.activeElement === input && showingPick && input.selectionStart === input.selectionEnd) {
576 input.setSelectionRange(0, input.value.length);
577 }
578 };
579 input.addEventListener("pointerdown", (e) => {
580 pointerFocus = document.activeElement === input ? "" : e.pointerType || "mouse";
581 });
582 input.addEventListener("focus", () => {
583 if (resuming) return;
584 if (pointerFocus !== "mouse") input.select();
585 openList(true);
586 });
587 input.addEventListener("click", () => {
588 if (pointerFocus) {
589 pointerFocus = "";
590 selectPick();
591 requestAnimationFrame(selectPick);
592 }
593 openList(true);
594 });
595 input.addEventListener("input", () => {
596 showingPick = false;
597 if (!lead.hidden) {
598 lead.hidden = true;
599 wrap.removeAttribute("rst-combo-has-lead");
600 }
601 if (worn.length) wear([]);
602 openList(false);
603 searchTyping();
604 });
605
606 let tookEnter = false;
607 input.addEventListener("keydown", (e) => {
608 tookEnter = false;
609 if (ACTS.has(e.key)) flush();
610 switch (e.key) {
611 case "ArrowDown": {
612 e.preventDefault();
613 if (!open) return openList(true);
614 const v = steps();
615 steered = true;
616 setActive(v[Math.min(v.indexOf(active) + 1, v.length - 1)] || v[0] || null);
617 break;
618 }
619 case "ArrowUp": {
620 e.preventDefault();
621 if (!open) return openList(true);
622 const v = steps();
623 steered = true;
624 setActive((active ? v[Math.max(v.indexOf(active) - 1, 0)] : v[v.length - 1]) || null);
625 break;
626 }
627 case "Home":
628 case "End":
629 if (open) {
630 e.preventDefault();
631 steered = true;
632 const v = steps();
633 setActive((e.key === "Home" ? v[0] : v[v.length - 1]) || null);
634 }
635 break;
636 case "Enter":
637 if (open) {
638 e.preventDefault();
639 tookEnter = true;
640 if (active) choose(active);
641 }
642 break;
643 case "Escape":
644 if (open) {
645 e.preventDefault();
646 e.stopPropagation();
647 closeList(true);
648 }
649 break;
650 case "Tab":
651 if (settledPick()) choose(settledPick());
652 else closeList(true);
653 break;
654 }
655 });
656
657 input.addEventListener("keypress", (e) => {
658 if (e.key === "Enter" && tookEnter) e.preventDefault();
659 tookEnter = false;
660 });
661
662 list.addEventListener("mousedown", (e) => {
663 const li = e.target.closest("[rst-combo-option]");
664 if (!li) return;
665 e.preventDefault();
666 choose(opts.find((o) => o.li === li));
667 });
668
669 wrap.addEventListener("focusout", (e) => {
670 if (gone) return;
671 if (wrap.contains(e.relatedTarget)) return;
672 flush();
673 if (settledPick()) choose(settledPick());
674 else closeList(true);
675 });
676
677 let given = [];
678 wrap.rstExtras = (rows) => {
679 given = rows || [];
680 for (let i = opts.length - 1; i >= 0; i--) {
681 if (!opts[i].run) break;
682 opts[i].li.remove();
683 opts.splice(i, 1);
684 }
685 for (const row of rows || []) {
686 const li = el("li", "rst-combo-option");
687 li.id = listId + "-extra-" + ++extras;
688 li.setAttribute("role", "option");
689 li.setAttribute("aria-selected", "false");
690 const name = el("span", "rst-combo-name");
691 name.textContent = row.label;
692 li.append(name);
693 if (row.meta) {
694 const meta = el("span", "rst-combo-desc");
695 meta.textContent = row.meta;
696 li.append(meta);
697 }
698 list.appendChild(li);
699 opts.push({ text: row.label, name: row.label, desc: row.meta || "", short: "", lead: "",
700 terms: [], classes: [], first: false, disabled: false, index: -1, group: null, el: null, li, run: row.run });
701 }
702 shown = opts;
703 if (open) filter(query());
704 };
705
706 const mine = new AbortController();
707 const follow = () => {
708 showText();
709 showPick();
710 markSelected();
711 input.setCustomValidity("");
712 settleValidity();
713 };
714 native.addEventListener("change", follow, { signal: mine.signal });
715 document.addEventListener("reset", (e) => {
716 if (e.target !== native.form) return;
717 setTimeout(() => {
718 if (e.defaultPrevented || gone) return;
719 closeList(false);
720 follow();
721 }, 0);
722 }, { signal: mine.signal });
723
724 const settleValidity = () => {
725 if (!native.validity.valid && native.willValidate) return;
726 input.removeAttribute("aria-invalid");
727 input.setCustomValidity("");
728 };
729 native.addEventListener("invalid", () => input.setAttribute("aria-invalid", "true"), { signal: mine.signal });
730 const settleRequired = () => {
731 if (native.required) input.setAttribute("aria-required", "true");
732 else input.removeAttribute("aria-required");
733 };
734 settleRequired();
735 const flips = new MutationObserver(() => {
736 input.disabled = native.disabled;
737 if (native.disabled) closeList(false);
738 settleRequired();
739 settleValidity();
740 for (const o of opts) {
741 if (!o.run) o.prompt = isPrompt(o.el.value, o.marked, native.required, offLimits(o.el));
742 }
743 markSelected();
744 if (open) {
745 if (showingPick) {
746 showText();
747 const o = picked();
748 wear(o && !o.prompt ? o.classes : []);
749 }
750 filter(query());
751 } else {
752 showText();
753 showPick();
754 }
755 });
756 flips.observe(native, { attributes: true, attributeFilter: ["required", "disabled"] });
757
758 let gone = false;
759 const stepAside = () => {
760 gone = true;
761 mine.abort();
762 flips.disconnect();
763 options.disconnect();
764 swapped.disconnect();
765 closeList(false);
766 cancelAnimationFrame(frame);
767 cancelAnimationFrame(layout);
768 native.classList.remove("rst-sr-only");
769 native.removeAttribute("tabindex");
770 native.removeAttribute("aria-hidden");
771 if (inputAttr !== null) native.setAttribute("rst-input", inputAttr);
772 delete native.dataset.rstEnhanced;
773 enhanced.delete(native);
774 if (label) label.htmlFor = native.id;
775 wrap.remove();
776 };
777 const options = new MutationObserver((records) => {
778 if (!records.some((r) => r.target !== native || r.type !== "attributes")) return;
779 if (!native.isConnected || native.parentNode !== wrap.parentNode) {
780 stepAside();
781 queueMicrotask(() => scan());
782 return;
783 }
784 flush();
785 const resume = {
786 focused: document.activeElement === input, open, typed: showingPick ? null : input.value,
787 at: active && active.el, extra: active && active.run ? opts.filter((o) => o.run).indexOf(active) : -1,
788 steered, given, tookEnter,
789 caret: [input.selectionStart, input.selectionEnd, input.selectionDirection],
790 };
791 stepAside();
792 combo(native, resume);
793 });
794 options.observe(native, { childList: true, subtree: true, characterData: true, attributes: true,
795 attributeFilter: ["value", "label", "disabled", "data-rst-prompt"] });
796 const swapped = new MutationObserver(() => {
797 if (native.isConnected && native.parentNode === wrap.parentNode) return;
798 stepAside();
799 queueMicrotask(() => scan());
800 });
801 if (wrap.parentNode) swapped.observe(wrap.parentNode, { childList: true });
802 native.addEventListener("focus", () => {
803 input.focus();
804 if (!native.validity.valid) {
805 input.setCustomValidity(native.validationMessage);
806 input.reportValidity();
807 }
808 }, { signal: mine.signal });
809 input.addEventListener("input", () => input.setCustomValidity(""));
810 input.addEventListener("blur", () => {
811 pointerFocus = "";
812 if (gone) return;
813 input.setCustomValidity("");
814 settleValidity();
815 });
816 if (resume) {
817 resuming = true;
818 if (resume.focused) input.focus();
819 resuming = false;
820 tookEnter = resume.tookEnter;
821 if (resume.given.length) wrap.rstExtras(resume.given);
822 if (resume.open) {
823 openList(resume.typed === null);
824 if (resume.typed !== null) {
825 input.value = resume.typed;
826 showingPick = false;
827 lead.hidden = true;
828 wrap.removeAttribute("rst-combo-has-lead");
829 wear([]);
830 filter(resume.typed);
831 }
832 const was = resume.extra >= 0 ? opts.filter((o) => o.run)[resume.extra]
833 : resume.at && opts.find((o) => o.el === resume.at && isShown(o.li) && !o.disabled);
834 if (was) {
835 setActive(was);
836 steered = resume.steered;
837 }
838 }
839 if (resume.focused) input.setSelectionRange(...resume.caret);
840 }
841 return input;
842 }
843
844 function scan(root) {
845 const within = root && root.querySelectorAll ? root : document;
846 const found = [...within.querySelectorAll("select[data-rst-select]")];
847 if (within.matches && within.matches("select[data-rst-select]")) found.push(within);
848 found.forEach((s) => {
849 if (s.dataset.rstSelect !== "false" && !s.multiple) combo(s);
850 });
851 }
852
853 if (document.readyState === "loading") {
854 document.addEventListener("DOMContentLoaded", () => scan());
855 } else {
856 scan();
857 }
858 document.addEventListener("rst:select-scan", (e) => queueMicrotask(() => scan(e.detail && e.detail.root)));
859})();
860