globalThis.Wegwichtel = globalThis.Wegwichtel || {}; (function (ns) { 'use strict'; const DROPDOWN_ANIMATION_MS = 180; const pageTitles = Object.freeze({ privacy: 'Datenschutzerklärung', imprint: 'Impressum', terms: 'Nutzungsvereinbarung', get technology() { return ns.I18n.t("info.storage"); } }); function menuEntries() { return [ ['privacy', ns.I18n.t("info.privacy")], ['imprint', ns.I18n.t("info.imprint")], ['terms', ns.I18n.t("info.terms")], ['technology', ns.I18n.t("info.storage")] ]; } 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) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback, { once: true }); return; } callback(); } function byId(id) { return typeof document.getElementById === 'function' ? document.getElementById(id) : null; } function qs(selector, root = document) { return typeof root?.querySelector === 'function' ? root.querySelector(selector) : null; } function qsa(selector, root = document) { if (typeof root?.querySelectorAll === 'function') return Array.from(root.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]) => assignAttribute(element, name, value)); if (text) element.textContent = text; return element; } 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 (typeof element?.classList?.toggle === 'function') { element.classList.toggle(className, enabled); } } function append(parent, child) { if (!child || typeof parent?.appendChild !== 'function') return false; parent.appendChild(child); return true; } function prependToBody(element) { if (!document.body || !element) return false; if (typeof document.body.prepend === 'function') { document.body.prepend(element); return true; } if (typeof document.body.insertBefore === 'function') { document.body.insertBefore(element, document.body.firstChild ?? null); return true; } 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() { const control = ensureControlContainer(); const infoButton = ensureInfoButton(control); const dropdown = ensureDropdown(control); configureInfoButton(infoButton); configureDropdown(dropdown); return { control, infoButton, dropdown }; } 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': ns.I18n.t("info.open"), title: ns.I18n.t("info.title") }); 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': ns.I18n.t("info.menu-label") }); 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', ns.I18n.t("info.open")); } 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', ns.I18n.t("info.menu-label")); 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); } 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() { 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', ns.I18n.t("info.back")); 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': ns.I18n.t("info.close-label"), title: ns.I18n.t("info.close") }, '×'); append(header, closeButton); return configureCloseButton(closeButton); } function configureCloseButton(closeButton) { setAttribute(closeButton, 'type', 'button'); setAttribute(closeButton, 'aria-label', ns.I18n.t("info.close-label")); 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', {}, ns.I18n.t("info.missing", { value1: pageTitles[name] }))); append(content, page); return page; } function ensureInfoUi() { return { ...ensureInfoControl(), ...ensureInfoDialog() }; } function isActivation(event) { if (!event) return false; if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' '; if (event.type !== 'pointerup') return false; if (event.isPrimary === false) return false; return event.pointerType !== 'mouse' || event.button === 0; } function activate(element, handler) { 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); setAttribute(elements.infoButton, 'aria-expanded', dropdownOpen ? 'true' : 'false'); cancelSchedule(dropdownCloseTimer); dropdownCloseTimer = null; if (dropdownOpen) openDropdown(); else closeDropdown(); } function openDropdown() { setHidden(elements.dropdown, false); runNextFrame(() => runNextFrame(() => { if (dropdownOpen) toggleClass(elements.dropdown, 'is-open', true); })); } function closeDropdown() { toggleClass(elements.dropdown, 'is-open', false); dropdownCloseTimer = schedule(() => { if (!dropdownOpen) setHidden(elements.dropdown, true); }, DROPDOWN_ANIMATION_MS); } function handleInfoButton() { setDropdownOpen(!dropdownOpen); } function elementContains(element, target) { return Boolean(element && target && (element === target || element.contains?.(target))); } function isInsideInfoControl(target) { return elementContains(elements?.infoButton, target) || elementContains(elements?.dropdown, target); } function isProgressStorageEnabled() { return ns?.PrivacyStorage?.isProgressStorageEnabled?.() === true; } function setProgressStorageEnabled(enabled) { return ns?.PrivacyStorage?.setProgressStorageEnabled?.(enabled) === true; } function syncStorageToggles() { const enabled = isProgressStorageEnabled(); 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 = pageNameOrDefault(name); const page = qs(`[data-info-page="${pageName}"]`, elements?.content) || qs(`[data-info-page="${pageName}"]`); if (!page) return; updateDialogTitle(pageName); qsa(selectors.infoPage).forEach(item => setHidden(item, item !== page)); if (elements?.content) elements.content.scrollTop = 0; } function updateDialogTitle(pageName) { if (!elements?.title) return; const isLegalPage = pageName !== 'technology'; if (isLegalPage) delete elements.title.dataset.i18n; else elements.title.dataset.i18n = 'info.storage'; setAttribute(elements.title, 'lang', isLegalPage ? 'de' : ns.I18n.language); elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("info.title"); } function focusSafely(element) { if (typeof element?.focus !== 'function') return; try { element.focus({ preventScroll: true }); } catch { element.focus(); } } function openDialog(name = 'privacy') { if (!elements?.dialog) return; setDropdownOpen(false); setHidden(elements.legacyMenu, true); setHidden(elements.backButton, true); showPage(pageNameOrDefault(name)); syncStorageToggles(); 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() { 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(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() { 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, () => setDropdownOpen(true)); infoTargetButtons().forEach(button => activate(button, () => openDialog(button.dataset?.infoTarget))); bindDocumentEvents(); bindDialogEvents(); } function bindDocumentEvents() { if (documentEventsBound) return; documentEventsBound = true; document.addEventListener('pointerup', closeDropdownOnOutsidePointer); document.addEventListener('keydown', closeDropdownOnEscape); } function closeDropdownOnOutsidePointer(event) { if (dropdownOpen && !isInsideInfoControl(event.target)) setDropdownOpen(false); } 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() { elements = ensureInfoUi(); bindDialog(); bindStorageToggles(); syncStorageToggles(); ns.LegalUi = Object.freeze({ open: openDialog, close: closeDialog, toggleMenu: handleInfoButton, closeMenu: () => setDropdownOpen(false), syncStorageToggles }); } ready(init); }(globalThis.Wegwichtel));