Revert "Updated imprint and legal documents"
Sonarqube Scanner / Build and analyze (push) Canceled after 0s
Sonarqube Scanner / Build and analyze (push) Canceled after 0s
This reverts commit fd16c66142.
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
# Wegwichtel Next
|
||||
|
||||
Neuaufbau des früheren schiffsbezogenen Ansagesystems als mobile Lernweg-Anwendung für Schulen. GPX-Strecken werden serverseitig verwaltet; POIs können einzeln verwaltete Bilder mit Beschreibungen und eine separat verwaltete Audioansage enthalten. Der Client schlägt anhand der aktuellen Position nahe Routen vor und aktiviert POIs während einer Wanderung über einen GPS-Watcher. Beim Erreichen eines POIs wird die Audioansage nur vorgeladen und erst nach einer bewussten Bedienung abgespielt.
|
||||
|
||||
## Architektur
|
||||
|
||||
- **Server:** Node.js 22.13+, Express 5, `node:sqlite`, Multer, `fast-xml-parser`, `file-type` und `@file-type/av`
|
||||
- **Client:** klassisches JavaScript, jQuery 4.0.0 und jQuery UI 1.14.2 mit lokalem Base-Theme
|
||||
- **Medien:** getrennte Verzeichnisse für aktive und zum Löschen markierte Strecken
|
||||
- **Löschmodell:** Soft Delete in SQLite plus atomisches Verschieben des gesamten Streckenordners in `storage/trash/routes`
|
||||
- **Wiederherstellung:** `POST /api/routes/:id/restore` verschiebt die Daten zurück und korrigiert alle gespeicherten Pfade
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
@@ -14,7 +18,10 @@ npm install
|
||||
npm run init-db
|
||||
npm start
|
||||
```
|
||||
|
||||
Danach lauscht Wegwichtel ausschließlich auf dem lokalen Socket `127.0.0.1:47145` und ist unter `http://127.0.0.1:47145` erreichbar. Die offiziellen, exakt versionierten Distributionsdateien sind bereits unter `public/vendor` enthalten. `npm install` installiert zusätzlich die Pakete `jquery@4.0.0` und `jquery-ui@1.14.2` und synchronisiert daraus JavaScript, Base-Theme, Themebilder und Lizenzdateien erneut in das Vendor-Verzeichnis. Der Browser lädt keine Bibliotheken von einem CDN.
|
||||
|
||||
|
||||
## Server-Socket
|
||||
|
||||
Die Standardwerte stehen in `.env.example` und werden auch verwendet, wenn keine `.env` vorhanden ist:
|
||||
@@ -33,7 +40,9 @@ PORT=47145
|
||||
"socket": "127.0.0.1:47145"
|
||||
}
|
||||
```
|
||||
|
||||
## Initialisierungsablauf des Clients
|
||||
|
||||
1. statischer Initialisierungsbildschirm erscheint ohne Bibliotheksabhängigkeit,
|
||||
2. lokale jQuery-Datei wird geladen und auf Version 4.0.0 geprüft,
|
||||
3. lokales jQuery UI 1.14.2 samt Base-Theme wird geladen und über `jQuery.ui.version` geprüft,
|
||||
@@ -42,7 +51,9 @@ PORT=47145
|
||||
6. der Client lädt die Routenliste,
|
||||
7. anschließend wird die Position ermittelt; daraus entstehen nahe Empfehlungen, während die Gesamtliste vollständig erhalten bleibt,
|
||||
8. die responsive Routenansicht wird freigeschaltet.
|
||||
|
||||
Scheitert ein Schritt, bleibt der Initialisierungsbildschirm mit einer konkreten Fehlermeldung und einem Wiederholungsbutton sichtbar.
|
||||
|
||||
## Lokale UI-Abhängigkeiten
|
||||
|
||||
Im Vendor-Verzeichnis liegen die zur Laufzeit verwendeten Dateien vollständig lokal:
|
||||
@@ -54,14 +65,20 @@ public/vendor/jquery-ui/jquery-ui-1.14.2.min.js
|
||||
public/vendor/jquery-ui/jquery-ui-1.14.2.min.css
|
||||
public/vendor/jquery-ui/images/*.png
|
||||
```
|
||||
|
||||
Die Anwendung verwendet aus jQuery UI insbesondere das Widget **Button**. Das offizielle vollständige jQuery-UI-Bundle bleibt lokal verfügbar, damit weitere aktuelle Widgets ohne erneuten CDN-Bezug ergänzt werden können. Die frühere jQuery-Mobile-Seitensteuerung wurde durch eine eigene, History-API-basierte Navigation ersetzt.
|
||||
|
||||
## API-Dokumentation
|
||||
|
||||
Die vollständige REST-API ist einschließlich aller Pfad-, Query-, Formular- und Datei-Parameter sowie ausführlicher `curl`-Beispiele in [`docs/REST-API.md`](docs/REST-API.md) dokumentiert. Die README beschreibt bewusst nur Installation, Architektur und Bedienverhalten.
|
||||
|
||||
|
||||
## Python-Werkzeuge
|
||||
Unter [`tools/python/`](tools/python/README.md) liegen eigenständige Python-3-Skripte zum Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audiodateien. Die Werkzeuge selbst verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden interaktiv abgefragt. Antwortet ein vorgeschalteter Passwortschutz mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den Request.
|
||||
Fehler werden mit einer verständlichen Zusammenfassung und dem vollständigen Python-Stacktrace auf `stderr` ausgegeben.
|
||||
|
||||
Unter [`tools/python/`](tools/python/README.md) liegen eigenständige Python-3-Skripte zum Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audiodateien. Die Werkzeuge selbst verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden interaktiv abgefragt. Antwortet ein vorgeschalteter Passwortschutz mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den Request. Fehler werden mit einer verständlichen Zusammenfassung und dem vollständigen Python-Stacktrace auf `stderr` ausgegeben.
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```text
|
||||
public/ mobiler Client
|
||||
index.html reduzierte semantische Seitenstruktur
|
||||
@@ -79,22 +96,28 @@ examples/ Beispiel-GPX
|
||||
test/ automatisierte Tests
|
||||
tools/python/ interaktive API-Werkzeuge ohne Node.js-Abhängigkeit
|
||||
```
|
||||
|
||||
## Automatisierte Tests
|
||||
|
||||
Die REST-Integrationstests starten die Anwendung über `npm start` mit isolierten temporären Daten- und Speicherverzeichnissen. Der eigentliche Node.js-Prozess lauscht innerhalb des Testaufbaus auf `127.0.0.1:47146`. Davor läuft ein ausschließlich für die Tests bestimmter Reverse-Proxy auf `http://127.0.0.1:47145`, sodass dieser Loopback-Endpunkt das einzige Ziel der API-Aufrufe bleibt. Der Produktivserver wird von der Testsuite nicht angesprochen.
|
||||
Der lokale Proxy lässt `GET`, `HEAD` und `OPTIONS` ohne Anmeldung passieren. Für `POST`, `PUT`, `PATCH` und `DELETE` antwortet er zunächst mit `401 Unauthorized` und `WWW-Authenticate: Basic`, sofern kein gültiger Authorization-Header vorhanden ist. Nach erfolgreicher Basic-Auth-Prüfung entfernt der Proxy den Header wieder und leitet den Request an den lokal per NPM gestarteten Server weiter.
|
||||
Damit wird ein möglicher Nginx-Passwortschutz für schreibende Operationen realistisch simuliert, ohne Authentifizierung in die Anwendung selbst einzubauen.
|
||||
|
||||
Der lokale Proxy lässt `GET`, `HEAD` und `OPTIONS` ohne Anmeldung passieren. Für `POST`, `PUT`, `PATCH` und `DELETE` antwortet er zunächst mit `401 Unauthorized` und `WWW-Authenticate: Basic`, sofern kein gültiger Authorization-Header vorhanden ist. Nach erfolgreicher Basic-Auth-Prüfung entfernt der Proxy den Header wieder und leitet den Request an den lokal per NPM gestarteten Server weiter. Damit wird ein möglicher Nginx-Passwortschutz für schreibende Operationen realistisch simuliert, ohne Authentifizierung in die Anwendung selbst einzubauen.
|
||||
|
||||
Der Integrationstest prüft dabei ausdrücklich, dass lesende Requests nicht präventiv authentifiziert werden und dass für `POST`, `PUT` und `DELETE` jeweils zuerst ein `401` und anschließend ein erfolgreicher authentifizierter Request erfolgt. Zusätzlich bleibt ein kleiner, isolierter HTTP-Mock auf einem zufälligen Loopback-Port bestehen, der die 401-Wiederholungslogik der Python-Request-Schicht unabhängig vom vollständigen REST-Test prüft.
|
||||
|
||||
Die kombinierte Coverage wird mit folgenden Befehlen erzeugt:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
Der Python-Coverage-Runner verwendet ausschließlich die Python-Standardbibliothek. Er führt die Unit-Tests mit `unittest` unter dem eingebauten `trace`-Modul aus und erzeugt daraus ohne `pip` oder zusätzliche Pakete einen Cobertura-kompatiblen Bericht. Ausführungsfehler werden mit vollständigem Python-Stacktrace auf `stderr` ausgegeben.
|
||||
|
||||
Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/coverage.xml` für die Python-Werkzeuge. Die zugehörigen SonarQube-Pfade stehen in `sonar-project.properties`. Die Coverage-Skripte erzwingen selbst keinen Mindestwert; die Bewertung und die Hinweise zu nicht abgedeckten Stellen bleiben Aufgabe des SonarQube Quality Gates.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Medienpfade werden relativ zu `storage/` gespeichert. So bleibt das Projekt verschiebbar.
|
||||
- Öffentliche GPX-, Bild- und Audiodateien werden ausschließlich über die zugehörigen `/api/routes/...`-Ressourcen ausgeliefert. Interne Speicherpfade und ein separates `/media`-URL-Schema werden nicht veröffentlicht.
|
||||
- Dateiverschiebung und Datenbankänderung sind durch eine kompensierende Rückverschiebung gekoppelt: Schlägt die SQL-Transaktion fehl, wird das Verzeichnis an seinen vorherigen Ort zurückbewegt.
|
||||
@@ -106,7 +129,9 @@ Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/cov
|
||||
- Bei gestarteter Route zeigt ein fixierter Footer vorrangig Entfernung und Richtung zum nächstgelegenen, noch nicht erreichten POI. Der GPX-Fortschritt wird parallel entlang der Trackpunkte nachgeführt; sind alle POIs erreicht, übernimmt wieder der nächste GPX-Trackpunkt beziehungsweise das Routenziel. Ohne Kompassdaten wird die Peilung mit Norden oben dargestellt.
|
||||
- Auf iPhone und iPad wird die Freigabe der Geräteausrichtung beim Start der Route innerhalb der Benutzeraktion angefordert.
|
||||
- Für die Geolokalisierung sollte der Client lokal über `http://127.0.0.1:47145` oder online ausschließlich über HTTPS geöffnet werden. Das betrifft ebenso den Gerätekompass.
|
||||
|
||||
## Nächste Ausbaustufen
|
||||
|
||||
- Administrationsoberfläche zum Zeichnen/Importieren von Routen und Platzieren der POIs
|
||||
- Benutzer-, Schul- und Projektzuordnung mit Rollenmodell
|
||||
- Offline-Cache/PWA für Wanderungen ohne Mobilfunkempfang
|
||||
@@ -114,44 +139,62 @@ Dabei entstehen `coverage/js/lcov.info` für JavaScript und `coverage/python/cov
|
||||
- Administrationsoberfläche für die bereits vorhandenen Einzelendpunkte zur Medienverwaltung
|
||||
- Hintergrundbereinigung des Papierkorbs nach einer konfigurierbaren Aufbewahrungsfrist
|
||||
- Integritätsjournal für Dateiverschiebungen und Wiederherstellungen
|
||||
|
||||
## Routenauswahl
|
||||
|
||||
Die Startseite zeigt standortbasierte Empfehlungen und darunter alle aktiven Routen. Die Gesamtliste bleibt auch bei verweigertem oder nicht verfügbarem GPS auswählbar und kann nach Name, Schule oder Beschreibung durchsucht werden.
|
||||
|
||||
## Mobile Bedienung und Routennavigation
|
||||
|
||||
Das HTML verwendet nur IDs, die von den Clientmodulen tatsächlich angesprochen werden. Die wenigen Klassen bilden wiederverwendete Layoutbausteine wie Seiten, Inhalte, Hinweise, Routenlisten und Routendaten ab. Automatisierte Tests gleichen diese Verwendungen ab.
|
||||
|
||||
Alle selbst implementierten Schaltaktionen reagieren primär auf `pointerup`; dadurch funktionieren dieselben Handler mit Touchscreen, Eingabestift und Maus. Die Diashow kann zusätzlich horizontal gewischt werden. Für die Tastatur werden Enter und Leertaste separat behandelt.
|
||||
|
||||
Beim Start der Route wird der aktuellen Position nächstgelegene GPX-Trackpunkt gesucht und der Fortschritt anschließend nur vorwärts entlang der Punktfolge nachgeführt. Für die sichtbare Navigation haben jedoch POIs Vorrang: Aus allen noch nicht automatisch aktivierten Stationen wird bei jeder Positionsänderung die räumlich nächstgelegene bestimmt. Der Footer zeigt:
|
||||
|
||||
- Name und auf ganze Meter gerundete Luftlinienentfernung zum nächsten POI,
|
||||
- einen frei rotierenden SVG-Pfeil relativ zur Geräte- oder Bewegungsrichtung,
|
||||
- andernfalls die absolute Peilung bei Norden oben,
|
||||
- nach dem Aktivieren unmittelbar den nächsten noch offenen POI,
|
||||
- und erst nach Erreichen aller POIs wieder den nächsten GPX-Trackpunkt beziehungsweise das Routenziel.
|
||||
|
||||
In der Stationsliste erhält jeder POI rechts einen kleinen Richtungspfeil und seine aktuelle Entfernung in Metern. Diese Werte werden mit jeder GPS- oder Kompassaktualisierung neu berechnet. Die Reihenfolge der Stationsliste bleibt dabei unverändert.
|
||||
|
||||
|
||||
## Routensteuerung und GPS-Tracking
|
||||
|
||||
Die Routenansicht besitzt drei nebeneinanderliegende, semantische Schaltflächen mit lokalen SVG-Symbolen:
|
||||
|
||||
- **Route starten beziehungsweise fortsetzen:** aktiviert `navigator.geolocation.watchPosition()` und die Geräteausrichtung.
|
||||
- **Route pausieren:** beendet den aktiven GPS-Watcher und den Kompass-Listener, pausiert eine laufende Audioansage und behält Routenfortschritt, Audioquelle, Abspielposition und bereits ausgelöste POIs bei.
|
||||
- **Route beenden:** beendet alle Sensor-Listener, hält die Audioansage an, entlädt ihre Quelldatei, setzt den Fortschritt zurück und kehrt zur Routenauswahl zurück.
|
||||
|
||||
Die aktuell geöffnete beziehungsweise automatisch aktivierte Station wird in der Stationsliste sichtbar hervorgehoben und mit `aria-current="step"` semantisch gekennzeichnet. Beim Eintritt in den Auslöseradius wird ihre Audiodatei mit `preload="auto"` vorbereitet, aber niemals automatisch gestartet. Die Wiedergabe beginnt ausschließlich über die nativen Audio-Bedienelemente.
|
||||
|
||||
Die Vibration API ist in `public/js/vibration.js` gekapselt. Die aus dem historischen Projekt übernommenen Konstanten `MICRO`, `MINI`, `SHORT`, `LONG`, `ALARM` und `POI` stehen unter `Wegwichtel.Vibration.Patterns` zur Verfügung. Für eine neu aktivierte Station wird `ACTIVE_POI` mit dem Muster `[500, 300, 500]` verwendet. Nicht unterstützende Browser ignorieren den Aufruf ohne Fehler; Pause und Beenden brechen ein laufendes Muster ab.
|
||||
|
||||
Beispiel für einen Aufruf mit Konstante:
|
||||
|
||||
```javascript
|
||||
Wegwichtel.Vibration.start(Wegwichtel.Vibration.Patterns.ACTIVE_POI);
|
||||
```
|
||||
|
||||
Während die Route läuft, werden Positionsänderungen fortlaufend verarbeitet. Eine Browser-Webanwendung ist jedoch kein nativer Hintergrunddienst: Betriebssystem und Browser können die Aktualisierung bei gesperrtem Bildschirm, Energiesparmodus oder im Hintergrund drosseln beziehungsweise anhalten.
|
||||
|
||||
Die Symbole liegen unter `public/images/icons/`. Sie befinden sich innerhalb nativer `<button>`-Elemente. Die Bilder selbst sind dekorativ (`alt=""`); der zugängliche Name wird über `aria-label` und `title` am jeweiligen Button bereitgestellt.
|
||||
|
||||
## Datenschutz, Impressum und lokale Browserdaten
|
||||
|
||||
Die Oberfläche enthält rechts oben einen global verfügbaren Informationsbutton als gefüllten Kreis mit kleinem `i`. Er klappt per Touch, Maus oder Tastatur ein animiertes Dropdown-Menü auf. Die Menüeinträge öffnen ein scrollbares, überblendetes Popup mit Datenschutzerklärung, Impressum sowie einer technischen Erläuterung zu Cookies, Local Storage und den lokal eingebundenen JavaScript-Bibliotheken.
|
||||
Die enthaltenen Rechtstexte sind mit den öffentlich angegebenen Angaben der Staatlichen Grundschule Herpf vorbefüllt. Im Impressum sind Schule, Anschrift, Telefon, Fax, E-Mail, Schulleitung, Schulträger und inhaltlich verantwortliche Person eingetragen. In der Datenschutzerklärung sind die Schul- und Datenschutzkontakte sowie die Thüringer Datenschutzaufsicht ergänzt. Vor einer öffentlichen Bereitstellung müssen weiterhin Hostinganbieter, konkrete Serverprotokollierung, Log-Speicherdauer und die endgültige Zuständigkeit für die konkrete Wegwichtel-Instanz geprüft werden. Eine Checkliste steht in [`docs/PUBLICATION-CHECKLIST.md`](docs/PUBLICATION-CHECKLIST.md).
|
||||
Die Oberfläche enthält rechts oben einen global verfügbaren Informationsbutton. Er öffnet ein touch- und tastaturbedienbares Overlay mit Datenschutzerklärung, Impressum sowie einer technischen Erläuterung zu Cookies, Local Storage und den lokal eingebundenen JavaScript-Bibliotheken.
|
||||
|
||||
Die enthaltenen Rechtstexte sind technische Vorlagen. Vor einer öffentlichen Bereitstellung müssen insbesondere Betreiber, ladungsfähige Anschrift, Kontakt, Hostinganbieter, Protokollierungsdauer, Datenschutzkontakt und zuständige Aufsicht ergänzt und rechtlich geprüft werden. Eine Checkliste steht in [`docs/PUBLICATION-CHECKLIST.md`](docs/PUBLICATION-CHECKLIST.md).
|
||||
|
||||
Wegwichtel setzt selbst keine Cookies und bindet keine Analyse-, Werbe- oder Trackingdienste ein. Der Startbildschirm weist darauf hin. Die freiwillige Speicherung des Streckenfortschritts wird getrennt gesteuert:
|
||||
|
||||
- `wegwichtel.privacy.v1` speichert die gewählte Einstellung,
|
||||
- `wegwichtel.route-progress.v1` enthält bei aktivierter Funktion ein JSON-Objekt mit aktueller Route, Routenstatus, besuchten und angesehenen Stationen, abgespielten Audioeinträgen und Zeitstempeln,
|
||||
- Streckendaten mit einer letzten Aktualisierung von mindestens 24 Stunden werden beim nächsten Start schrittweise entfernt,
|
||||
- beim Ausschalten der Funktion wird der gesamte Streckenfortschritt sofort aus dem Local Storage gelöscht,
|
||||
- ohne aktivierte Speicherung bleiben alle Routen- und Stationsdaten ausschließlich im Arbeitsspeicher des geöffneten Browserfensters.
|
||||
|
||||
Der Initialisierungsbildschirm bleibt nach erfolgreichem Laden sichtbar. Erst eine bewusste Pointer- oder Tastaturaktion öffnet die eigentliche Anwendung. Das Informationsfenster und die Datenschutzeinstellungen können bereits auf dem Startbildschirm verwendet werden, ohne die Anwendung weiterzuschalten.
|
||||
|
||||
+63
-83
@@ -19,6 +19,7 @@ body {
|
||||
color: var(--text);
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
@@ -39,6 +40,7 @@ button,
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#app-shell,
|
||||
.app-page {
|
||||
min-height: 100dvh;
|
||||
@@ -47,6 +49,7 @@ button,
|
||||
.app-page {
|
||||
background: var(--page-background);
|
||||
}
|
||||
|
||||
#app-shell.has-navigation .app-page {
|
||||
padding-bottom: calc(var(--footer-height) + env(safe-area-inset-bottom));
|
||||
}
|
||||
@@ -64,6 +67,7 @@ button,
|
||||
color: #fff;
|
||||
box-shadow: 0 .15rem .5rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.app-page > header.has-back-button {
|
||||
grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
|
||||
gap: .5rem;
|
||||
@@ -84,6 +88,7 @@ button,
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-page > header .back-button.ui-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -103,6 +108,7 @@ button,
|
||||
height: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: min(48rem, 100%);
|
||||
box-sizing: border-box;
|
||||
@@ -117,6 +123,7 @@ button,
|
||||
border-radius: .25rem;
|
||||
background: var(--notice);
|
||||
}
|
||||
|
||||
#routes-page .content > section + section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.25rem;
|
||||
@@ -140,6 +147,7 @@ button,
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: .35rem;
|
||||
}
|
||||
|
||||
#route-search {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
@@ -162,6 +170,7 @@ button,
|
||||
min-height: 3rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#all-routes-summary {
|
||||
margin: .75rem 0;
|
||||
color: var(--muted);
|
||||
@@ -180,6 +189,7 @@ button,
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.route-list button {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@@ -204,6 +214,7 @@ button,
|
||||
.route-list button > small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.route-list button > small {
|
||||
margin-top: .25rem;
|
||||
font-weight: 600;
|
||||
@@ -219,10 +230,12 @@ button,
|
||||
cursor: progress;
|
||||
opacity: .65;
|
||||
}
|
||||
|
||||
#poi-list button {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#poi-list button > strong,
|
||||
#poi-list button > span:not(.poi-proximity) {
|
||||
grid-column: 1;
|
||||
@@ -247,12 +260,14 @@ button,
|
||||
transition: transform 180ms linear;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.poi-proximity small {
|
||||
color: var(--text);
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#poi-list button[aria-current="step"] {
|
||||
border-color: var(--primary);
|
||||
background: #e7f1e9;
|
||||
@@ -273,6 +288,7 @@ button,
|
||||
background: var(--surface);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#route-page dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -302,6 +318,7 @@ button,
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#route-page fieldset {
|
||||
display: grid;
|
||||
padding: 0;
|
||||
@@ -323,6 +340,7 @@ button,
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#route-page fieldset button.ui-button:focus-visible {
|
||||
outline: .2rem solid var(--focus);
|
||||
outline-offset: .15rem;
|
||||
@@ -339,6 +357,7 @@ button,
|
||||
height: 2rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#route-page fieldset > legend,
|
||||
#slideshow > fieldset > legend {
|
||||
position: absolute;
|
||||
@@ -365,6 +384,7 @@ button,
|
||||
background: #9b2c2c;
|
||||
border-color: #701f1f;
|
||||
}
|
||||
|
||||
#slideshow {
|
||||
overflow: hidden;
|
||||
border-radius: .75rem;
|
||||
@@ -385,6 +405,7 @@ button,
|
||||
object-fit: contain;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
#slideshow > fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
|
||||
@@ -396,6 +417,7 @@ button,
|
||||
margin: 0;
|
||||
padding: .65rem;
|
||||
}
|
||||
|
||||
#slideshow > fieldset > button.ui-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -412,6 +434,7 @@ button,
|
||||
height: 1.2rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#slideshow > fieldset > div {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
@@ -431,6 +454,7 @@ button,
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
#slide-position {
|
||||
display: block;
|
||||
margin-top: .15rem;
|
||||
@@ -449,6 +473,7 @@ button,
|
||||
#poi-audio[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#navigation-footer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
@@ -468,6 +493,7 @@ button,
|
||||
box-shadow: 0 -.25rem .8rem rgba(0, 0, 0, .2);
|
||||
backdrop-filter: blur(.4rem);
|
||||
}
|
||||
|
||||
#navigation-arrow {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
@@ -490,6 +516,7 @@ button,
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#navigation-distance {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.1;
|
||||
@@ -508,6 +535,7 @@ button,
|
||||
background: var(--primary-dark);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#pause-route:not(:disabled):hover,
|
||||
#pause-route:not(:disabled):focus {
|
||||
background: #704a00;
|
||||
@@ -538,37 +566,27 @@ button,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: .01ms !important;
|
||||
transition-duration: .01ms !important;
|
||||
}
|
||||
}
|
||||
.info-control {
|
||||
|
||||
.info-button {
|
||||
position: fixed;
|
||||
top: calc(env(safe-area-inset-top) + .45rem);
|
||||
right: max(.65rem, env(safe-area-inset-right));
|
||||
z-index: 10001;
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: .45rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.info-control > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: var(--icon-button-size);
|
||||
height: var(--icon-button-size);
|
||||
padding: 0;
|
||||
border: 2px solid rgba(255, 255, 255, .95);
|
||||
border: 2px solid rgba(255, 255, 255, .9);
|
||||
border-radius: 50%;
|
||||
background: var(--primary-dark);
|
||||
color: #fff;
|
||||
@@ -578,7 +596,6 @@ button,
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
transition: background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.info-button:focus-visible {
|
||||
@@ -586,60 +603,11 @@ button,
|
||||
outline-offset: .15rem;
|
||||
}
|
||||
|
||||
.info-button[aria-expanded="true"] {
|
||||
background: var(--primary);
|
||||
box-shadow: 0 .22rem .7rem rgba(0, 0, 0, .34);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.info-dropdown {
|
||||
position: absolute;
|
||||
top: calc(var(--icon-button-size) + .45rem);
|
||||
right: 0;
|
||||
display: grid;
|
||||
width: min(18rem, calc(100vw - 1.25rem - env(safe-area-inset-left) - env(safe-area-inset-right)));
|
||||
box-sizing: border-box;
|
||||
gap: .35rem;
|
||||
padding: .45rem;
|
||||
border: 1px solid rgba(49, 93, 58, .24);
|
||||
border-radius: .8rem;
|
||||
background: rgba(255, 255, 255, .98);
|
||||
box-shadow: 0 .45rem 1.4rem rgba(0, 0, 0, .24);
|
||||
opacity: 0;
|
||||
transform: translateY(-.35rem) scale(.98);
|
||||
transform-origin: top right;
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.info-dropdown.is-open {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.info-dropdown button {
|
||||
min-height: 3rem;
|
||||
padding: .75rem .85rem;
|
||||
border: 1px solid #aebcaf;
|
||||
border-radius: .6rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.info-dropdown button:focus-visible {
|
||||
outline: .2rem solid var(--focus);
|
||||
outline-offset: .12rem;
|
||||
}
|
||||
|
||||
.app-page > header:not(.has-back-button) h1 {
|
||||
padding-right: calc(var(--icon-button-size) + 1rem);
|
||||
padding-left: calc(var(--icon-button-size) + 1rem);
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
@@ -667,6 +635,7 @@ button,
|
||||
color: var(--muted);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.switch-row > input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -685,6 +654,7 @@ button,
|
||||
background: #d9ded9;
|
||||
transition: background-color 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.switch-track::after {
|
||||
position: absolute;
|
||||
top: .15rem;
|
||||
@@ -706,6 +676,7 @@ button,
|
||||
.switch-row > input:checked + .switch-track::after {
|
||||
transform: translateX(1.45rem);
|
||||
}
|
||||
|
||||
.switch-row > input:focus-visible + .switch-track {
|
||||
outline: .2rem solid var(--focus);
|
||||
outline-offset: .15rem;
|
||||
@@ -719,6 +690,7 @@ button,
|
||||
.compact-switch {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.info-dialog {
|
||||
width: min(48rem, calc(100% - 1.5rem));
|
||||
max-width: 48rem;
|
||||
@@ -738,6 +710,7 @@ button,
|
||||
background: rgba(12, 20, 14, .72);
|
||||
backdrop-filter: blur(.15rem);
|
||||
}
|
||||
|
||||
.info-dialog-shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
@@ -746,22 +719,24 @@ button,
|
||||
|
||||
.info-dialog-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) var(--icon-button-size);
|
||||
grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: max(.4rem, env(safe-area-inset-top)) .5rem .4rem;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.info-dialog-header h2 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 1.15rem;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-dialog-header button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -777,6 +752,7 @@ button,
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.info-dialog-content {
|
||||
min-height: 0;
|
||||
padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1rem + env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
|
||||
@@ -787,6 +763,25 @@ button,
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.info-menu {
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.info-menu button {
|
||||
min-height: 3.5rem;
|
||||
padding: .85rem 1rem;
|
||||
border: 1px solid #aebcaf;
|
||||
border-radius: .65rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
box-shadow: 0 .12rem .35rem rgba(0, 0, 0, .08);
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.info-page {
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -800,6 +795,7 @@ button,
|
||||
.info-page h3:first-of-type {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.info-page code {
|
||||
padding: .08rem .25rem;
|
||||
border-radius: .2rem;
|
||||
@@ -814,22 +810,6 @@ button,
|
||||
background: var(--notice);
|
||||
}
|
||||
|
||||
@keyframes info-dialog-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(.45rem) scale(.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog[open] {
|
||||
animation: info-dialog-in 160ms ease-out;
|
||||
}
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
.info-dialog {
|
||||
width: 100%;
|
||||
|
||||
+70
-60
@@ -10,16 +10,10 @@
|
||||
<link rel="stylesheet" href="css/app.css?v=0.12.14">
|
||||
</head>
|
||||
<body>
|
||||
<div class="info-control">
|
||||
<button id="info-button" class="info-button" type="button" aria-label="Informationsmenü öffnen" title="Informationen" aria-haspopup="true" aria-expanded="false" aria-controls="info-dropdown">
|
||||
<span aria-hidden="true">i</span>
|
||||
</button>
|
||||
<nav id="info-dropdown" class="info-dropdown" aria-label="Rechtliche und technische Informationen" hidden>
|
||||
<button type="button" data-info-target="privacy">Datenschutzerklärung</button>
|
||||
<button type="button" data-info-target="imprint">Impressum</button>
|
||||
<button type="button" data-info-target="technology">Cookies und verwendete JavaScripts</button>
|
||||
</nav>
|
||||
</div>
|
||||
<button id="info-button" class="info-button" type="button" aria-label="Informationen, Datenschutz und Impressum öffnen" title="Informationen">
|
||||
<span aria-hidden="true">i</span>
|
||||
</button>
|
||||
|
||||
<div id="bootstrap-screen" aria-live="polite" aria-busy="true">
|
||||
<section>
|
||||
<span aria-hidden="true">🧭</span>
|
||||
@@ -28,6 +22,7 @@
|
||||
|
||||
<div class="privacy-summary">
|
||||
<p><strong>Cookies:</strong> Diese Anwendung setzt derzeit keine Cookies und verwendet weder Werbung noch Tracking.</p>
|
||||
|
||||
<label class="switch-row switch-disabled">
|
||||
<span>
|
||||
<strong>Optionale Cookies</strong>
|
||||
@@ -36,6 +31,7 @@
|
||||
<input class="cookie-toggle" type="checkbox" role="switch" disabled>
|
||||
<span class="switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
<label class="switch-row">
|
||||
<span>
|
||||
<strong>Streckenfortschritt lokal speichern</strong>
|
||||
@@ -45,9 +41,11 @@
|
||||
<span class="switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p>
|
||||
|
||||
<progress id="bootstrap-progress" max="6" value="0"></progress>
|
||||
|
||||
<ol id="bootstrap-steps">
|
||||
<li data-step="jquery">jQuery laden</li>
|
||||
<li data-step="jquery-ui">jQuery UI laden</li>
|
||||
@@ -56,12 +54,14 @@
|
||||
<li data-step="routes">Routen laden</li>
|
||||
<li data-step="location">Standort ermitteln</li>
|
||||
</ol>
|
||||
|
||||
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit. Tippe oder klicke auf diese Startseite, um die Anwendung zu öffnen.</p>
|
||||
<p id="bootstrap-error" hidden></p>
|
||||
<button id="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
|
||||
<button id="bootstrap-enter" type="button" hidden>Anwendung öffnen</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<main id="app-shell" hidden>
|
||||
<section id="routes-page" class="app-page">
|
||||
<header>
|
||||
@@ -77,6 +77,7 @@
|
||||
<li><p>Routen werden geladen …</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Alle Routen</h2>
|
||||
<label for="route-search">Routen durchsuchen</label>
|
||||
@@ -90,6 +91,7 @@
|
||||
placeholder="Name, Schule oder Beschreibung">
|
||||
<button id="clear-route-search" type="button">Suche leeren</button>
|
||||
</div>
|
||||
|
||||
<p id="all-routes-summary" aria-live="polite">Routen werden geladen …</p>
|
||||
<ul id="all-route-list" class="route-list" aria-live="polite">
|
||||
<li><p>Routen werden geladen …</p></li>
|
||||
@@ -97,6 +99,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="route-page" class="app-page" hidden>
|
||||
<header class="has-back-button">
|
||||
<button class="back-button" type="button" data-page-target="#routes-page" aria-label="Zurück zur Routenauswahl" title="Zurück zur Routenauswahl">
|
||||
@@ -107,11 +110,13 @@
|
||||
|
||||
<div class="content">
|
||||
<p id="route-description"></p>
|
||||
|
||||
<dl>
|
||||
<div><dt>Länge</dt><dd id="route-distance">–</dd></div>
|
||||
<div><dt>Höhenmeter</dt><dd id="route-elevation">–</dd></div>
|
||||
<div><dt>POIs</dt><dd id="route-poi-count">–</dd></div>
|
||||
</dl>
|
||||
|
||||
<fieldset>
|
||||
<legend>Routensteuerung</legend>
|
||||
<button id="start-route" type="button" aria-label="Route starten" title="Route starten">
|
||||
@@ -125,6 +130,7 @@
|
||||
</button>
|
||||
</fieldset>
|
||||
<p id="tracking-status" class="notice" hidden></p>
|
||||
|
||||
<h2>Stationen</h2>
|
||||
<ul id="poi-list" class="route-list"></ul>
|
||||
</div>
|
||||
@@ -137,11 +143,13 @@
|
||||
</button>
|
||||
<h1 id="poi-title">Station</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div id="slideshow" aria-live="polite">
|
||||
<figure>
|
||||
<img id="slide-image" alt="">
|
||||
</figure>
|
||||
|
||||
<fieldset>
|
||||
<legend>Bildnavigation</legend>
|
||||
<button id="slide-prev" type="button" aria-label="Vorheriges Bild" title="Vorheriges Bild">
|
||||
@@ -156,10 +164,12 @@
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<p id="poi-description"></p>
|
||||
<audio id="poi-audio" controls preload="metadata" aria-label="Audiowiedergabe zur Station" hidden>Audio kann auf diesem Gerät nicht wiedergegeben werden.</audio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer id="navigation-footer" hidden aria-live="polite">
|
||||
<svg id="navigation-arrow" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
|
||||
<path d="M32 4 53 54 32 44 11 54Z"/>
|
||||
@@ -170,42 +180,45 @@
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<dialog id="info-dialog" class="info-dialog" aria-labelledby="info-dialog-title">
|
||||
<div class="info-dialog-shell">
|
||||
<header class="info-dialog-header">
|
||||
<button id="info-back" type="button" aria-label="Zurück zum Informationsmenü" title="Zurück" hidden>‹</button>
|
||||
<h2 id="info-dialog-title">Informationen</h2>
|
||||
<button id="info-close" type="button" aria-label="Informationsfenster schließen" title="Schließen">×</button>
|
||||
</header>
|
||||
|
||||
<div class="info-dialog-content">
|
||||
<nav id="info-menu" class="info-menu" aria-label="Rechtliche und technische Informationen">
|
||||
<button type="button" data-info-target="privacy">Datenschutzerklärung</button>
|
||||
<button type="button" data-info-target="imprint">Impressum</button>
|
||||
<button type="button" data-info-target="technology">Cookies und verwendete JavaScripts</button>
|
||||
</nav>
|
||||
|
||||
<article class="info-page" data-info-page="privacy" hidden>
|
||||
<p class="legal-warning"><strong>Hinweis:</strong> Diese Datenschutzhinweise sind mit den öffentlich angegebenen Kontaktdaten der Staatlichen Grundschule Herpf vorbefüllt. Hostinganbieter, konkrete Serverprotokollierung und Speicherdauer müssen für die tatsächlich betriebene Wegwichtel-Instanz noch technisch geprüft und ergänzt werden.</p>
|
||||
<h3>1. Verantwortliche Stelle und Datenschutzkontakt</h3>
|
||||
<p class="legal-warning"><strong>Vor der öffentlichen Freigabe:</strong> Die gekennzeichneten Angaben müssen durch die verantwortliche Schule, Gemeinde, Initiative oder sonstige Betreiberin ergänzt und rechtlich geprüft werden.</p>
|
||||
|
||||
<h3>1. Verantwortliche Stelle</h3>
|
||||
<p>
|
||||
<strong>Staatliche Grundschule Herpf</strong><br>
|
||||
Zum Eichig 3<br>
|
||||
98617 Meiningen / OT Herpf<br>
|
||||
Telefon: (036943) 63722<br>
|
||||
E-Mail: info@grundschule-herpf.de
|
||||
<strong>[Name der verantwortlichen Organisation ergänzen]</strong><br>
|
||||
[Straße und Hausnummer ergänzen]<br>
|
||||
[Postleitzahl und Ort ergänzen]<br>
|
||||
E-Mail: [E-Mail-Adresse ergänzen]<br>
|
||||
Telefon: [Telefonnummer ergänzen]
|
||||
</p>
|
||||
<p>
|
||||
Datenschutzangaben der Schule:<br>
|
||||
<strong>Staatliches Schulamt Südthüringen</strong><br>
|
||||
Datenschutzbeauftragter<br>
|
||||
Hölderlinstraße 1<br>
|
||||
98527 Suhl<br>
|
||||
Telefon: 03681 / 7341 25<br>
|
||||
Fax: 03681 / 7341 09<br>
|
||||
E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de
|
||||
</p>
|
||||
<p>Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.</p>
|
||||
|
||||
<h3>2. Bereitstellung der Website</h3>
|
||||
<p>Beim Aufruf der Website können durch den Webserver technisch erforderliche Verbindungsdaten verarbeitet werden. Dazu können IP-Adresse, Datum und Uhrzeit der Anfrage, Zeitzonendifferenz zur Greenwich Mean Time, aufgerufene Seite, Zugriffsstatus oder HTTP-Statuscode, übertragene Datenmenge, Referrer, Browser, Betriebssystem, Oberfläche sowie Sprache und Version der Browsersoftware gehören. Diese Verarbeitung dient der technischen Bereitstellung, Stabilität und Sicherheit der Website.</p>
|
||||
<p>Hostinganbieter, konkrete Protokolldaten und Speicherdauer dieser Wegwichtel-Instanz: <strong>[Anbieter, Anschrift, Art der Serverlogs und Speicherdauer ergänzen]</strong></p>
|
||||
<p>Beim Aufruf der Website können durch den Webserver technisch erforderliche Verbindungsdaten verarbeitet werden. Dazu können IP-Adresse, Zeitpunkt, aufgerufene Adresse, übertragene Datenmenge, Browsertyp und Betriebssystem gehören. Welche Protokolldaten tatsächlich gespeichert werden und wie lange dies geschieht, muss für den eingesetzten Hostingdienst ergänzt werden.</p>
|
||||
<p>Hostinganbieter: <strong>[Anbieter, Anschrift und Speicherdauer ergänzen]</strong></p>
|
||||
|
||||
<h3>3. Standortdaten</h3>
|
||||
<p>Die Standortfreigabe ist freiwillig. Sie wird benötigt, um nahe Routen vorzuschlagen und während einer gestarteten Route Entfernungen und Richtungen zu berechnen. Die Anwendung verarbeitet die laufende Position im Browser. Sie legt daraus keinen serverseitigen Standortverlauf an.</p>
|
||||
|
||||
<h3>4. Lokaler Streckenfortschritt</h3>
|
||||
<p>Nach ausdrücklicher Aktivierung speichert die Anwendung ein JSON-Objekt im Local Storage dieses Browsers. Es enthält die aktuell ausgewählte Strecke, besuchte und angesehene Stationen, abgespielte Audioeinträge, den Routenstatus sowie Zeitstempel. Diese Angaben bleiben auf dem verwendeten Gerät und werden nicht mit dem Server synchronisiert.</p>
|
||||
<p>Gespeicherte Streckendaten werden beim nächsten Öffnen entfernt, sobald ihre letzte Aktualisierung mindestens 24 Stunden zurückliegt. Beim Ausschalten der Funktion werden die Streckendaten sofort gelöscht.</p>
|
||||
|
||||
<label class="switch-row compact-switch">
|
||||
<span>
|
||||
<strong>Streckenfortschritt lokal speichern</strong>
|
||||
@@ -214,60 +227,52 @@
|
||||
<input class="progress-storage-toggle" type="checkbox" role="switch">
|
||||
<span class="switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
<h3>5. Cookies und Tracking</h3>
|
||||
<p>Die Anwendung setzt derzeit keine Cookies. Sie verwendet keine Reichweitenmessung, keine personalisierte Werbung und keine Trackingdienste. Eine technisch notwendige Local-Storage-Einstellung merkt sich lediglich, ob die freiwillige Speicherung des Streckenfortschritts aktiviert wurde.</p>
|
||||
|
||||
<h3>6. Inhalte von Schülerinnen und Schülern</h3>
|
||||
<p>Fotos, Texte und Sprachaufnahmen dürfen nur veröffentlicht werden, wenn die erforderlichen Rechte und Einwilligungen vorliegen. Besonders bei erkennbaren Kindern sind die Vorgaben der Schule und die Einwilligungen der Sorgeberechtigten zu beachten.</p>
|
||||
|
||||
<h3>7. Rechte betroffener Personen</h3>
|
||||
<p>Betroffene Personen können je nach rechtlicher Voraussetzung Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch verlangen. Erteilte Einwilligungen können für die Zukunft widerrufen werden. Außerdem besteht das Recht, sich bei einer Datenschutzaufsichtsbehörde über die Verarbeitung personenbezogener Daten zu beschweren.</p>
|
||||
<p>
|
||||
Zuständige Datenschutzaufsicht:<br>
|
||||
<strong>Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)</strong><br>
|
||||
Postfach 90 04 55, 99107 Erfurt<br>
|
||||
Häßlerstraße 8, 99096 Erfurt<br>
|
||||
Telefon: 0361 / 57 311 29 00<br>
|
||||
Telefax: 0361 / 57 311 29 04<br>
|
||||
E-Mail: poststelle@datenschutz.thueringen.de<br>
|
||||
Website: www.tlfdi.de
|
||||
</p>
|
||||
<p>Betroffene Personen können je nach rechtlicher Voraussetzung Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch verlangen. Erteilte Einwilligungen können für die Zukunft widerrufen werden.</p>
|
||||
<p>Datenschutzkontakt: <strong>[Kontakt ergänzen]</strong><br>Zuständige Datenschutzaufsicht: <strong>[Behörde und Kontakt ergänzen]</strong></p>
|
||||
</article>
|
||||
|
||||
<article class="info-page" data-info-page="imprint" hidden>
|
||||
<p class="legal-warning"><strong>Hinweis:</strong> Dieses Impressum ist mit den öffentlich angegebenen Angaben der Staatlichen Grundschule Herpf vorbefüllt. Vor Veröffentlichung der Wegwichtel-Instanz sollte geprüft werden, ob für dieses konkrete Angebot zusätzliche oder abweichende Pflichtangaben erforderlich sind.</p>
|
||||
<p class="legal-warning"><strong>Platzhalter:</strong> Dieses Impressum ist vor der Veröffentlichung mit den Angaben der tatsächlichen Betreiberin zu vervollständigen.</p>
|
||||
|
||||
<h3>Anbieter und verantwortlich für dieses Angebot</h3>
|
||||
<p>
|
||||
<strong>Staatliche Grundschule Herpf</strong><br>
|
||||
Zum Eichig 3<br>
|
||||
98617 Meiningen / OT Herpf
|
||||
<strong>[Vollständiger Name der Schule, Gemeinde, Initiative oder Person]</strong><br>
|
||||
[Rechtsform oder Vertretungsberechtigung ergänzen]<br>
|
||||
[Straße und Hausnummer]<br>
|
||||
[Postleitzahl und Ort]
|
||||
</p>
|
||||
|
||||
<h3>Kontakt</h3>
|
||||
<p>
|
||||
Telefon: (036943) 63722<br>
|
||||
Telefax: (036943) 63843<br>
|
||||
E-Mail: info@grundschule-herpf.de
|
||||
</p>
|
||||
<h3>Vertretungsberechtigte Person</h3>
|
||||
<p>Frau Wichert, Schulleiterin</p>
|
||||
<h3>Schulträger</h3>
|
||||
<p>
|
||||
<strong>Landkreis Schmalkalden-Meiningen</strong><br>
|
||||
Landratsamt<br>
|
||||
Obertshäuser Platz 1<br>
|
||||
98617 Meiningen
|
||||
Telefon: [Telefonnummer ergänzen]<br>
|
||||
E-Mail: [E-Mail-Adresse ergänzen]
|
||||
</p>
|
||||
|
||||
<h3>Inhaltlich verantwortlich</h3>
|
||||
<p>Frau Wichert</p>
|
||||
<p>[Name und ladungsfähige Anschrift ergänzen]</p>
|
||||
|
||||
<h3>Weitere Pflichtangaben</h3>
|
||||
<p>Je nach konkretem Betrieb der Wegwichtel-Instanz können zusätzliche Angaben erforderlich sein, zum Beispiel zur technischen Betreiberin, zum Hosting oder zu ergänzenden Zuständigkeiten. Nicht benötigte Angaben sind zu entfernen.</p>
|
||||
<p>Je nach Betreiberin können zusätzliche Angaben erforderlich sein, zum Beispiel zur Aufsichtsbehörde, Registernummer, Umsatzsteuer-ID oder zuständigen Körperschaft. Nicht benötigte Angaben sind zu entfernen.</p>
|
||||
|
||||
<h3>Haftung für Inhalte und Verweise</h3>
|
||||
<p>Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Für Hinweise auf fehlerhafte, veraltete oder rechtsverletzende Inhalte steht der oben genannte Kontakt zur Verfügung. Für Inhalte externer Seiten sind deren Betreiber verantwortlich.</p>
|
||||
|
||||
<h3>Urheber- und Nutzungsrechte</h3>
|
||||
<p>Soweit nicht anders angegeben, liegen die Rechte bei der Grundschule Herpf. Texte, Bilder, Tonaufnahmen und Routendaten dürfen nur im Rahmen der jeweils eingeräumten Nutzungsrechte verwendet werden. Die Rechte der beteiligten Kinder, Familien, Lehrkräfte und weiteren Urheberinnen und Urheber bleiben unberührt.</p>
|
||||
<p>Texte, Bilder, Tonaufnahmen und Routendaten dürfen nur im Rahmen der jeweils eingeräumten Nutzungsrechte verwendet werden. Die Rechte der beteiligten Kinder, Familien, Lehrkräfte und weiteren Urheberinnen und Urheber bleiben unberührt.</p>
|
||||
</article>
|
||||
|
||||
<article class="info-page" data-info-page="technology" hidden>
|
||||
<h3>Cookies</h3>
|
||||
<p>Wegwichtel setzt derzeit keine Cookies. Die Anwendung besitzt keine Werbe-, Analyse- oder Trackingfunktionen. Der Cookie-Schalter bleibt deshalb ausgeschaltet.</p>
|
||||
|
||||
<label class="switch-row switch-disabled compact-switch">
|
||||
<span>
|
||||
<strong>Optionale Cookies</strong>
|
||||
@@ -276,8 +281,10 @@
|
||||
<input class="cookie-toggle" type="checkbox" role="switch" disabled>
|
||||
<span class="switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
<h3>Local Storage</h3>
|
||||
<p>Der Browser kann auf Wunsch Streckenfortschritte lokal speichern. Verwendet werden die Schlüssel <code>wegwichtel.privacy.v1</code> für die gewählte Einstellung und <code>wegwichtel.route-progress.v1</code> für die freiwilligen Streckendaten. Die Streckendaten werden nach 24 Stunden bereinigt oder beim Ausschalten sofort entfernt.</p>
|
||||
|
||||
<label class="switch-row compact-switch">
|
||||
<span>
|
||||
<strong>Streckenfortschritt lokal speichern</strong>
|
||||
@@ -286,6 +293,7 @@
|
||||
<input class="progress-storage-toggle" type="checkbox" role="switch">
|
||||
<span class="switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
<h3>Lokal eingebundene JavaScripts</h3>
|
||||
<ul>
|
||||
<li><strong>jQuery 4.0.0:</strong> DOM-Zugriffe, Ereignisbehandlung und API-Anfragen.</li>
|
||||
@@ -293,6 +301,7 @@
|
||||
<li><strong>Wegwichtel-Module:</strong> Routenanzeige, Navigation, Standort, Geräteausrichtung, Bilder, Audio, Vibration, Datenschutz und lokaler Fortschritt.</li>
|
||||
</ul>
|
||||
<p>Alle Bibliotheken werden vom eigenen Server ausgeliefert. Die Anwendung lädt keine JavaScripts, Schriftarten oder Analysepixel von externen Anbietern.</p>
|
||||
|
||||
<h3>Browser-Schnittstellen</h3>
|
||||
<ul>
|
||||
<li>Geolocation API für freiwillige Standortdaten,</li>
|
||||
@@ -301,6 +310,7 @@
|
||||
<li>HTML-Audio und HTML-Bilder für lokal bereitgestellte Medien,</li>
|
||||
<li>History API für die Navigation zwischen den Ansichten.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Technische Grenzen</h3>
|
||||
<p>Browser und Betriebssystem können Standort, Kompass, Audio, Vibration oder Local Storage sperren. Die Anwendung bleibt grundsätzlich nutzbar, einzelne Komfortfunktionen können dann jedoch fehlen.</p>
|
||||
</article>
|
||||
|
||||
+34
-143
@@ -1,21 +1,17 @@
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const DROPDOWN_ANIMATION_MS = 180;
|
||||
const dialog = document.getElementById('info-dialog');
|
||||
const title = document.getElementById('info-dialog-title');
|
||||
const menu = document.getElementById('info-menu');
|
||||
const backButton = document.getElementById('info-back');
|
||||
const closeButton = document.getElementById('info-close');
|
||||
const infoButton = document.getElementById('info-button');
|
||||
const pageTitles = Object.freeze({
|
||||
privacy: 'Datenschutzerklärung',
|
||||
imprint: 'Impressum',
|
||||
technology: 'Cookies und Technik'
|
||||
});
|
||||
const menuEntries = Object.freeze([
|
||||
['privacy', 'Datenschutzerklärung'],
|
||||
['imprint', 'Impressum'],
|
||||
['technology', 'Cookies und verwendete JavaScripts']
|
||||
]);
|
||||
|
||||
let elements = null;
|
||||
let dropdownOpen = false;
|
||||
let dropdownCloseTimer = null;
|
||||
|
||||
function ready(callback) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -234,9 +230,6 @@
|
||||
}
|
||||
|
||||
function activate(element, handler) {
|
||||
if (!element || typeof element.addEventListener !== 'function') return;
|
||||
if (element.dataset.wegwichtelLegalUiBound === 'true') return;
|
||||
element.dataset.wegwichtelLegalUiBound = 'true';
|
||||
element.addEventListener('pointerup', event => {
|
||||
if (!isActivation(event)) return;
|
||||
event.preventDefault();
|
||||
@@ -251,75 +244,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setClass(element, className, enabled) {
|
||||
if (!element || !element.classList) return;
|
||||
element.classList.toggle(className, enabled);
|
||||
}
|
||||
|
||||
function runNextFrame(callback) {
|
||||
if (typeof globalThis.requestAnimationFrame === 'function') {
|
||||
globalThis.requestAnimationFrame(callback);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
function schedule(callback, timeout) {
|
||||
if (typeof globalThis.setTimeout === 'function') return globalThis.setTimeout(callback, timeout);
|
||||
callback();
|
||||
return null;
|
||||
}
|
||||
|
||||
function cancelSchedule(timer) {
|
||||
if (timer === null || typeof globalThis.clearTimeout !== 'function') return;
|
||||
globalThis.clearTimeout(timer);
|
||||
}
|
||||
|
||||
function setDropdownOpen(open) {
|
||||
if (!elements?.dropdown || !elements?.infoButton) return;
|
||||
dropdownOpen = open;
|
||||
elements.infoButton.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
elements.infoButton.setAttribute('aria-label', open ? 'Informationsmenü schließen' : 'Informationsmenü öffnen');
|
||||
cancelSchedule(dropdownCloseTimer);
|
||||
dropdownCloseTimer = null;
|
||||
|
||||
if (open) {
|
||||
elements.dropdown.hidden = false;
|
||||
runNextFrame(() => setClass(elements.dropdown, 'is-open', true));
|
||||
return;
|
||||
}
|
||||
|
||||
setClass(elements.dropdown, 'is-open', false);
|
||||
dropdownCloseTimer = schedule(() => {
|
||||
if (dropdownOpen || !elements?.dropdown) return;
|
||||
elements.dropdown.hidden = true;
|
||||
}, DROPDOWN_ANIMATION_MS);
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}
|
||||
|
||||
function elementContains(element, target) {
|
||||
if (!element || !target) return false;
|
||||
if (element === target) return true;
|
||||
return typeof element.contains === 'function' && element.contains(target);
|
||||
}
|
||||
|
||||
function isInsideInfoControl(target) {
|
||||
return elementContains(elements?.infoButton, target) || elementContains(elements?.dropdown, target);
|
||||
}
|
||||
|
||||
function isProgressStorageEnabled() {
|
||||
if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.isProgressStorageEnabled !== 'function') return false;
|
||||
return ns.PrivacyStorage.isProgressStorageEnabled();
|
||||
}
|
||||
|
||||
function setProgressStorageEnabled(enabled) {
|
||||
if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.setProgressStorageEnabled !== 'function') return false;
|
||||
return ns.PrivacyStorage.setProgressStorageEnabled(enabled);
|
||||
}
|
||||
|
||||
function syncStorageToggles() {
|
||||
const enabled = isProgressStorageEnabled();
|
||||
document.querySelectorAll('.progress-storage-toggle').forEach(toggle => {
|
||||
@@ -330,13 +254,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu() {
|
||||
title.textContent = 'Informationen';
|
||||
menu.hidden = false;
|
||||
backButton.hidden = true;
|
||||
document.querySelectorAll('[data-info-page]').forEach(page => {
|
||||
page.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
function showPage(name) {
|
||||
const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
|
||||
const page = elements?.content?.querySelector(`[data-info-page="${pageName}"]`)
|
||||
?? document.querySelector(`[data-info-page="${pageName}"]`);
|
||||
if (!page) return;
|
||||
|
||||
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen';
|
||||
title.textContent = pageTitles[name] ?? 'Informationen';
|
||||
menu.hidden = true;
|
||||
backButton.hidden = false;
|
||||
document.querySelectorAll('[data-info-page]').forEach(item => {
|
||||
item.hidden = item !== page;
|
||||
});
|
||||
@@ -352,26 +287,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(name = 'privacy') {
|
||||
if (!elements?.dialog) return;
|
||||
const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
|
||||
setDropdownOpen(false);
|
||||
showPage(pageName);
|
||||
function openDialog() {
|
||||
if (dialog.open) return;
|
||||
showMenu();
|
||||
syncStorageToggles();
|
||||
if (!elements.dialog.open) {
|
||||
if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal();
|
||||
else elements.dialog.setAttribute('open', '');
|
||||
}
|
||||
focusSafely(elements.closeButton ?? elements.dialog);
|
||||
if (typeof dialog.showModal === 'function') dialog.showModal();
|
||||
else dialog.setAttribute('open', '');
|
||||
closeButton.focus();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
if (elements?.dialog) {
|
||||
if (typeof elements.dialog.close === 'function') elements.dialog.close();
|
||||
else elements.dialog.removeAttribute('open');
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
focusSafely(elements?.infoButton);
|
||||
if (typeof dialog.close === 'function') dialog.close();
|
||||
else dialog.removeAttribute('open');
|
||||
infoButton.focus();
|
||||
}
|
||||
|
||||
function bindStorageToggles() {
|
||||
@@ -388,26 +316,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
function bindDialog() {
|
||||
activate(elements.infoButton, toggleDropdown);
|
||||
activate(elements.closeButton, closeDialog);
|
||||
elements.dropdown.querySelectorAll('[data-info-target]').forEach(button => {
|
||||
activate(button, () => openDialog(button.dataset.infoTarget));
|
||||
});
|
||||
|
||||
if (!document.documentElement.dataset.wegwichtelLegalUiDocumentBound) {
|
||||
document.documentElement.dataset.wegwichtelLegalUiDocumentBound = 'true';
|
||||
document.addEventListener('pointerup', event => {
|
||||
if (!dropdownOpen || isInsideInfoControl(event.target)) return;
|
||||
setDropdownOpen(false);
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
if (!dropdownOpen || event.key !== 'Escape' || elements?.dialog?.open) return;
|
||||
event.preventDefault();
|
||||
setDropdownOpen(false);
|
||||
focusSafely(elements?.infoButton);
|
||||
});
|
||||
}
|
||||
activate(infoButton, openDialog);
|
||||
activate(closeButton, closeDialog);
|
||||
activate(backButton, showMenu);
|
||||
document.querySelectorAll('[data-info-target]').forEach(button => {
|
||||
activate(button, () => showPage(button.dataset.infoTarget));
|
||||
});
|
||||
|
||||
if (elements.dialog.dataset.wegwichtelLegalUiDialogBound !== 'true') {
|
||||
elements.dialog.dataset.wegwichtelLegalUiDialogBound = 'true';
|
||||
@@ -439,28 +353,5 @@
|
||||
});
|
||||
}
|
||||
|
||||
ns.LegalUi = Object.freeze({
|
||||
open: name => ready(() => {
|
||||
if (!elements) init();
|
||||
openDialog(name);
|
||||
}),
|
||||
close: () => ready(() => {
|
||||
if (!elements) init();
|
||||
closeDialog();
|
||||
}),
|
||||
toggleMenu: () => ready(() => {
|
||||
if (!elements) init();
|
||||
toggleDropdown();
|
||||
}),
|
||||
closeMenu: () => ready(() => {
|
||||
if (!elements) init();
|
||||
setDropdownOpen(false);
|
||||
}),
|
||||
syncStorageToggles: () => ready(() => {
|
||||
if (!elements) init();
|
||||
syncStorageToggles();
|
||||
})
|
||||
});
|
||||
|
||||
ready(init);
|
||||
ns.LegalUi = Object.freeze({ open: openDialog, close: closeDialog, syncStorageToggles });
|
||||
}(globalThis.Wegwichtel ??= {}));
|
||||
|
||||
+16
-32
@@ -14,6 +14,7 @@ test('start screen exposes cookie status, optional local progress and deliberate
|
||||
const html = await read('public/index.html');
|
||||
const loader = await read('public/js/bootstrap-loader.js');
|
||||
const storage = await read('public/js/privacy-storage.js');
|
||||
|
||||
assert.match(html, /Diese Anwendung setzt derzeit keine Cookies/);
|
||||
assert.match(html, /class="cookie-toggle"[^>]*disabled/);
|
||||
assert.match(html, /class="progress-storage-toggle"/);
|
||||
@@ -27,15 +28,12 @@ test('start screen exposes cookie status, optional local progress and deliberate
|
||||
assert.match(storage, /cookiesEnabled: \(\) => false/);
|
||||
});
|
||||
|
||||
test('information button toggles dropdown and opens scrollable overlay pages', async () => {
|
||||
test('information button opens scrollable overlay pages for privacy, imprint and technology', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const css = await read('public/css/app.css');
|
||||
const ui = await read('public/js/legal-ui.js');
|
||||
|
||||
assert.match(html, /id="info-button"/);
|
||||
assert.match(html, /aria-haspopup="true"/);
|
||||
assert.match(html, /aria-expanded="false"/);
|
||||
assert.match(html, /id="info-dropdown"/);
|
||||
assert.match(html, /<dialog id="info-dialog"/);
|
||||
assert.match(html, /data-info-target="privacy"/);
|
||||
assert.match(html, /data-info-target="imprint"/);
|
||||
@@ -45,15 +43,14 @@ test('information button toggles dropdown and opens scrollable overlay pages', a
|
||||
assert.match(html, /jQuery 4\.0\.0/);
|
||||
assert.match(css, /\.info-dialog-content[\s\S]*overflow-y:\s*auto/);
|
||||
assert.match(css, /\.info-dialog-content[\s\S]*touch-action:\s*pan-y/);
|
||||
assert.match(css, /\.info-dropdown[\s\S]*transition:\s*opacity 180ms ease, transform 180ms ease/);
|
||||
assert.match(css, /\.info-dropdown\.is-open[\s\S]*pointer-events:\s*auto/);
|
||||
assert.match(ui, /pointerup/);
|
||||
assert.match(ui, /keydown/);
|
||||
assert.doesNotMatch(ui, /addEventListener\(['"]click['"]\)/);
|
||||
assert.doesNotMatch(ui, /addEventListener\(['"]click['"]/);
|
||||
});
|
||||
|
||||
test('application persists current route, viewed stations and played audio only through privacy storage', async () => {
|
||||
const app = await read('public/js/app.js');
|
||||
|
||||
assert.match(app, /PrivacyStorage\.saveRouteState/);
|
||||
assert.match(app, /PrivacyStorage\.activateRoute/);
|
||||
assert.match(app, /state\.viewed\.add\(state\.activePoiId\)/);
|
||||
@@ -63,6 +60,7 @@ test('application persists current route, viewed stations and played audio only
|
||||
assert.match(app, /await initializeLocation\(context\)/);
|
||||
});
|
||||
|
||||
|
||||
class FakeElement extends EventTarget {
|
||||
constructor(id = '') {
|
||||
super();
|
||||
@@ -76,28 +74,12 @@ class FakeElement extends EventTarget {
|
||||
this.dataset = {};
|
||||
this.attributes = new Map();
|
||||
this.focused = false;
|
||||
this.classNames = new Set();
|
||||
this.classList = {
|
||||
toggle: (name, enabled) => {
|
||||
if (enabled) this.classNames.add(name);
|
||||
else this.classNames.delete(name);
|
||||
},
|
||||
contains: name => this.classNames.has(name)
|
||||
};
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.focused = true;
|
||||
}
|
||||
|
||||
contains(target) {
|
||||
return target === this;
|
||||
}
|
||||
|
||||
getAttribute(name) {
|
||||
return this.attributes.get(name) ?? null;
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes.set(name, String(value));
|
||||
if (name === 'open') this.open = true;
|
||||
@@ -133,9 +115,10 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
const elements = new Map([
|
||||
['info-dialog', new FakeElement('info-dialog')],
|
||||
['info-dialog-title', new FakeElement('info-dialog-title')],
|
||||
['info-menu', new FakeElement('info-menu')],
|
||||
['info-back', new FakeElement('info-back')],
|
||||
['info-close', new FakeElement('info-close')],
|
||||
['info-button', new FakeElement('info-button')],
|
||||
['info-dropdown', new FakeElement('info-dropdown')]
|
||||
['info-button', new FakeElement('info-button')]
|
||||
]);
|
||||
const content = new FakeElement();
|
||||
const privacyPage = new FakeElement();
|
||||
@@ -151,12 +134,14 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
const cookieToggles = [new FakeElement()];
|
||||
let enabled = false;
|
||||
const changes = [];
|
||||
|
||||
class TestCustomEvent extends Event {
|
||||
constructor(type, options = {}) {
|
||||
super(type);
|
||||
this.detail = options.detail;
|
||||
}
|
||||
}
|
||||
|
||||
const document = new EventTarget();
|
||||
document.getElementById = id => elements.get(id);
|
||||
document.querySelectorAll = selector => {
|
||||
@@ -172,6 +157,7 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
return match ? pages[match[1]] : null;
|
||||
};
|
||||
document.addEventListener('wegwichtel:progress-storage-changed', event => changes.push(event.detail.enabled));
|
||||
|
||||
const context = {
|
||||
Wegwichtel: {
|
||||
PrivacyStorage: {
|
||||
@@ -191,16 +177,12 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
|
||||
elements.get('info-button').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('info-dialog').open, false);
|
||||
assert.equal(elements.get('info-dropdown').hidden, false);
|
||||
assert.equal(elements.get('info-dropdown').classList.contains('is-open'), true);
|
||||
assert.equal(elements.get('info-button').getAttribute('aria-expanded'), 'true');
|
||||
assert.equal(elements.get('info-dialog').open, true);
|
||||
assert.equal(elements.get('info-menu').hidden, false);
|
||||
|
||||
targetButtons[0].dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('info-dialog').open, true);
|
||||
assert.equal(elements.get('info-dropdown').hidden, true);
|
||||
assert.equal(elements.get('info-button').getAttribute('aria-expanded'), 'false');
|
||||
assert.equal(privacyPage.hidden, false);
|
||||
assert.equal(elements.get('info-back').hidden, false);
|
||||
assert.equal(elements.get('info-dialog-title').textContent, 'Datenschutzerklärung');
|
||||
|
||||
progressToggles[0].checked = true;
|
||||
@@ -209,6 +191,8 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
||||
assert.equal(progressToggles[1].checked, true);
|
||||
assert.equal(cookieToggles[0].checked, false);
|
||||
|
||||
elements.get('info-back').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('info-menu').hidden, false);
|
||||
elements.get('info-close').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('info-dialog').open, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user