/* * Eine Auswahlliste fuer die ganze Anwendung. * * Die Liste, die der Browser zu einem seinen eigenen Knopf; * das im Dokument werden von selbst uebernommen (MutationObserver), * auch die, die der Automatik-Editor bei jedem Zeichnen neu baut. * * Ausnahmen: und alles mit * data-auswahl="aus". */ const AUSWAHL_SUCHE_AB = 9; // ab so vielen Eintraegen ein Suchfeld const AUSWAHL_MIN_BREITE = 190; const AUSWAHL_MAX_HOEHE = 300; let auswahlOffen = null; // { select, knopf, panel, eintraege } /** Die Beschriftung des Knopfes aus dem gewaehlten Eintrag nachziehen. */ function auswahlAktualisieren(select) { const huelle = select.closest(".aw"); if (!huelle) return; const gewaehlt = select.selectedOptions[0]; const text = huelle.querySelector(".aw-wert"); text.textContent = gewaehlt ? gewaehlt.textContent : "–"; text.classList.toggle("aw-leer", !gewaehlt); huelle.querySelector(".aw-knopf").disabled = select.disabled; } /** Ein wandern mit: daran haengt, wie das Feld an // dieser Stelle aussehen soll (.feld-als-text im Editor, .kw-format in den // Einstellungen). Nur Bootstraps eigene Feldgestaltung bleibt weg - die // bringt ihren eigenen hellen Pfeil mit. const klassen = [...select.classList].filter(c => !c.startsWith("form-select") && !c.startsWith("form-control")); knopf.className = ["aw-knopf"].concat(klassen).join(" "); if (klassen.indexOf("feld-als-text") >= 0) huelle.classList.add("aw-text"); knopf.setAttribute("aria-haspopup", "listbox"); knopf.setAttribute("aria-expanded", "false"); const zettel = select.id ? document.querySelector("label[for='" + CSS.escape(select.id) + "']") : null; const beschriftung = select.getAttribute("aria-label") || (zettel ? zettel.textContent.trim() : ""); if (beschriftung) knopf.setAttribute("aria-label", beschriftung); knopf.innerHTML = ""; select.parentNode.insertBefore(huelle, select); huelle.appendChild(knopf); huelle.appendChild(select); auswahlAktualisieren(select); select.addEventListener("change", () => auswahlAktualisieren(select)); knopf.addEventListener("click", () => { if (auswahlOffen && auswahlOffen.select === select) auswahlSchliessen(); else auswahlOeffnen(select, knopf); }); knopf.addEventListener("keydown", e => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); auswahlOeffnen(select, knopf); } }); } /** Alle noch nicht ergaenzten "; } const liste = document.createElement("div"); liste.className = "aw-liste"; liste.setAttribute("role", "listbox"); panel.appendChild(liste); // Gruppen bleiben Gruppen: in den Einstellungen stehen die Geraete nach // Etagen sortiert, und ohne die Zwischenueberschrift waere die Liste eine // Wand aus Namen. const eintraege = []; const bauen = function (opt) { const k = document.createElement("button"); k.type = "button"; k.className = "aw-eintrag"; k.setAttribute("role", "option"); k.setAttribute("aria-selected", String(opt.selected)); k.disabled = opt.disabled; k.textContent = opt.textContent; k.addEventListener("click", () => auswahlWaehlen(opt)); liste.appendChild(k); eintraege.push({ knopf: k, option: opt }); }; [...select.children].forEach(kind => { if (kind.tagName === "OPTGROUP") { const titel = document.createElement("div"); titel.className = "aw-gruppe"; titel.textContent = kind.label; liste.appendChild(titel); [...kind.children].forEach(bauen); } else if (kind.tagName === "OPTION") { bauen(kind); } }); document.body.appendChild(panel); auswahlOffen = { select: select, knopf: knopf, panel: panel, eintraege: eintraege }; knopf.setAttribute("aria-expanded", "true"); auswahlStellen(); const gewaehlt = eintraege.findIndex(e => e.option.selected); if (gewaehlt >= 0) { eintraege[gewaehlt].knopf.classList.add("aktiv"); eintraege[gewaehlt].knopf.scrollIntoView({ block: "center" }); } const suche = panel.querySelector(".aw-suche"); if (suche) { suche.focus({ preventScroll: true }); suche.addEventListener("input", () => { const text = suche.value.trim().toLowerCase(); eintraege.forEach(e => { e.knopf.hidden = text !== "" && e.knopf.textContent.toLowerCase().indexOf(text) < 0; }); // Ein Gruppentitel ohne sichtbare Eintraege darunter verschwindet mit. [...liste.querySelectorAll(".aw-gruppe")].forEach(titel => { let el = titel.nextElementSibling, sichtbar = false; while (el && !el.classList.contains("aw-gruppe")) { if (!el.hidden) { sichtbar = true; break; } el = el.nextElementSibling; } titel.hidden = !sichtbar; }); auswahlStellen(); }); } } /** Die Liste an ihren Platz legen: unter den Knopf, sonst darueber. */ function auswahlStellen() { if (!auswahlOffen) return; const knopf = auswahlOffen.knopf, panel = auswahlOffen.panel; const r = knopf.getBoundingClientRect(); const breite = Math.min(Math.max(r.width, AUSWAHL_MIN_BREITE), window.innerWidth - 16); panel.style.width = breite + "px"; const platzUnten = window.innerHeight - r.bottom - 12; const platzOben = r.top - 12; const unten = platzUnten >= Math.min(AUSWAHL_MAX_HOEHE, panel.scrollHeight) || platzUnten >= platzOben; panel.style.maxHeight = Math.min(AUSWAHL_MAX_HOEHE, unten ? platzUnten : platzOben) + "px"; panel.style.left = Math.max(8, Math.min(r.left, window.innerWidth - breite - 8)) + "px"; panel.style.top = unten ? (r.bottom + 4) + "px" : ""; panel.style.bottom = unten ? "" : (window.innerHeight - r.top + 4) + "px"; } function auswahlWaehlen(option) { if (!auswahlOffen) return; const select = auswahlOffen.select; const knopf = auswahlOffen.knopf; auswahlSchliessen(); if (option.disabled) return; if (select.value !== option.value) { select.value = option.value; auswahlAktualisieren(select); select.dispatchEvent(new Event("input", { bubbles: true })); select.dispatchEvent(new Event("change", { bubbles: true })); } // Nach einem Neuzeichnen - der Editor baut seine Zeilen neu - gibt es den // Knopf vielleicht gar nicht mehr. if (document.body.contains(knopf)) knopf.focus(); } function auswahlSchritt(richtung) { const sichtbar = auswahlOffen.eintraege.filter(e => !e.knopf.hidden && !e.knopf.disabled); if (!sichtbar.length) return; const jetzt = sichtbar.findIndex(e => e.knopf.classList.contains("aktiv")); const naechst = jetzt < 0 ? (richtung > 0 ? 0 : sichtbar.length - 1) : (jetzt + richtung + sichtbar.length) % sichtbar.length; auswahlOffen.eintraege.forEach(e => e.knopf.classList.remove("aktiv")); sichtbar[naechst].knopf.classList.add("aktiv"); sichtbar[naechst].knopf.scrollIntoView({ block: "nearest" }); } document.addEventListener("pointerdown", e => { if (!auswahlOffen) return; if (e.target.closest(".aw-panel") || e.target.closest(".aw")) return; auswahlSchliessen(); }, true); document.addEventListener("keydown", e => { if (!auswahlOffen) return; if (e.key === "Escape") { // Ohne das Anhalten macht Bootstrap das Modal darunter gleich mit zu. e.preventDefault(); e.stopPropagation(); auswahlSchliessen(true); } else if (e.key === "ArrowDown") { e.preventDefault(); auswahlSchritt(1); } else if (e.key === "ArrowUp") { e.preventDefault(); auswahlSchritt(-1); } else if (e.key === "Enter") { const aktiv = auswahlOffen.eintraege.find(x => x.knopf.classList.contains("aktiv") && !x.knopf.hidden); if (aktiv) { e.preventDefault(); auswahlWaehlen(aktiv.option); } } else if (e.key === "Tab") { auswahlSchliessen(); } }, true); // Scrollt die Seite oder das Modal, wandert die Liste mit; ist der Knopf dabei // aus dem Bild, geht sie zu. window.addEventListener("scroll", () => { if (!auswahlOffen) return; const r = auswahlOffen.knopf.getBoundingClientRect(); if (r.bottom < 0 || r.top > window.innerHeight) auswahlSchliessen(); else auswahlStellen(); }, true); window.addEventListener("resize", () => auswahlSchliessen()); document.addEventListener("DOMContentLoaded", function () { auswahlAlle(document); // Was spaeter dazukommt - Modals, neu gezeichnete Editorzeilen - wird von // selbst uebernommen; die Aufrufer muessen daran nicht denken. new MutationObserver(aenderungen => { for (const a of aenderungen) { for (const knoten of a.addedNodes) { if (knoten.nodeType !== 1) continue; if (knoten.tagName === "SELECT") auswahlAufsetzen(knoten); else auswahlAlle(knoten); } } }).observe(document.body, { childList: true, subtree: true }); });