diff --git a/public/css/bootstrap.css b/public/css/bootstrap.css index efa9f9b..a65ba1c 100644 --- a/public/css/bootstrap.css +++ b/public/css/bootstrap.css @@ -1,113 +1,151 @@ -html, -body { - min-height: 100%; - margin: 0; - background: #eef4e9; - color: #1f3022; - font-family: system-ui, sans-serif; +:root { + --bootstrap-bg: #f3f7ee; + --bootstrap-card-bg: #ffffff; + --bootstrap-text: #1c2b20; + --bootstrap-muted: #53605a; + --bootstrap-accent: #315d3a; + --bootstrap-accent-strong: #25462c; + --bootstrap-border: #d6dfd3; + --bootstrap-error: #8b1e1e; + --bootstrap-shadow: 0 1rem 3rem rgb(29 52 36 / 18%); } #bootstrap-screen { position: fixed; inset: 0; - z-index: 9999; + z-index: 1000; display: grid; place-items: center; - box-sizing: border-box; - padding: 1rem; + min-height: 100svh; + padding: max(1rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); + color: var(--bootstrap-text); + background: + radial-gradient(circle at 20% 10%, rgb(135 185 112 / 32%), transparent 34rem), + radial-gradient(circle at 85% 90%, rgb(230 198 116 / 25%), transparent 28rem), + var(--bootstrap-bg); +} + +#bootstrap-screen[hidden] { + display: none; } #bootstrap-screen > section { - width: min(30rem, 100%); - box-sizing: border-box; - padding: 1.5rem; + width: min(100%, 34rem); + padding: clamp(1.25rem, 5vw, 2rem); + border: 1px solid var(--bootstrap-border); + border-radius: 1.5rem; + box-shadow: var(--bootstrap-shadow); + background: var(--bootstrap-card-bg); +} + +#bootstrap-screen [aria-hidden="true"] { + display: inline-block; + margin-bottom: .25rem; + font-size: 2rem; +} + +#bootstrap-screen h1 { + margin: 0 0 .5rem; + font-size: clamp(1.8rem, 8vw, 2.5rem); + line-height: 1.05; +} + +#bootstrap-screen p { + margin: .65rem 0; + color: var(--bootstrap-muted); +} + +#bootstrap-screen .privacy-summary { + display: grid; + gap: .75rem; + margin: 1rem 0; + padding: .85rem; + border: 1px solid var(--bootstrap-border); border-radius: 1rem; - background: #fff; - box-shadow: 0 .5rem 2rem rgba(0, 0, 0, .15); - text-align: center; + background: #f9fbf6; } -#bootstrap-screen > section > span { - font-size: 4rem; +#bootstrap-screen .privacy-summary > p { + margin: 0; } -#bootstrap-progress { +#bootstrap-screen .privacy-summary > .switch-row.switch-disabled { + display: none; +} + +#bootstrap-screen progress { width: 100%; - height: 1.2rem; + height: .9rem; + overflow: hidden; + border: 0; + border-radius: 999px; + background: #dce7d8; +} + +#bootstrap-screen progress::-webkit-progress-bar { + border-radius: 999px; + background: #dce7d8; +} + +#bootstrap-screen progress::-webkit-progress-value { + border-radius: 999px; + background: var(--bootstrap-accent); +} + +#bootstrap-screen progress::-moz-progress-bar { + border-radius: 999px; + background: var(--bootstrap-accent); } #bootstrap-steps { - line-height: 1.8; - text-align: left; + display: grid; + gap: .35rem; + margin: 1rem 0; + padding-left: 1.4rem; + color: var(--bootstrap-muted); } -#bootstrap-steps li.done::marker { - content: "✓ "; +#bootstrap-steps li[data-state="done"] { + color: var(--bootstrap-accent-strong); } -#bootstrap-steps li.failed { - color: #8b1e1e; +#bootstrap-steps li[data-state="active"] { + font-weight: 700; + color: var(--bootstrap-text); } #bootstrap-error { - padding: .75rem; - border-radius: .5rem; - background: #fee; - color: #701010; -} - -#bootstrap-screen { - align-items: start; - overflow-y: auto; - padding-top: calc(1rem + env(safe-area-inset-top)); - padding-bottom: calc(1rem + env(safe-area-inset-bottom)); - overscroll-behavior: contain; -} - -#bootstrap-screen.ready { - cursor: pointer; -} - -#bootstrap-screen > section { - margin: auto 0; -} - -.privacy-summary { - display: grid; - gap: .65rem; - margin: 1rem 0; - text-align: left; -} - -.privacy-summary > p { - margin: 0; - padding: .7rem; - border-left: .35rem solid #315d3a; - border-radius: .25rem; - background: #eef4e9; - line-height: 1.4; -} - -.storage-cleanup-status, -.bootstrap-ready { - margin: .85rem 0; - padding: .75rem; - border-radius: .5rem; - background: #fff8d6; - color: #493700; - line-height: 1.4; -} - -.bootstrap-ready { - border: 2px solid #315d3a; - background: #eef4e9; - color: #1f3022; + color: var(--bootstrap-error); font-weight: 700; } -#bootstrap-enter, -#bootstrap-retry { +#bootstrap-retry, +#bootstrap-enter { + width: 100%; min-height: 3rem; - margin: .35rem; - touch-action: manipulation; + margin-top: .75rem; + border: 0; + border-radius: 999px; + color: #ffffff; + background: var(--bootstrap-accent); + font: inherit; + font-weight: 700; + cursor: pointer; +} + +#bootstrap-retry:hover, +#bootstrap-enter:hover, +#bootstrap-retry:focus-visible, +#bootstrap-enter:focus-visible { + background: var(--bootstrap-accent-strong); +} + +.bootstrap-ready { + font-weight: 700; + color: var(--bootstrap-accent-strong); +} + +.storage-cleanup-status { + min-height: 1.25rem; + font-size: .95rem; } diff --git a/public/index.html b/public/index.html index 17e6053..38c007b 100644 --- a/public/index.html +++ b/public/index.html @@ -5,15 +5,21 @@ Wegwichtel - + - + - - +
+ + +
@@ -22,30 +28,26 @@

Cookies: Diese Anwendung setzt derzeit keine Cookies und verwendet weder Werbung noch Tracking.

- -
- -
  1. jQuery laden
  2. jQuery UI laden
  3. @@ -54,14 +56,12 @@
  4. Routen laden
  5. Standort ermitteln
-
-
@@ -77,7 +77,6 @@
  • Routen werden geladen …

  • -

    Alle Routen

    @@ -91,7 +90,6 @@ placeholder="Name, Schule oder Beschreibung"> -

    Routen werden geladen …

    - -
    -
    -

    Informationen

    -
    - - - - @@ -318,8 +308,8 @@
    - - + + diff --git a/public/js/legal-ui.js b/public/js/legal-ui.js index 61bd959..a30eada 100644 --- a/public/js/legal-ui.js +++ b/public/js/legal-ui.js @@ -1,20 +1,32 @@ +globalThis.Wegwichtel = globalThis.Wegwichtel || {}; + (function (ns) { 'use strict'; + const DROPDOWN_ANIMATION_MS = 180; 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'] ]); + const selectors = Object.freeze({ + content: '.info-dialog-content', + cookieToggle: '.cookie-toggle', + infoControl: '.info-control', + infoDropdown: '.info-dropdown', + infoPage: '[data-info-page]', + infoTarget: '[data-info-target]', + progressToggle: '.progress-storage-toggle' + }); let elements = null; let dropdownOpen = false; + let dropdownCloseTimer = null; let documentEventsBound = false; function ready(callback) { @@ -30,54 +42,62 @@ } function qs(selector, root = document) { - return root && typeof root.querySelector === 'function' ? root.querySelector(selector) : null; + return typeof root?.querySelector === 'function' ? root.querySelector(selector) : null; } function qsa(selector, root = document) { - if (root && typeof root.querySelectorAll === 'function') return Array.from(root.querySelectorAll(selector)); + if (typeof root?.querySelectorAll === 'function') return Array.from(root.querySelectorAll(selector)); if (root !== document && typeof document.querySelectorAll === 'function') return Array.from(document.querySelectorAll(selector)); - if (root === document && typeof document.querySelectorAll === 'function') return Array.from(document.querySelectorAll(selector)); return []; } function createElement(tagName, attributes = {}, text = '') { if (typeof document.createElement !== 'function') return null; const element = document.createElement(tagName); - Object.entries(attributes).forEach(([name, value]) => { - if (value === null || value === undefined) return; - if (name === 'className') { - element.className = value; - return; - } - if (name === 'dataset') { - Object.entries(value).forEach(([key, dataValue]) => { - element.dataset[key] = dataValue; - }); - return; - } - element.setAttribute(name, value); - }); + Object.entries(attributes).forEach(([name, value]) => assignAttribute(element, name, value)); if (text) element.textContent = text; return element; } - function setAttribute(element, name, value) { - if (!element || typeof element.setAttribute !== 'function') return; + function assignAttribute(element, name, value) { + if (value === null || value === undefined) return; + if (name === 'className') { + element.className = value; + return; + } + if (name === 'dataset') { + Object.entries(value).forEach(([key, dataValue]) => { + element.dataset[key] = dataValue; + }); + return; + } element.setAttribute(name, value); } + function setAttribute(element, name, value) { + if (typeof element?.setAttribute !== 'function') return; + element.setAttribute(name, value); + } + + function setHidden(element, hidden) { + if (!element) return; + element.hidden = Boolean(hidden); + } + + function setSwitchState(toggle, checked) { + if (!toggle) return; + toggle.checked = Boolean(checked); + setAttribute(toggle, 'aria-checked', checked ? 'true' : 'false'); + } + function toggleClass(element, className, enabled) { - if (!element || !element.classList) return; - if (typeof element.classList.toggle === 'function') { + if (typeof element?.classList?.toggle === 'function') { element.classList.toggle(className, enabled); - return; } - if (enabled && typeof element.classList.add === 'function') element.classList.add(className); - if (!enabled && typeof element.classList.remove === 'function') element.classList.remove(className); } function append(parent, child) { - if (!parent || !child || typeof parent.appendChild !== 'function') return false; + if (!child || typeof parent?.appendChild !== 'function') return false; parent.appendChild(child); return true; } @@ -95,238 +115,293 @@ return append(document.body, element); } + function runNextFrame(callback) { + if (typeof globalThis.requestAnimationFrame === 'function') { + globalThis.requestAnimationFrame(callback); + return; + } + callback(); + } + + function schedule(callback, timeout) { + if (typeof globalThis.setTimeout !== 'function') { + callback(); + return null; + } + return globalThis.setTimeout(callback, timeout); + } + + function cancelSchedule(timer) { + if (timer !== null && typeof globalThis.clearTimeout === 'function') globalThis.clearTimeout(timer); + } + function ensureInfoControl() { - let control = qs('.info-control'); - let infoButton = byId('info-button') || qs('.info-button'); - let dropdown = byId('info-dropdown') || qs('.info-dropdown'); - - if (!control && typeof document.createElement === 'function') { - control = createElement('div', { className: 'info-control' }); - prependToBody(control); - } - - if (!infoButton && control) { - infoButton = createElement('button', { - id: 'info-button', - className: 'info-button', - type: 'button', - 'aria-label': 'Informationsmenü öffnen', - title: 'Informationen' - }); - append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i')); - append(control, infoButton); - } - - if (!dropdown && control) { - dropdown = createElement('nav', { - id: 'info-dropdown', - className: 'info-dropdown', - 'aria-label': 'Rechtliche und technische Informationen' - }); - append(control, dropdown); - } - - if (infoButton) { - setAttribute(infoButton, 'type', 'button'); - setAttribute(infoButton, 'aria-haspopup', 'true'); - setAttribute(infoButton, 'aria-expanded', 'false'); - setAttribute(infoButton, 'aria-controls', 'info-dropdown'); - if (typeof infoButton.getAttribute !== 'function' || !infoButton.getAttribute('aria-label')) { - setAttribute(infoButton, 'aria-label', 'Informationsmenü öffnen'); - } - if (!String(infoButton.textContent ?? '').trim()) { - append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i')); - } - } - - if (dropdown) { - setAttribute(dropdown, 'aria-label', 'Rechtliche und technische Informationen'); - dropdown.hidden = true; - - menuEntries.forEach(([target, label]) => { - let button = qs(`[data-info-target="${target}"]`, dropdown) - || qsa('[data-info-target]', dropdown).find(item => item.dataset?.infoTarget === target); - if (!button) { - button = createElement('button', { - type: 'button', - dataset: { infoTarget: target } - }, label); - append(dropdown, button); - } - if (!button) return; - setAttribute(button, 'type', 'button'); - button.dataset.infoTarget = target; - if (!String(button.textContent ?? '').trim()) button.textContent = label; - }); - } - + const control = ensureControlContainer(); + const infoButton = ensureInfoButton(control); + const dropdown = ensureDropdown(control); + configureInfoButton(infoButton); + configureDropdown(dropdown); return { control, infoButton, dropdown }; } - function ensureInfoPage(content, name) { - let page = qs(`[data-info-page="${name}"]`, content) || qs(`[data-info-page="${name}"]`); - if (!page && content) { - page = createElement('article', { - className: 'info-page', - dataset: { infoPage: name } - }); - append(page, createElement('p', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`)); - append(content, page); + function ensureControlContainer() { + const existing = qs(selectors.infoControl); + if (existing || typeof document.createElement !== 'function') return existing; + const control = createElement('div', { className: 'info-control' }); + prependToBody(control); + return control; + } + + function ensureInfoButton(control) { + const existing = byId('info-button') || qs('.info-button'); + if (existing || !control) return existing; + const infoButton = createElement('button', { + id: 'info-button', + className: 'info-button', + type: 'button', + 'aria-label': 'Informationsmenü öffnen', + title: 'Informationen' + }); + append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i')); + append(control, infoButton); + return infoButton; + } + + function ensureDropdown(control) { + const existing = byId('info-dropdown') || qs(selectors.infoDropdown); + if (existing || !control) return existing; + const dropdown = createElement('nav', { + id: 'info-dropdown', + className: 'info-dropdown', + 'aria-label': 'Rechtliche und technische Informationen' + }); + append(control, dropdown); + return dropdown; + } + + function configureInfoButton(infoButton) { + if (!infoButton) return; + setAttribute(infoButton, 'type', 'button'); + setAttribute(infoButton, 'aria-haspopup', 'true'); + setAttribute(infoButton, 'aria-expanded', 'false'); + setAttribute(infoButton, 'aria-controls', 'info-dropdown'); + setInfoButtonLabel(infoButton); + ensureInfoButtonIcon(infoButton); + } + + function setInfoButtonLabel(infoButton) { + if (typeof infoButton.getAttribute === 'function' && infoButton.getAttribute('aria-label')) return; + setAttribute(infoButton, 'aria-label', 'Informationsmenü öffnen'); + } + + function ensureInfoButtonIcon(infoButton) { + if (String(infoButton.textContent ?? '').trim()) return; + append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i')); + } + + function configureDropdown(dropdown) { + if (!dropdown) return; + setAttribute(dropdown, 'aria-label', 'Rechtliche und technische Informationen'); + setHidden(dropdown, true); + menuEntries.forEach(([target, label]) => ensureMenuButton(dropdown, target, label)); + } + + function ensureMenuButton(dropdown, target, label) { + let button = qs(`[data-info-target="${target}"]`, dropdown); + button = button || qsa(selectors.infoTarget, dropdown).find(item => item.dataset?.infoTarget === target); + if (!button) { + button = createElement('button', { type: 'button', dataset: { infoTarget: target } }, label); + append(dropdown, button); } - if (page) { - page.dataset.infoPage = name; - if (name !== 'privacy') page.hidden = true; - } - return page; + configureMenuButton(button, target, label); + } + + function configureMenuButton(button, target, label) { + if (!button) return; + setAttribute(button, 'type', 'button'); + button.dataset.infoTarget = target; + if (!String(button.textContent ?? '').trim()) button.textContent = label; } function ensureInfoDialog() { - let dialog = byId('info-dialog') || qs('#info-dialog'); - let title = byId('info-dialog-title') || qs('#info-dialog-title', dialog); - let backButton = byId('info-back') || qs('#info-back', dialog); - let legacyMenu = byId('info-menu') || qs('#info-menu', dialog); - let closeButton = byId('info-close') || qs('#info-close', dialog); - let content = qs('.info-dialog-content', dialog) || qs('.info-dialog-content'); - - if (!dialog && typeof document.createElement === 'function') { - dialog = createElement('dialog', { - id: 'info-dialog', - className: 'info-dialog', - 'aria-labelledby': 'info-dialog-title' - }); - append(document.body, dialog); - } - - if (dialog && (!title || !closeButton || !content) && typeof document.createElement === 'function') { - let shell = qs('.info-dialog-shell', dialog); - if (!shell) { - shell = createElement('div', { className: 'info-dialog-shell' }); - append(dialog, shell); - } - - let header = qs('.info-dialog-header', shell); - if (!header) { - header = createElement('header', { className: 'info-dialog-header' }); - append(shell, header); - } - - if (!title) { - title = createElement('h2', { id: 'info-dialog-title' }, pageTitles.privacy); - append(header, title); - } - - if (!closeButton) { - closeButton = createElement('button', { - id: 'info-close', - type: 'button', - 'aria-label': 'Informationsfenster schließen', - title: 'Schließen' - }, '×'); - append(header, closeButton); - } - - if (!content) { - content = createElement('div', { className: 'info-dialog-content' }); - append(shell, content); - } - } - - if (dialog) setAttribute(dialog, 'aria-labelledby', 'info-dialog-title'); - if (backButton) { - setAttribute(backButton, 'type', 'button'); - setAttribute(backButton, 'aria-label', 'Zurück zum Informationsmenü'); - backButton.hidden = true; - } - if (legacyMenu) legacyMenu.hidden = true; - if (closeButton) { - setAttribute(closeButton, 'type', 'button'); - setAttribute(closeButton, 'aria-label', 'Informationsfenster schließen'); - if (!String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×'; - } - - if (content) Object.keys(pageTitles).forEach(name => ensureInfoPage(content, name)); - + const dialog = ensureDialogElement(); + const shell = ensureDialogShell(dialog); + const header = ensureDialogHeader(shell); + const title = ensureDialogTitle(dialog, header); + const backButton = configureBackButton(byId('info-back') || qs('#info-back', dialog)); + const legacyMenu = configureLegacyMenu(byId('info-menu') || qs('#info-menu', dialog)); + const closeButton = ensureCloseButton(dialog, header); + const content = ensureDialogContent(dialog, shell); + configureDialog(dialog); + ensurePages(content); return { dialog, title, backButton, legacyMenu, closeButton, content }; } + function ensureDialogElement() { + const existing = byId('info-dialog') || qs('#info-dialog'); + if (existing || typeof document.createElement !== 'function') return existing; + const dialog = createElement('dialog', { + id: 'info-dialog', + className: 'info-dialog', + 'aria-labelledby': 'info-dialog-title' + }); + append(document.body, dialog); + return dialog; + } + + function ensureDialogShell(dialog) { + if (!dialog) return null; + const existing = qs('.info-dialog-shell', dialog); + if (existing || typeof document.createElement !== 'function') return existing; + const shell = createElement('div', { className: 'info-dialog-shell' }); + append(dialog, shell); + return shell; + } + + function ensureDialogHeader(shell) { + if (!shell) return null; + const existing = qs('.info-dialog-header', shell); + if (existing || typeof document.createElement !== 'function') return existing; + const header = createElement('header', { className: 'info-dialog-header' }); + append(shell, header); + return header; + } + + function ensureDialogTitle(dialog, header) { + const existing = byId('info-dialog-title') || qs('#info-dialog-title', dialog); + if (existing || !header) return existing; + const title = createElement('h2', { id: 'info-dialog-title' }, pageTitles.privacy); + append(header, title); + return title; + } + + function configureBackButton(backButton) { + setAttribute(backButton, 'type', 'button'); + setAttribute(backButton, 'aria-label', 'Zurück zum Informationsmenü'); + setHidden(backButton, true); + return backButton; + } + + function configureLegacyMenu(legacyMenu) { + setHidden(legacyMenu, true); + return legacyMenu; + } + + function ensureCloseButton(dialog, header) { + const existing = byId('info-close') || qs('#info-close', dialog); + if (existing || !header) return configureCloseButton(existing); + const closeButton = createElement('button', { + id: 'info-close', + type: 'button', + 'aria-label': 'Informationsfenster schließen', + title: 'Schließen' + }, '×'); + append(header, closeButton); + return configureCloseButton(closeButton); + } + + function configureCloseButton(closeButton) { + setAttribute(closeButton, 'type', 'button'); + setAttribute(closeButton, 'aria-label', 'Informationsfenster schließen'); + if (closeButton && !String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×'; + return closeButton; + } + + function ensureDialogContent(dialog, shell) { + const existing = qs(selectors.content, dialog) || qs(selectors.content); + if (existing || !shell) return existing; + const content = createElement('div', { className: 'info-dialog-content' }); + append(shell, content); + return content; + } + + function configureDialog(dialog) { + setAttribute(dialog, 'aria-labelledby', 'info-dialog-title'); + } + + function ensurePages(content) { + if (!content) return; + Object.keys(pageTitles).forEach(name => ensureInfoPage(content, name)); + } + + function ensureInfoPage(content, name) { + const existing = qs(`[data-info-page="${name}"]`, content) || qs(`[data-info-page="${name}"]`); + const page = existing || createFallbackPage(content, name); + if (!page) return null; + page.dataset.infoPage = name; + setHidden(page, name !== 'privacy'); + return page; + } + + function createFallbackPage(content, name) { + const page = createElement('article', { className: 'info-page', dataset: { infoPage: name } }); + append(page, createElement('p', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`)); + append(content, page); + return page; + } + function ensureInfoUi() { - return { - ...ensureInfoControl(), - ...ensureInfoDialog() - }; + return { ...ensureInfoControl(), ...ensureInfoDialog() }; } function isActivation(event) { if (!event) return false; - if (event.type === 'pointerup') { - if (event.isPrimary === false) return false; - if (event.pointerType === 'mouse' && event.button !== 0) return false; - return true; - } if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' '; - return false; + if (event.type !== 'pointerup') return false; + if (event.isPrimary === false) return false; + return event.pointerType !== 'mouse' || event.button === 0; } function activate(element, handler) { - if (!element || typeof element.addEventListener !== 'function') return; - if (element.dataset) { - const key = handler.name ? `wegwichtelLegalUiBound${handler.name}` : 'wegwichtelLegalUiBound'; - if (element.dataset[key] === 'true') return; - element.dataset[key] = 'true'; - } - element.addEventListener('pointerup', event => { - if (!isActivation(event)) return; - if (typeof event.preventDefault === 'function') event.preventDefault(); - if (typeof event.stopPropagation === 'function') event.stopPropagation(); - handler(event); - }); - element.addEventListener('keydown', event => { - if (!isActivation(event)) return; - if (typeof event.preventDefault === 'function') event.preventDefault(); - if (typeof event.stopPropagation === 'function') event.stopPropagation(); - handler(event); - }); + if (typeof element?.addEventListener !== 'function') return; + if (!markBound(element, handler)) return; + element.addEventListener('pointerup', event => handleActivation(event, handler)); + element.addEventListener('keydown', event => handleActivation(event, handler)); + } + + function markBound(element, handler) { + if (!element.dataset) return true; + const key = handler.name ? `wegwichtelLegalUiBound${handler.name}` : 'wegwichtelLegalUiBound'; + if (element.dataset[key] === 'true') return false; + element.dataset[key] = 'true'; + return true; + } + + function handleActivation(event, handler) { + if (!isActivation(event)) return; + event.preventDefault?.(); + event.stopPropagation?.(); + handler(event); } function setDropdownOpen(open) { if (!elements?.dropdown || !elements?.infoButton) return; dropdownOpen = Boolean(open); - elements.dropdown.hidden = !dropdownOpen; - toggleClass(elements.dropdown, 'is-open', dropdownOpen); setAttribute(elements.infoButton, 'aria-expanded', dropdownOpen ? 'true' : 'false'); + cancelSchedule(dropdownCloseTimer); + dropdownCloseTimer = null; + if (dropdownOpen) openDropdown(); + else closeDropdown(); } - function hidePages() { - qsa('[data-info-page]').forEach(item => { - item.hidden = true; - }); + function openDropdown() { + setHidden(elements.dropdown, false); + runNextFrame(() => toggleClass(elements.dropdown, 'is-open', true)); } - function openLegacyMenu() { - if (!elements?.dialog) return; - hidePages(); - if (elements.title) elements.title.textContent = 'Informationen'; - if (elements.legacyMenu) elements.legacyMenu.hidden = false; - if (elements.backButton) elements.backButton.hidden = true; - if (!elements.dialog.open) { - if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal(); - else setAttribute(elements.dialog, 'open', ''); - } - focusSafely(elements.closeButton ?? elements.dialog); + function closeDropdown() { + toggleClass(elements.dropdown, 'is-open', false); + dropdownCloseTimer = schedule(() => { + if (!dropdownOpen) setHidden(elements.dropdown, true); + }, DROPDOWN_ANIMATION_MS); } function handleInfoButton() { - if (elements?.dropdown) { - setDropdownOpen(!dropdownOpen); - return; - } - openLegacyMenu(); + setDropdownOpen(!dropdownOpen); } function elementContains(element, target) { - if (!element || !target) return false; - if (element === target) return true; - return typeof element.contains === 'function' && element.contains(target); + return Boolean(element && target && (element === target || element.contains?.(target))); } function isInsideInfoControl(target) { @@ -334,39 +409,38 @@ } function isProgressStorageEnabled() { - if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.isProgressStorageEnabled !== 'function') return false; - return ns.PrivacyStorage.isProgressStorageEnabled(); + return ns?.PrivacyStorage?.isProgressStorageEnabled?.() === true; } function setProgressStorageEnabled(enabled) { - if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.setProgressStorageEnabled !== 'function') return false; - return ns.PrivacyStorage.setProgressStorageEnabled(enabled); + return ns?.PrivacyStorage?.setProgressStorageEnabled?.(enabled) === true; } function syncStorageToggles() { const enabled = isProgressStorageEnabled(); - qsa('.progress-storage-toggle').forEach(toggle => { - toggle.checked = enabled; - }); - qsa('.cookie-toggle').forEach(toggle => { - toggle.checked = false; - }); + qsa(selectors.progressToggle).forEach(toggle => setSwitchState(toggle, enabled)); + qsa(selectors.cookieToggle).forEach(toggle => setSwitchState(toggle, false)); + } + + function pageNameOrDefault(name) { + return Object.hasOwn(pageTitles, name) ? name : 'privacy'; } function showPage(name) { - const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy'; + const pageName = pageNameOrDefault(name); const page = qs(`[data-info-page="${pageName}"]`, elements?.content) || qs(`[data-info-page="${pageName}"]`); if (!page) return; - - if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen'; - qsa('[data-info-page]').forEach(item => { - item.hidden = item !== page; - }); + updateDialogTitle(pageName); + qsa(selectors.infoPage).forEach(item => setHidden(item, item !== page)); if (elements?.content) elements.content.scrollTop = 0; } + function updateDialogTitle(pageName) { + if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen'; + } + function focusSafely(element) { - if (!element || typeof element.focus !== 'function') return; + if (typeof element?.focus !== 'function') return; try { element.focus({ preventScroll: true }); } catch { @@ -376,85 +450,99 @@ function openDialog(name = 'privacy') { if (!elements?.dialog) return; - const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy'; setDropdownOpen(false); - if (elements?.legacyMenu) elements.legacyMenu.hidden = true; - if (elements?.backButton) elements.backButton.hidden = !elements?.legacyMenu; - showPage(pageName); + setHidden(elements.legacyMenu, true); + setHidden(elements.backButton, true); + showPage(pageNameOrDefault(name)); syncStorageToggles(); - if (!elements.dialog.open) { - if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal(); - else setAttribute(elements.dialog, 'open', ''); - } + showDialog(); focusSafely(elements.closeButton ?? elements.dialog); } + function showDialog() { + if (elements.dialog.open) return; + if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal(); + else setAttribute(elements.dialog, 'open', ''); + } + function closeDialog() { - if (elements?.dialog) { - if (typeof elements.dialog.close === 'function') elements.dialog.close(); - else if (typeof elements.dialog.removeAttribute === 'function') elements.dialog.removeAttribute('open'); - } + closeNativeDialog(); setDropdownOpen(false); focusSafely(elements?.infoButton); } + function closeNativeDialog() { + if (!elements?.dialog) return; + if (typeof elements.dialog.close === 'function') elements.dialog.close(); + else elements.dialog.removeAttribute?.('open'); + } + function bindStorageToggles() { - qsa('.progress-storage-toggle').forEach(toggle => { - if (toggle.dataset?.wegwichtelStorageBound === 'true') return; - if (toggle.dataset) toggle.dataset.wegwichtelStorageBound = 'true'; - toggle.addEventListener('change', event => { - const source = event.currentTarget || event.target || toggle; - const enabled = setProgressStorageEnabled(Boolean(source.checked)); - syncStorageToggles(); - document.dispatchEvent(new CustomEvent('wegwichtel:progress-storage-changed', { - detail: { enabled } - })); - }); - }); + qsa(selectors.progressToggle).forEach(bindStorageToggle); + } + + function bindStorageToggle(toggle) { + if (toggle.dataset?.wegwichtelStorageBound === 'true') return; + if (toggle.dataset) toggle.dataset.wegwichtelStorageBound = 'true'; + toggle.addEventListener('change', event => handleStorageToggleChange(event, toggle)); + } + + function handleStorageToggleChange(event, fallbackToggle) { + const source = event.currentTarget || event.target || fallbackToggle; + const enabled = setProgressStorageEnabled(Boolean(source.checked)); + syncStorageToggles(); + document.dispatchEvent(new CustomEvent('wegwichtel:progress-storage-changed', { detail: { enabled } })); } function infoTargetButtons() { - if (elements?.dropdown) return qsa('[data-info-target]', elements.dropdown); - if (elements?.legacyMenu) return qsa('[data-info-target]', elements.legacyMenu); - return qsa('[data-info-target]'); + const root = elements?.dropdown || elements?.legacyMenu || document; + return qsa(selectors.infoTarget, root); } function bindDialog() { activate(elements?.infoButton, handleInfoButton); activate(elements?.closeButton, closeDialog); - activate(elements?.backButton, openLegacyMenu); + activate(elements?.backButton, () => setDropdownOpen(true)); + infoTargetButtons().forEach(button => activate(button, () => openDialog(button.dataset?.infoTarget))); + bindDocumentEvents(); + bindDialogEvents(); + } - infoTargetButtons().forEach(button => { - activate(button, () => openDialog(button.dataset?.infoTarget)); - }); + function bindDocumentEvents() { + if (documentEventsBound) return; + documentEventsBound = true; + document.addEventListener('pointerup', closeDropdownOnOutsidePointer); + document.addEventListener('keydown', closeDropdownOnEscape); + } - if (!documentEventsBound) { - documentEventsBound = 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; - if (typeof event.preventDefault === 'function') event.preventDefault(); - setDropdownOpen(false); - focusSafely(elements?.infoButton); - }); - } + function closeDropdownOnOutsidePointer(event) { + if (dropdownOpen && !isInsideInfoControl(event.target)) setDropdownOpen(false); + } - if (elements?.dialog && elements.dialog.dataset?.wegwichtelLegalUiDialogBound !== 'true') { - if (elements.dialog.dataset) elements.dialog.dataset.wegwichtelLegalUiDialogBound = 'true'; - elements.dialog.addEventListener('pointerup', event => { - if (event.target !== elements.dialog || !isActivation(event)) return; - if (typeof event.preventDefault === 'function') event.preventDefault(); - if (typeof event.stopPropagation === 'function') event.stopPropagation(); - closeDialog(); - }); - elements.dialog.addEventListener('cancel', event => { - if (typeof event.preventDefault === 'function') event.preventDefault(); - closeDialog(); - }); - } + function closeDropdownOnEscape(event) { + if (!dropdownOpen || event.key !== 'Escape' || elements?.dialog?.open) return; + event.preventDefault?.(); + setDropdownOpen(false); + focusSafely(elements?.infoButton); + } + + function bindDialogEvents() { + if (!elements?.dialog || elements.dialog.dataset?.wegwichtelLegalUiDialogBound === 'true') return; + if (elements.dialog.dataset) elements.dialog.dataset.wegwichtelLegalUiDialogBound = 'true'; + elements.dialog.addEventListener('pointerup', closeDialogOnBackdropPointer); + elements.dialog.addEventListener('cancel', closeDialogOnCancel); + } + + function closeDialogOnBackdropPointer(event) { + if (event.target !== elements.dialog || !isActivation(event)) return; + event.preventDefault?.(); + event.stopPropagation?.(); + closeDialog(); + } + + function closeDialogOnCancel(event) { + event.preventDefault?.(); + closeDialog(); } function init() { @@ -462,7 +550,6 @@ bindDialog(); bindStorageToggles(); syncStorageToggles(); - ns.LegalUi = Object.freeze({ open: openDialog, close: closeDialog, @@ -473,4 +560,4 @@ } ready(init); -}(globalThis.Wegwichtel = globalThis.Wegwichtel || {})); \ No newline at end of file +}(globalThis.Wegwichtel)); diff --git a/public/js/privacy-storage.js b/public/js/privacy-storage.js index f441eff..52c70d9 100644 --- a/public/js/privacy-storage.js +++ b/public/js/privacy-storage.js @@ -1,11 +1,11 @@ +globalThis.Wegwichtel = globalThis.Wegwichtel || {}; + (function (ns) { 'use strict'; - const SETTINGS_KEY = 'wegwichtel.privacy.v1'; + const PRIVACY_KEY = 'wegwichtel.privacy.v1'; const PROGRESS_KEY = 'wegwichtel.route-progress.v1'; - const COOKIE_PREFIX = 'wegwichtel_'; const MAX_PROGRESS_AGE_MS = 24 * 60 * 60 * 1000; - const STORE_VERSION = 1; function storage() { try { @@ -15,28 +15,28 @@ } } - function nowIso() { - return new Date().toISOString(); + function now() { + return Date.now(); } function readJson(key, fallback) { - const backend = storage(); - if (!backend) return fallback; - + const store = storage(); + if (!store) return fallback; try { - const raw = backend.getItem(key); - return raw ? JSON.parse(raw) : fallback; + const raw = store.getItem(key); + if (!raw) return fallback; + const value = JSON.parse(raw); + return value && typeof value === 'object' ? value : fallback; } catch { return fallback; } } function writeJson(key, value) { - const backend = storage(); - if (!backend) return false; - + const store = storage(); + if (!store) return false; try { - backend.setItem(key, JSON.stringify(value)); + store.setItem(key, JSON.stringify(value)); return true; } catch { return false; @@ -44,208 +44,164 @@ } function removeItem(key) { - const backend = storage(); - if (!backend) return; - + const store = storage(); + if (!store) return false; try { - backend.removeItem(key); + store.removeItem(key); + return true; } catch { - // Private browsing modes may reject storage access. The app remains usable without persistence. + return false; } } - function defaultSettings() { - return { progressStorage: false, updatedAt: null }; - } - - function settings() { - const value = readJson(SETTINGS_KEY, defaultSettings()); + function normalizeSettings(value) { return { progressStorage: value?.progressStorage === true, - updatedAt: typeof value?.updatedAt === 'string' ? value.updatedAt : null + cookies: false, + updatedAt: Number(value?.updatedAt) || now() }; } - function saveSettings(progressStorage) { - return writeJson(SETTINGS_KEY, { - progressStorage: progressStorage === true, - updatedAt: nowIso() - }); + function readSettings() { + return normalizeSettings(readJson(PRIVACY_KEY, {})); } - function defaultProgressStore() { - return { version: STORE_VERSION, currentRouteId: null, routes: {} }; + function writeSettings(settings) { + return writeJson(PRIVACY_KEY, normalizeSettings({ ...settings, updatedAt: now() })); } - function progressStore() { - const value = readJson(PROGRESS_KEY, defaultProgressStore()); - if (value?.version !== STORE_VERSION || typeof value?.routes !== 'object' || value.routes === null) { - return defaultProgressStore(); - } - - return { - version: STORE_VERSION, - currentRouteId: value.currentRouteId ?? null, - routes: value.routes - }; - } - - function persistProgressStore(value) { - if (!settings().progressStorage) return false; - return writeJson(PROGRESS_KEY, value); - } - - function uniqueIds(values) { - return [...new Set((Array.isArray(values) ? values : []) - .map(value => Number(value)) - .filter(Number.isFinite))]; - } - - function routeRecord(route, previous = {}) { - const timestamp = nowIso(); - return { - routeId: route.id, - routeName: String(route.name ?? previous.routeName ?? ''), - routeState: previous.routeState ?? 'idle', - routeProgressIndex: Number.isInteger(previous.routeProgressIndex) - ? previous.routeProgressIndex - : null, - visitedPoiIds: uniqueIds(previous.visitedPoiIds), - viewedPoiIds: uniqueIds(previous.viewedPoiIds), - playedAudioPoiIds: uniqueIds(previous.playedAudioPoiIds), - startedAt: previous.startedAt ?? timestamp, - updatedAt: timestamp - }; - } - - function updateRoute(routeId, updater) { - if (!settings().progressStorage || routeId == null) return null; - - const store = progressStore(); - const key = String(routeId); - const current = store.routes[key]; - if (!current) return null; - - const updated = updater({ ...current }); - updated.updatedAt = nowIso(); - store.currentRouteId = routeId; - store.routes[key] = updated; - persistProgressStore(store); - return updated; - } - - function markPoi(routeId, field, poiId) { - const normalizedId = Number(poiId); - if (!Number.isFinite(normalizedId)) return null; - - return updateRoute(routeId, record => { - record[field] = uniqueIds([...(record[field] ?? []), normalizedId]); - return record; - }); - } - - function clearApplicationCookies() { - const cookieText = globalThis.document?.cookie; - if (!cookieText) return; - - cookieText.split(';').forEach(item => { - const name = item.split('=', 1)[0].trim(); - if (!name.startsWith(COOKIE_PREFIX)) return; - globalThis.document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`; - }); + function isProgressStorageEnabled() { + return readSettings().progressStorage === true; } function setProgressStorageEnabled(enabled) { - const active = enabled === true; - const saved = saveSettings(active); - if (!active) removeItem(PROGRESS_KEY); - return saved && settings().progressStorage; + const nextValue = Boolean(enabled); + writeSettings({ progressStorage: nextValue, cookies: false }); + if (!nextValue) removeItem(PROGRESS_KEY); + return nextValue; + } + + function cookiesEnabled() { + return false; + } + + function routeId(route) { + const id = route?.id ?? route?.routeId ?? route; + return id === null || id === undefined ? null : String(id); + } + + function asNumberOrNull(value) { + const number = Number(value); + return Number.isFinite(number) ? number : null; + } + + function numericIdSet(values) { + if (!Array.isArray(values)) return []; + return [...new Set(values.map(Number).filter(Number.isFinite))]; + } + + function readProgressStore() { + const data = readJson(PROGRESS_KEY, {}); + const entries = data?.routes && typeof data.routes === 'object' ? data.routes : data; + return entries && typeof entries === 'object' ? entries : {}; + } + + function writeProgressStore(routes) { + return writeJson(PROGRESS_KEY, { + version: 1, + updatedAt: now(), + routes + }); + } + + function normalizeSavedRouteState(value) { + if (!value || typeof value !== 'object') return null; + return { + routeId: value.routeId === null || value.routeId === undefined ? null : String(value.routeId), + routeState: value.routeState === 'running' || value.routeState === 'paused' ? value.routeState : 'idle', + routeProgressIndex: Number.isInteger(value.routeProgressIndex) ? value.routeProgressIndex : null, + visitedPoiIds: numericIdSet(value.visitedPoiIds), + viewedPoiIds: numericIdSet(value.viewedPoiIds), + playedAudioPoiIds: numericIdSet(value.playedAudioPoiIds), + savedAt: Number(value.savedAt) || Number(value.updatedAt) || 0, + updatedAt: Number(value.updatedAt) || Number(value.savedAt) || 0 + }; + } + + function isExpired(value, referenceTime = now()) { + const saved = Number(value?.updatedAt) || Number(value?.savedAt) || 0; + return saved <= 0 || referenceTime - saved > MAX_PROGRESS_AGE_MS; + } + + function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds = [], viewedPoiIds = [], playedAudioPoiIds = []) { + if (!isProgressStorageEnabled()) return false; + const id = routeId(route); + if (!id) return false; + const routes = readProgressStore(); + const timestamp = now(); + routes[id] = { + routeId: id, + routeName: route?.name ?? '', + routeState: routeState === 'running' || routeState === 'paused' ? routeState : 'idle', + routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null, + visitedPoiIds: numericIdSet(visitedPoiIds), + viewedPoiIds: numericIdSet(viewedPoiIds), + playedAudioPoiIds: numericIdSet(playedAudioPoiIds), + savedAt: timestamp, + updatedAt: timestamp + }; + return writeProgressStore(routes); } function activateRoute(route) { - if (!settings().progressStorage || route?.id == null) return null; - - const store = progressStore(); - const key = String(route.id); - const record = routeRecord(route, store.routes[key]); - store.currentRouteId = route.id; - store.routes[key] = record; - persistProgressStore(store); - return record; + if (!isProgressStorageEnabled()) return null; + const id = routeId(route); + if (!id) return null; + const routes = cleanupExpiredEntries().routes; + const saved = normalizeSavedRouteState(routes[id]); + return saved && !isExpired(saved) ? saved : null; } - function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds, viewedPoiIds, playedAudioPoiIds) { - if (route?.id == null) return null; - - return updateRoute(route.id, record => ({ - ...record, - routeName: String(route.name ?? record.routeName ?? ''), - routeState: String(routeState ?? 'idle'), - routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null, - visitedPoiIds: uniqueIds(visitedPoiIds), - viewedPoiIds: uniqueIds(viewedPoiIds), - playedAudioPoiIds: uniqueIds(playedAudioPoiIds) - })); - } - - function loadRoute(routeId) { - if (!settings().progressStorage || routeId == null) return null; - return progressStore().routes[String(routeId)] ?? null; - } - - function nextTurn() { - return new Promise(resolve => globalThis.setTimeout(resolve, 0)); - } - - async function cleanupExpired(onProgress = () => {}) { - if (!settings().progressStorage) { - removeItem(PROGRESS_KEY); - return { removed: 0, total: 0 }; - } - - const store = progressStore(); - const cutoff = Date.now() - MAX_PROGRESS_AGE_MS; - const expiredKeys = Object.entries(store.routes) - .filter(([, route]) => { - const timestamp = Date.parse(route?.updatedAt ?? ''); - return !Number.isFinite(timestamp) || timestamp <= cutoff; - }) - .map(([key]) => key); - - if (!expiredKeys.length) return { removed: 0, total: 0 }; - + function cleanupExpiredEntries(progressCallback) { + const referenceTime = now(); + const routes = readProgressStore(); + const entries = Object.entries(routes); + const kept = {}; let removed = 0; - onProgress(removed, expiredKeys.length); - for (const key of expiredKeys) { - delete store.routes[key]; - if (String(store.currentRouteId) === key) store.currentRouteId = null; - removed += 1; - persistProgressStore(store); - onProgress(removed, expiredKeys.length); - await nextTurn(); - } + entries.forEach(([id, value], index) => { + const normalized = normalizeSavedRouteState(value); + if (normalized && !isExpired(normalized, referenceTime)) kept[id] = normalized; + else removed += 1; + reportCleanupProgress(progressCallback, index + 1, entries.length, removed); + }); - return { removed, total: expiredKeys.length }; + if (removed > 0) writeProgressStore(kept); + return { removed, total: entries.length, routes: kept }; } - clearApplicationCookies(); + function reportCleanupProgress(callback, processed, total, removed) { + if (typeof callback !== 'function') return; + callback({ processed, total, removed }); + } + + async function cleanupExpiredProgress(progressCallback) { + return cleanupExpiredEntries(progressCallback); + } ns.PrivacyStorage = Object.freeze({ - SETTINGS_KEY, + PRIVACY_KEY, PROGRESS_KEY, MAX_PROGRESS_AGE_MS, - cookiesEnabled: () => false, - clearApplicationCookies, - isProgressStorageEnabled: () => settings().progressStorage, - setProgressStorageEnabled, - cleanupExpired, activateRoute, - loadRoute, + cleanExpiredProgress: cleanupExpiredProgress, + cleanupExpiredProgress, + cookiesEnabled: () => false, + getPrivacySettings: readSettings, + isProgressStorageEnabled, + removeExpiredProgress: cleanupExpiredProgress, + removeExpiredRouteProgress: cleanupExpiredProgress, saveRouteState, - markVisited: (routeId, poiId) => markPoi(routeId, 'visitedPoiIds', poiId), - markViewed: (routeId, poiId) => markPoi(routeId, 'viewedPoiIds', poiId), - markAudioPlayed: (routeId, poiId) => markPoi(routeId, 'playedAudioPoiIds', poiId), - clearProgress: () => removeItem(PROGRESS_KEY) + setProgressStorageEnabled }); -}(globalThis.Wegwichtel ??= {})); +}(globalThis.Wegwichtel)); diff --git a/test/legal-ui.test.js b/test/legal-ui.test.js index a532f24..65cfb3c 100644 --- a/test/legal-ui.test.js +++ b/test/legal-ui.test.js @@ -18,6 +18,7 @@ test('start screen exposes cookie status, optional local progress and deliberate assert.match(html, /Diese Anwendung setzt derzeit keine Cookies/); assert.match(html, /class="cookie-toggle"[^>]*disabled/); assert.match(html, /class="progress-storage-toggle"/); + assert.match(html, /role="switch"[^>]*aria-checked="false"/); assert.match(html, /id="storage-cleanup-status"/); assert.match(html, /id="bootstrap-enter"[^>]*hidden/); assert.match(loader, /Entferne alte Streckendaten aus dem Speicher \(\$\{removed\} \/ \$\{total\}\)/); @@ -34,6 +35,7 @@ test('information button opens scrollable overlay pages for privacy, imprint and const ui = await read('public/js/legal-ui.js'); assert.match(html, /id="info-button"/); + assert.match(html, /id="info-dropdown"/); assert.match(html, / elements.get(id); + document.readyState = 'complete'; + document.getElementById = id => elements.get(id) || null; document.querySelectorAll = selector => { if (selector === '.progress-storage-toggle') return progressToggles; if (selector === '.cookie-toggle') return cookieToggles; @@ -177,22 +180,28 @@ 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, true); - assert.equal(elements.get('info-menu').hidden, false); + assert.equal(elements.get('info-dropdown').hidden, false); + assert.equal(elements.get('info-button').attributes.get('aria-expanded'), 'true'); + assert.equal(elements.get('info-dialog').open, 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-menu').hidden, true); + assert.equal(elements.get('info-back').hidden, true); 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; progressToggles[0].dispatchEvent(new Event('change')); assert.deepEqual(changes, [true]); + assert.equal(progressToggles[0].checked, true); assert.equal(progressToggles[1].checked, true); + assert.equal(progressToggles[0].attributes.get('aria-checked'), 'true'); + assert.equal(progressToggles[1].attributes.get('aria-checked'), 'true'); assert.equal(cookieToggles[0].checked, false); + assert.equal(cookieToggles[0].attributes.get('aria-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); });