Files
Wegwichtel/public/js/legal-ui.js
T
Florian Zumpe 5c4d20bd1d
Sonarqube Scanner / Prepare environment (push) Successful in 19s
Sonarqube Scanner / Build and test (push) Successful in 43s
Sonarqube Scanner / SonarQube analysis (push) Failing after 2m28s
Sonarqube Scanner / Deploy to Server (push) Successful in 16s
fix sonar findings and translation structure
2026-10-01 16:31:46 +02:00

564 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
globalThis.Wegwichtel = globalThis.Wegwichtel || {};
(function (ns) {
'use strict';
const DROPDOWN_ANIMATION_MS = 180;
const pageTitles = Object.freeze({
get privacy() { return ns.I18n.t("info.privacy"); },
get imprint() { return ns.I18n.t("info.imprint"); },
get technology() { return ns.I18n.t("info.storage"); }
});
function menuEntries() { return [
['privacy', ns.I18n.t("info.privacy")],
['imprint', ns.I18n.t("info.imprint")],
['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(() => 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) 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));