Eigene Auswahllisten ueberall: dunkel und ueber der Seite statt im Fluss
Chrome zeichnet die Liste eines <select> hell auf grau und laesst sich davon nicht abbringen. js/solar/auswahl.js ergaenzt jedes <select> um einen eigenen Knopf; die Liste haengt an <body> und liegt ueber der Seite, damit ein Modal nicht hoeher wird und der Inhalt nicht springt. Ab neun Eintraegen mit Suchfeld. Der eigene Picker im Raumfenster faellt dafuer weg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,17 @@ Lampen mit Birne als Schalter, Farbfeldern und Effekten. Jede Wahl gilt
|
|||||||
sofort. Jalousien werden nach einem Befehl bis zum Stillstand verfolgt
|
sofort. Jalousien werden nach einem Befehl bis zum Stillstand verfolgt
|
||||||
(`rfJalousieVerfolgen()`), die Heizwerte kommen über `raumfensterMqtt()` live.
|
(`rfJalousieVerfolgen()`), die Heizwerte kommen über `raumfensterMqtt()` live.
|
||||||
|
|
||||||
|
**`auswahl.js`** — eine Auswahlliste für die ganze Anwendung. Wird auf jeder
|
||||||
|
Seite geladen (`footer.php`) und ergänzt jedes `<select>` um einen eigenen
|
||||||
|
Knopf; das `<select>` bleibt stehen und hält weiter den Wert, vorhandene
|
||||||
|
`onchange`-Zuhörer merken nichts davon. Die aufgeklappte Liste hängt an
|
||||||
|
`<body>` und liegt mit `position: fixed` über der Seite — im Fluss machte sie
|
||||||
|
ein Modal höher, und der ganze Inhalt sprang. Ab neun Einträgen gibt es ein
|
||||||
|
Suchfeld, Gruppen (`<optgroup>`) bleiben Gruppen, die Tastatur bedient sie.
|
||||||
|
Neue `<select>` übernimmt ein `MutationObserver` von selbst. Wer den Wert im
|
||||||
|
Code setzt, ruft `auswahlAktualisieren(select)` oder löst `change` aus;
|
||||||
|
`data-auswahl="aus"` lässt ein Feld in Ruhe.
|
||||||
|
|
||||||
**`solarMQTT.js`** — die Solar-Seite. Startet die Energiefluss-Übersicht
|
**`solarMQTT.js`** — die Solar-Seite. Startet die Energiefluss-Übersicht
|
||||||
(`energieflussStarten()`) samt Anlage-Ansicht, füllt die Bewässerungszonen,
|
(`energieflussStarten()`) samt Anlage-Ansicht, füllt die Bewässerungszonen,
|
||||||
darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
|
darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
|
||||||
@@ -887,6 +898,7 @@ ausgeliefert.
|
|||||||
| Ein anderes Haus einrichten | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge; dann Einstellungen → Grundriss und → Übersicht |
|
| Ein anderes Haus einrichten | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge; dann Einstellungen → Grundriss und → Übersicht |
|
||||||
| Kreis der Energiefluss-Übersicht aus, umbenennen, verschieben; PV-Flächen | Einstellungen → Übersicht (keine Codeänderung) |
|
| Kreis der Energiefluss-Übersicht aus, umbenennen, verschieben; PV-Flächen | Einstellungen → Übersicht (keine Codeänderung) |
|
||||||
| Neuer Kreis in der Energiefluss-Übersicht (Gerät, Etage, Quelle) | ein Eintrag in `js/solar/energieflussKatalog.js`; neues Symbol dazu in `SYMBOLE` (`energiefluss.js`) |
|
| Neuer Kreis in der Energiefluss-Übersicht (Gerät, Etage, Quelle) | ein Eintrag in `js/solar/energieflussKatalog.js`; neues Symbol dazu in `SYMBOLE` (`energiefluss.js`) |
|
||||||
|
| Auswahlliste irgendwo | ein gewöhnliches `<select>` schreiben — `js/solar/auswahl.js` macht daraus die eigene Liste. Keine zweite Variante bauen |
|
||||||
| Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` |
|
| Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` |
|
||||||
| Symbol für eine Gerätegruppe (Raum-Modal, Zeitleiste, Editor) | ein Bootstrap-Icon in `raumGruppenTitel()` (`roomControls.php`), `zeitleisteBahnen()` (`zeitleiste.php`) und `GERAETE_SYMBOL` (`autoActionFuncs.js`) — überall dasselbe. Fehlt es in der Icon-Schrift, eine eigene Maske in `css/solar.css` anlegen (Vorbild: `bi-beschattung`) |
|
| Symbol für eine Gerätegruppe (Raum-Modal, Zeitleiste, Editor) | ein Bootstrap-Icon in `raumGruppenTitel()` (`roomControls.php`), `zeitleisteBahnen()` (`zeitleiste.php`) und `GERAETE_SYMBOL` (`autoActionFuncs.js`) — überall dasselbe. Fehlt es in der Icon-Schrift, eine eigene Maske in `css/solar.css` anlegen (Vorbild: `bi-beschattung`) |
|
||||||
| Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts |
|
| Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts |
|
||||||
|
|||||||
+78
-19
@@ -111,6 +111,78 @@ body.bg-body-tertiary,
|
|||||||
mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Crect x=%221.5%22 y=%221.6%22 width=%2213%22 height=%222.6%22 rx=%22.9%22/%3E%3Crect x=%222.6%22 y=%225.7%22 width=%2210.8%22 height=%221.7%22 rx=%22.85%22/%3E%3Crect x=%222.6%22 y=%228.6%22 width=%2210.8%22 height=%221.7%22 rx=%22.85%22/%3E%3Crect x=%221.5%22 y=%2211.8%22 width=%2213%22 height=%222.6%22 rx=%22.9%22/%3E%3C/svg%3E") center / contain no-repeat;
|
mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Crect x=%221.5%22 y=%221.6%22 width=%2213%22 height=%222.6%22 rx=%22.9%22/%3E%3Crect x=%222.6%22 y=%225.7%22 width=%2210.8%22 height=%221.7%22 rx=%22.85%22/%3E%3Crect x=%222.6%22 y=%228.6%22 width=%2210.8%22 height=%221.7%22 rx=%22.85%22/%3E%3Crect x=%221.5%22 y=%2211.8%22 width=%2213%22 height=%222.6%22 rx=%22.9%22/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Auswahllisten -------------------------------------------------------
|
||||||
|
Eigener Knopf statt der Liste, die der Browser zeichnet (js/solar/auswahl.js).
|
||||||
|
Chrome malt sie hell auf grau, und gestalten laesst sie sich nicht. Die
|
||||||
|
aufgeklappte Liste haengt an <body> und liegt ueber der Seite - im Fluss
|
||||||
|
machte sie ein Modal hoeher, und der ganze Inhalt sprang.
|
||||||
|
-------------------------------------------------------------------------- */
|
||||||
|
.aw { display: block; position: relative; min-width: 0; }
|
||||||
|
.aw.aw-text { display: inline-block; max-width: 100%; }
|
||||||
|
.aw > select { display: none; }
|
||||||
|
.aw .aw-knopf {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: .4rem;
|
||||||
|
width: 100%; min-width: 0; padding: .35rem .6rem;
|
||||||
|
border: 1px solid var(--ton-linie-satt, rgba(138, 148, 158, .38));
|
||||||
|
border-radius: var(--ton-radius-klein, .5rem);
|
||||||
|
background: var(--ton-flaeche-2, #2b3237); color: var(--ton-schrift, #e8ede9);
|
||||||
|
font: inherit; text-align: left; cursor: pointer;
|
||||||
|
}
|
||||||
|
.aw .aw-knopf:hover { border-color: var(--bs-link-color, #f0b429); }
|
||||||
|
.aw .aw-knopf:focus-visible { outline: 2px solid var(--bs-link-color, #f0b429); outline-offset: 1px; }
|
||||||
|
.aw .aw-knopf[aria-expanded="true"] { border-color: var(--bs-link-color, #f0b429); }
|
||||||
|
.aw .aw-knopf:disabled { opacity: .55; cursor: default; }
|
||||||
|
.aw .aw-wert { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.aw .aw-wert.aw-leer { color: var(--ton-schrift-3, #6f7a81); }
|
||||||
|
.aw .aw-knopf > .bi { flex: none; font-size: .7em; color: var(--ton-schrift-2, #9aa6ad); transition: transform .2s; }
|
||||||
|
.aw .aw-knopf[aria-expanded="true"] > .bi { transform: rotate(180deg); }
|
||||||
|
/* Als Text im Fliesstext (Automatik-Editor): der Knopf traegt dort die
|
||||||
|
Gestaltung von .feld-als-text, nur der Pfeil kommt dazu. */
|
||||||
|
.aw .aw-knopf.feld-als-text { display: inline-flex; width: auto; max-width: 100%; }
|
||||||
|
.input-group > .aw { flex: 1 1 auto; width: 1%; }
|
||||||
|
.input-group > .aw .aw-knopf { border-radius: 0; height: 100%; }
|
||||||
|
.input-group > .aw:last-child .aw-knopf { border-top-right-radius: var(--ton-radius-klein, .5rem); border-bottom-right-radius: var(--ton-radius-klein, .5rem); }
|
||||||
|
|
||||||
|
/* Ein <select> mit schwebender Beschriftung (.form-floating): Bootstrap
|
||||||
|
laesst die Beschriftung nur dann nach oben ruecken, wenn es das Feld
|
||||||
|
selbst kennt. Unser Knopf ist keins - also steht sie hier fest oben, was
|
||||||
|
bei einer Auswahl ohnehin immer gilt: sie hat immer einen Wert. */
|
||||||
|
.form-floating > .aw .aw-knopf { height: calc(3.5rem + 2px); padding: 1.625rem .75rem .625rem; }
|
||||||
|
.form-floating > .aw ~ label { opacity: .65; transform: scale(.85) translateY(-.5rem) translateX(.15rem); }
|
||||||
|
|
||||||
|
.aw-panel {
|
||||||
|
position: fixed; z-index: 2000; display: flex; flex-direction: column; gap: .35rem;
|
||||||
|
padding: .35rem; border-radius: var(--ton-radius-klein, .5rem);
|
||||||
|
border: 1px solid var(--ton-linie-satt, rgba(138, 148, 158, .38));
|
||||||
|
background: var(--ton-flaeche, #23282c); box-shadow: 0 .8rem 2rem rgba(0, 0, 0, .5);
|
||||||
|
animation: aw-auf .15s cubic-bezier(.2, .7, .2, 1);
|
||||||
|
}
|
||||||
|
@keyframes aw-auf { from { opacity: 0; transform: translateY(-4px); } }
|
||||||
|
.aw-panel .aw-suche {
|
||||||
|
flex: none; padding: .35rem .55rem; border: 0; border-radius: .4rem;
|
||||||
|
background: var(--ton-flaeche-2, #2b3237); color: var(--ton-schrift, #e8ede9); font: inherit;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08);
|
||||||
|
}
|
||||||
|
.aw-panel .aw-suche::placeholder { color: var(--ton-schrift-3, #6f7a81); }
|
||||||
|
.aw-panel .aw-suche:focus { outline: 0; box-shadow: inset 0 0 0 1.5px var(--bs-link-color, #f0b429); }
|
||||||
|
.aw-panel .aw-liste { display: block; min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #3f474e transparent; }
|
||||||
|
.aw-panel .aw-gruppe {
|
||||||
|
padding: .4rem .55rem .15rem; font-size: .7rem; font-weight: 600; letter-spacing: .06em;
|
||||||
|
text-transform: uppercase; color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
}
|
||||||
|
.aw-panel .aw-gruppe[hidden] { display: none; }
|
||||||
|
.aw-panel .aw-eintrag {
|
||||||
|
display: block; box-sizing: border-box; width: 100%; margin: 0; cursor: pointer;
|
||||||
|
padding: .35rem .55rem; border: 0; border-radius: .4rem; background: none; appearance: none;
|
||||||
|
font: inherit; font-size: .9rem; line-height: 1.5; text-align: left;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
}
|
||||||
|
.aw-panel .aw-eintrag[hidden] { display: none; }
|
||||||
|
.aw-panel .aw-eintrag:hover, .aw-panel .aw-eintrag.aktiv { background: var(--ton-flaeche-2, #2b3237); }
|
||||||
|
.aw-panel .aw-eintrag[aria-selected="true"] { color: var(--bs-link-color, #f0b429); font-weight: 600; }
|
||||||
|
.aw-panel .aw-eintrag:disabled { opacity: .45; cursor: default; }
|
||||||
|
|
||||||
/* --- Karten --------------------------------------------------------------
|
/* --- Karten --------------------------------------------------------------
|
||||||
Eine Karte ist eine Flaeche, kein Kasten: ein Ton heller als der Grund, eine
|
Eine Karte ist eine Flaeche, kein Kasten: ein Ton heller als der Grund, eine
|
||||||
Haarlinie als Rand, kein Schlagschatten. Der Schatten trennte frueher, was
|
Haarlinie als Rand, kein Schlagschatten. Der Schatten trennte frueher, was
|
||||||
@@ -1131,30 +1203,17 @@ input[type='range']::-webkit-slider-runnable-track {
|
|||||||
.rf .rf-farbe:focus-visible, .rf .rf-farbe-eigen:focus-within { outline: 2px solid #f7c000; outline-offset: 3px; }
|
.rf .rf-farbe:focus-visible, .rf .rf-farbe-eigen:focus-within { outline: 2px solid #f7c000; outline-offset: 3px; }
|
||||||
.rf .rf-farbe-eigen { background: conic-gradient(#f55, #fd5, #5f7, #5df, #75f, #f5d, #f55); }
|
.rf .rf-farbe-eigen { background: conic-gradient(#f55, #fd5, #5f7, #5df, #75f, #f5d, #f55); }
|
||||||
.rf .rf-farbe-eigen input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
|
.rf .rf-farbe-eigen input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
|
||||||
/* Preset, Effekt: Titel links, Chips oder Picker rechts. Lange Listen
|
/* Preset und Effekt: Titel links, Chips oder die allgemeine Auswahl rechts
|
||||||
klappen als durchsuchbarer Kasten unter der Zeile auf. */
|
(js/solar/auswahl.js) - lange Listen waeren als Chips eine Wand. */
|
||||||
.rf .rf-liste { display: grid; grid-template-columns: 4.3rem minmax(0, 1fr); align-items: start; gap: .4rem .5rem; padding-top: .55rem; border-top: 1px solid rgba(255, 255, 255, .06); }
|
.rf .rf-liste { display: grid; grid-template-columns: 4.3rem minmax(0, 1fr); align-items: center; gap: .4rem .5rem; padding-top: .55rem; border-top: 1px solid rgba(255, 255, 255, .06); }
|
||||||
.rf .rf-liste + .rf-liste { border-top: 0; padding-top: 0; }
|
.rf .rf-liste + .rf-liste { border-top: 0; padding-top: 0; }
|
||||||
.rf .rf-liste > .rf-reihe-titel { padding-top: .35rem; }
|
|
||||||
.rf .rf-liste .rf-chip { padding: .28rem .65rem; font-size: .78rem; }
|
.rf .rf-liste .rf-chip { padding: .28rem .65rem; font-size: .78rem; }
|
||||||
.rf .rf-liste .rf-chip[aria-pressed="true"] { color: #1a1406; }
|
.rf .rf-liste .rf-chip[aria-pressed="true"] { color: #1a1406; }
|
||||||
.rf .rf-picker-knopf { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-width: 0; padding: .38rem .7rem; border-radius: .6rem; background: #22292d; font-size: .82rem; font-weight: 500; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06); }
|
.rf .rf-liste .aw .aw-knopf { background: #22292d; border-color: transparent; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06); font-size: .82rem; font-weight: 500; }
|
||||||
.rf .rf-picker-knopf span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.rf .rf-liste .aw .aw-knopf:hover { background: #283035; }
|
||||||
.rf .rf-picker-knopf .bi { font-size: .75rem; color: var(--ton-schrift-2, #9aa6ad); transition: transform .2s; }
|
.rf .rf-liste .aw .aw-knopf[aria-expanded="true"] { border-color: transparent; box-shadow: inset 0 0 0 1.5px var(--a); }
|
||||||
.rf .rf-picker-knopf:hover { background: #283035; }
|
|
||||||
.rf .rf-picker-knopf[aria-expanded="true"] { box-shadow: inset 0 0 0 1.5px var(--a); }
|
|
||||||
.rf .rf-picker-knopf[aria-expanded="true"] .bi { transform: rotate(180deg); }
|
|
||||||
.rf .rf-picker-knopf:focus-visible { outline: 2px solid var(--a); outline-offset: 1px; }
|
|
||||||
.rf .rf-picker { grid-column: 1 / -1; display: grid; gap: .45rem; padding: .5rem; border-radius: .65rem; background: #22292d; animation: rf-auf .2s cubic-bezier(.2, .7, .2, 1); }
|
|
||||||
.rf .rf-picker[hidden] { display: none; }
|
|
||||||
.rf .rf-suche { all: unset; box-sizing: border-box; width: 100%; padding: .4rem .65rem; border-radius: .5rem; background: var(--ton-flaeche-2, #2b3237); color: var(--ton-schrift, #e8ede9); font-size: .82rem; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08); }
|
|
||||||
.rf .rf-suche::placeholder { color: var(--ton-schrift-3, #6f7a81); }
|
|
||||||
.rf .rf-suche:focus { box-shadow: inset 0 0 0 1.5px var(--a); }
|
|
||||||
.rf .rf-picker-liste { display: flex; flex-wrap: wrap; align-content: flex-start; gap: .3rem; max-height: 11rem; overflow-y: auto; padding-right: .25rem; scrollbar-width: thin; scrollbar-color: #3f474e transparent; }
|
|
||||||
.rf .rf-picker-liste .rf-chip[hidden] { display: none; }
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
.rf .rf-liste { grid-template-columns: 1fr; gap: .3rem; }
|
.rf .rf-liste { grid-template-columns: 1fr; gap: .3rem; }
|
||||||
.rf .rf-liste > .rf-reihe-titel { padding-top: 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Schalter --- */
|
/* --- Schalter --- */
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
* Eine Auswahlliste fuer die ganze Anwendung.
|
||||||
|
*
|
||||||
|
* Die Liste, die der Browser zu einem <select> aufmacht, zeichnet er selbst -
|
||||||
|
* bei Chrome hell auf grau, mitten in einer dunklen Seite. Gestalten laesst
|
||||||
|
* sie sich nicht. Deshalb bekommt hier jedes <select> seinen eigenen Knopf;
|
||||||
|
* das <select> bleibt stehen und haelt weiterhin den Wert, sodass jeder
|
||||||
|
* vorhandene Zuhoerer (onchange, Formulare, dataset) unveraendert
|
||||||
|
* funktioniert. Wer den Wert im Code setzt, ruft auswahlAktualisieren(select)
|
||||||
|
* oder loest "change" aus - beides zieht die Beschriftung nach.
|
||||||
|
*
|
||||||
|
* Die aufgeklappte Liste haengt an <body> und liegt mit position:fixed ueber
|
||||||
|
* der Seite. Das ist der zweite Grund fuer dieses Skript: eine Liste im Fluss
|
||||||
|
* des Modals machte es hoeher, und der ganze Inhalt sprang.
|
||||||
|
*
|
||||||
|
* Neue <select> im Dokument werden von selbst uebernommen (MutationObserver),
|
||||||
|
* auch die, die der Automatik-Editor bei jedem Zeichnen neu baut.
|
||||||
|
*
|
||||||
|
* Ausnahmen: <select multiple>, <select size="2"> 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 <select> um seinen eigenen Knopf ergaenzen. */
|
||||||
|
function auswahlAufsetzen(select) {
|
||||||
|
if (select.dataset.aw || select.multiple || select.size > 1
|
||||||
|
|| select.dataset.auswahl === "aus") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.dataset.aw = "1";
|
||||||
|
|
||||||
|
const huelle = document.createElement("span");
|
||||||
|
huelle.className = "aw";
|
||||||
|
const knopf = document.createElement("button");
|
||||||
|
knopf.type = "button";
|
||||||
|
// Die Klassen des <select> 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 = "<span class='aw-wert'></span><i class='bi bi-chevron-down'></i>";
|
||||||
|
|
||||||
|
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 <select> unterhalb von wurzel uebernehmen. */
|
||||||
|
function auswahlAlle(wurzel) {
|
||||||
|
(wurzel || document).querySelectorAll("select:not([data-aw])").forEach(auswahlAufsetzen);
|
||||||
|
}
|
||||||
|
|
||||||
|
function auswahlSchliessen(zurueck) {
|
||||||
|
if (!auswahlOffen) return;
|
||||||
|
const knopf = auswahlOffen.knopf;
|
||||||
|
auswahlOffen.panel.remove();
|
||||||
|
knopf.setAttribute("aria-expanded", "false");
|
||||||
|
auswahlOffen = null;
|
||||||
|
if (zurueck && document.body.contains(knopf)) knopf.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function auswahlOeffnen(select, knopf) {
|
||||||
|
auswahlSchliessen();
|
||||||
|
// Steckt der Knopf in einem ausgeblendeten Reiter, hat er keine Lage - die
|
||||||
|
// Liste landete sonst in der Ecke oben links.
|
||||||
|
if (!knopf.getBoundingClientRect().width) return;
|
||||||
|
const panel = document.createElement("div");
|
||||||
|
panel.className = "aw-panel";
|
||||||
|
if (select.options.length >= AUSWAHL_SUCHE_AB) {
|
||||||
|
panel.innerHTML = "<input type='search' class='aw-suche' placeholder='Suchen …' aria-label='In der Liste suchen'>";
|
||||||
|
}
|
||||||
|
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 });
|
||||||
|
});
|
||||||
+20
-41
@@ -620,60 +620,39 @@ function rfLampeStarten(karte) {
|
|||||||
eigen.addEventListener("change", () => setzen(eigen.value));
|
eigen.addEventListener("change", () => setzen(eigen.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preset, Effekt und andere Listen: Chips, lange Listen im aufklappbaren
|
// Preset, Effekt und andere Listen: wenige Eintraege als Chips, lange
|
||||||
// Picker mit Suche.
|
// Listen als <select>, aus dem js/solar/auswahl.js die eigene Liste macht.
|
||||||
karte.querySelectorAll(".rf-liste").forEach(liste => {
|
karte.querySelectorAll(".rf-liste").forEach(liste => {
|
||||||
const knopf = liste.querySelector(".rf-picker-knopf");
|
|
||||||
const picker = liste.querySelector(".rf-picker");
|
|
||||||
const suche = liste.querySelector(".rf-suche");
|
|
||||||
const auf = offen => {
|
|
||||||
if (!picker) return;
|
|
||||||
picker.hidden = !offen;
|
|
||||||
knopf.setAttribute("aria-expanded", String(offen));
|
|
||||||
if (offen) {
|
|
||||||
suche.value = "";
|
|
||||||
liste.querySelectorAll("[data-wert]").forEach(k => { k.hidden = false; });
|
|
||||||
const gewaehlt = liste.querySelector("[data-wert][aria-pressed='true']");
|
|
||||||
if (gewaehlt) gewaehlt.scrollIntoView({ block: "nearest" });
|
|
||||||
suche.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (knopf) knopf.addEventListener("click", () => auf(picker.hidden));
|
|
||||||
if (suche) {
|
|
||||||
suche.addEventListener("input", () => {
|
|
||||||
const text = suche.value.trim().toLowerCase();
|
|
||||||
liste.querySelectorAll("[data-wert]").forEach(k => { k.hidden = text !== "" && !k.textContent.toLowerCase().includes(text); });
|
|
||||||
});
|
|
||||||
suche.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "Escape") { e.stopPropagation(); auf(false); knopf.focus(); }
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
const erster = [...liste.querySelectorAll("[data-wert]")].find(k => !k.hidden);
|
|
||||||
if (erster) erster.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
liste.querySelectorAll("[data-wert]").forEach(k => k.addEventListener("click", () => {
|
liste.querySelectorAll("[data-wert]").forEach(k => k.addEventListener("click", () => {
|
||||||
rfListeSetzen(liste, k.dataset.wert);
|
rfListeSetzen(liste, k.dataset.wert);
|
||||||
auf(false);
|
rfListeSenden(liste, k.dataset.wert);
|
||||||
const params = {};
|
|
||||||
params[liste.dataset.param] = k.dataset.wert;
|
|
||||||
rfKommando(Number(liste.dataset.cmd), params, liste);
|
|
||||||
}));
|
}));
|
||||||
|
const auswahl = liste.querySelector("select");
|
||||||
|
if (auswahl) auswahl.addEventListener("change", () => rfListeSenden(liste, auswahl.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (karte.dataset.wled) rfWledLesen(karte);
|
if (karte.dataset.wled) rfWledLesen(karte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Den gewaehlten Eintrag an das Geraet schicken. */
|
||||||
|
function rfListeSenden(liste, wert) {
|
||||||
|
const params = {};
|
||||||
|
params[liste.dataset.param] = wert;
|
||||||
|
rfKommando(Number(liste.dataset.cmd), params, liste);
|
||||||
|
}
|
||||||
|
|
||||||
/** Einen Eintrag einer Liste als gewaehlt zeigen; null = keiner. */
|
/** Einen Eintrag einer Liste als gewaehlt zeigen; null = keiner. */
|
||||||
function rfListeSetzen(liste, wert) {
|
function rfListeSetzen(liste, wert) {
|
||||||
let name = null;
|
|
||||||
liste.querySelectorAll("[data-wert]").forEach(k => {
|
liste.querySelectorAll("[data-wert]").forEach(k => {
|
||||||
const ja = wert !== null && k.dataset.wert === String(wert);
|
k.setAttribute("aria-pressed", String(wert !== null && k.dataset.wert === String(wert)));
|
||||||
k.setAttribute("aria-pressed", String(ja));
|
|
||||||
if (ja) name = k.textContent;
|
|
||||||
});
|
});
|
||||||
const anzeige = liste.querySelector("[data-rf-picker-wert]");
|
const auswahl = liste.querySelector("select");
|
||||||
if (anzeige) anzeige.textContent = name === null ? "–" : name;
|
if (auswahl && wert !== null) {
|
||||||
|
auswahl.value = String(wert);
|
||||||
|
// Die Beschriftung des eigenen Knopfes haengt am <select>, nicht am DOM
|
||||||
|
// darunter - sie muss also mitgezogen werden.
|
||||||
|
if (typeof auswahlAktualisieren === "function") auswahlAktualisieren(auswahl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
<script src="js/leaflet.js"></script>
|
<script src="js/leaflet.js"></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<script src="js/solar/common.js?v=<?= filemtime("js/solar/common.js") ?>"></script>
|
<script src="js/solar/common.js?v=<?= filemtime("js/solar/common.js") ?>"></script>
|
||||||
|
<script src="js/solar/auswahl.js?v=<?= filemtime("js/solar/auswahl.js") ?>"></script>
|
||||||
<?php foreach ($page["scripts"] as $script): ?>
|
<?php foreach ($page["scripts"] as $script): ?>
|
||||||
<script src="<?= $script ?>?v=<?= filemtime($script) ?>"></script>
|
<script src="<?= $script ?>?v=<?= filemtime($script) ?>"></script>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -508,8 +508,7 @@ function zeichneLicht($d)
|
|||||||
// Presets, Effekte und andere Listen. Wenige Eintraege stehen als Chips
|
// Presets, Effekte und andere Listen. Wenige Eintraege stehen als Chips
|
||||||
// da; lange Listen (WLED hat ueber 180 Effekte) bekommen einen Knopf mit
|
// da; lange Listen (WLED hat ueber 180 Effekte) bekommen einen Knopf mit
|
||||||
// dem aktuellen Eintrag, der eine durchsuchbare Liste aufklappt. Die
|
// dem aktuellen Eintrag, der eine durchsuchbare Liste aufklappt. Die
|
||||||
// native Auswahlliste zeichnet der Browser hell auf hell und passt nicht
|
// Welcher Eintrag gerade gilt, liest
|
||||||
// in das dunkle Fenster. Welcher Eintrag gerade gilt, liest
|
|
||||||
// js/solar/raumfenster.js bei WLED live aus (room.php?action=wled);
|
// js/solar/raumfenster.js bei WLED live aus (room.php?action=wled);
|
||||||
// data-feld ist der Parametername, "effect" oder "preset".
|
// data-feld ist der Parametername, "effect" oder "preset".
|
||||||
foreach ($listen as $c) {
|
foreach ($listen as $c) {
|
||||||
@@ -525,13 +524,16 @@ function zeichneLicht($d)
|
|||||||
if (count($p["options"]) <= 16) {
|
if (count($p["options"]) <= 16) {
|
||||||
$html .= $kopf . "<div class='rf-chips'>" . $knoepfe . "</div></div>";
|
$html .= $kopf . "<div class='rf-chips'>" . $knoepfe . "</div></div>";
|
||||||
} else {
|
} else {
|
||||||
$html .= $kopf
|
// Ueber 180 WLED-Effekte waeren als Chips eine Wand. Aus dem
|
||||||
. "<button type='button' class='rf-picker-knopf' aria-expanded='false'>"
|
// <select> macht js/solar/auswahl.js die eigene Liste mit Suchfeld,
|
||||||
. "<span data-rf-picker-wert>–</span><i class='bi bi-chevron-down'></i></button>"
|
// und die liegt ueber dem Fenster, statt es hoeher zu machen.
|
||||||
. "<div class='rf-picker' hidden>"
|
$html .= $kopf . "<select class='form-select form-select-sm'"
|
||||||
. "<input type='search' class='rf-suche' placeholder='" . htmlspecialchars($c["name"], ENT_QUOTES) . " suchen …'"
|
. " aria-label='" . htmlspecialchars($c["name"], ENT_QUOTES) . "'>";
|
||||||
. " aria-label='" . htmlspecialchars($c["name"], ENT_QUOTES) . " suchen'>"
|
foreach ($p["options"] as $o) {
|
||||||
. "<div class='rf-picker-liste'>" . $knoepfe . "</div></div></div>";
|
$html .= "<option value='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'>"
|
||||||
|
. htmlspecialchars($o["label"]) . "</option>";
|
||||||
|
}
|
||||||
|
$html .= "</select></div>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $html . "</div></div>";
|
return $html . "</div></div>";
|
||||||
|
|||||||
Reference in New Issue
Block a user