Files
Wegwichtel/public/js/legal-ui.js
T
MrRobot bba13d6907
Sonarqube Scanner / Build and analyze (push) Canceled after 0s
fix event assignment order
2026-08-29 17:13:33 +02:00

467 lines
15 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.
(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']
]);
let elements = null;
let dropdownOpen = false;
let dropdownCloseTimer = null;
function ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback, { once: true });
return;
}
callback();
}
function createElement(tagName, attributes = {}, text = '') {
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);
});
if (text) element.textContent = text;
return element;
}
function ensureInfoControl() {
let control = document.querySelector('.info-control');
let button = document.getElementById('info-button')
?? document.querySelector('[aria-controls="info-dropdown"], .info-button');
let dropdown = document.getElementById('info-dropdown')
?? document.querySelector('.info-dropdown');
if (!control) {
control = createElement('div', { className: 'info-control' });
document.body.insertBefore(control, document.body.firstChild);
}
if (!button) {
button = createElement('button', {
id: 'info-button',
className: 'info-button',
type: 'button',
'aria-label': 'Informationsmenü öffnen',
title: 'Informationen'
});
const icon = createElement('span', { 'aria-hidden': 'true' }, 'i');
button.appendChild(icon);
}
if (button.parentElement !== control) {
control.insertBefore(button, control.firstChild);
}
button.id = 'info-button';
button.classList.add('info-button');
button.setAttribute('type', 'button');
button.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', 'info-dropdown');
if (!button.getAttribute('aria-label')) button.setAttribute('aria-label', 'Informationsmenü öffnen');
if (!button.textContent.trim()) {
const icon = createElement('span', { 'aria-hidden': 'true' }, 'i');
button.appendChild(icon);
}
if (!dropdown) {
dropdown = createElement('nav', {
id: 'info-dropdown',
className: 'info-dropdown',
'aria-label': 'Rechtliche und technische Informationen'
});
}
if (dropdown.parentElement !== control) {
control.appendChild(dropdown);
}
dropdown.id = 'info-dropdown';
dropdown.classList.add('info-dropdown');
dropdown.setAttribute('aria-label', dropdown.getAttribute('aria-label') ?? 'Rechtliche und technische Informationen');
dropdown.hidden = true;
menuEntries.forEach(([target, label]) => {
let menuButton = dropdown.querySelector(`[data-info-target="${target}"]`);
if (!menuButton) {
menuButton = createElement('button', {
type: 'button',
dataset: { infoTarget: target }
}, label);
dropdown.appendChild(menuButton);
}
menuButton.setAttribute('type', 'button');
menuButton.dataset.infoTarget = target;
if (!menuButton.textContent.trim()) menuButton.textContent = label;
});
return { control, button, dropdown };
}
function moveExistingPagesInto(content, dialog) {
dialog.querySelectorAll('[data-info-page]').forEach(page => {
if (page.parentElement !== content) content.appendChild(page);
});
}
function ensureInfoPage(content, name) {
let page = content.querySelector(`[data-info-page="${name}"]`)
?? document.querySelector(`[data-info-page="${name}"]`);
if (!page) {
page = createElement('article', {
className: 'info-page',
dataset: { infoPage: name }
});
page.appendChild(createElement('p', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`));
content.appendChild(page);
}
page.classList.add('info-page');
page.dataset.infoPage = name;
page.hidden = true;
if (page.parentElement !== content) content.appendChild(page);
return page;
}
function ensureInfoDialog() {
let dialog = document.getElementById('info-dialog');
if (!dialog) {
dialog = createElement('dialog', {
id: 'info-dialog',
className: 'info-dialog',
'aria-labelledby': 'info-dialog-title'
});
document.body.appendChild(dialog);
}
dialog.id = 'info-dialog';
dialog.classList.add('info-dialog');
dialog.setAttribute('aria-labelledby', 'info-dialog-title');
let shell = dialog.querySelector('.info-dialog-shell');
if (!shell) {
shell = createElement('div', { className: 'info-dialog-shell' });
while (dialog.firstChild) shell.appendChild(dialog.firstChild);
dialog.appendChild(shell);
}
let header = shell.querySelector('.info-dialog-header');
if (!header) {
header = createElement('header', { className: 'info-dialog-header' });
shell.insertBefore(header, shell.firstChild);
}
let title = document.getElementById('info-dialog-title')
?? header.querySelector('h1, h2, .info-dialog-title');
if (!title) {
title = createElement('h2', { id: 'info-dialog-title' }, 'Informationen');
header.insertBefore(title, header.firstChild);
}
title.id = 'info-dialog-title';
if (!title.textContent.trim()) title.textContent = 'Informationen';
let closeButton = document.getElementById('info-close')
?? header.querySelector('[data-info-close], .info-close, .info-dialog-close, button[aria-label="Dialog schließen"], button[aria-label="Schließen"]');
if (!closeButton) {
closeButton = createElement('button', {
id: 'info-close',
type: 'button',
'aria-label': 'Informationsfenster schließen',
title: 'Schließen'
}, '×');
header.appendChild(closeButton);
}
closeButton.id = 'info-close';
closeButton.setAttribute('type', 'button');
closeButton.setAttribute('aria-label', closeButton.getAttribute('aria-label') ?? 'Informationsfenster schließen');
closeButton.setAttribute('title', closeButton.getAttribute('title') ?? 'Schließen');
if (!closeButton.textContent.trim()) closeButton.textContent = '×';
if (closeButton.parentElement !== header) header.appendChild(closeButton);
let content = shell.querySelector('.info-dialog-content');
if (!content) {
content = createElement('div', { className: 'info-dialog-content' });
shell.appendChild(content);
}
moveExistingPagesInto(content, dialog);
Object.keys(pageTitles).forEach(name => ensureInfoPage(content, name));
return { dialog, title, closeButton, content };
}
function ensureInfoUi() {
const control = ensureInfoControl();
const dialog = ensureInfoDialog();
return {
control: control.control,
infoButton: control.button,
dropdown: control.dropdown,
dialog: dialog.dialog,
title: dialog.title,
closeButton: dialog.closeButton,
content: dialog.content
};
}
function isActivation(event) {
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 (!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();
event.stopPropagation();
handler(event);
});
element.addEventListener('keydown', event => {
if (!isActivation(event)) return;
event.preventDefault();
event.stopPropagation();
handler(event);
});
}
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 => {
toggle.checked = enabled;
});
document.querySelectorAll('.cookie-toggle').forEach(toggle => {
toggle.checked = false;
});
}
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';
document.querySelectorAll('[data-info-page]').forEach(item => {
item.hidden = item !== page;
});
if (elements?.content) elements.content.scrollTop = 0;
}
function focusSafely(element) {
if (!element || typeof element.focus !== 'function') return;
try {
element.focus({ preventScroll: true });
} catch {
element.focus();
}
}
function openDialog(name = 'privacy') {
if (!elements?.dialog) return;
const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
setDropdownOpen(false);
showPage(pageName);
syncStorageToggles();
if (!elements.dialog.open) {
if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal();
else elements.dialog.setAttribute('open', '');
}
focusSafely(elements.closeButton ?? elements.dialog);
}
function closeDialog() {
if (elements?.dialog) {
if (typeof elements.dialog.close === 'function') elements.dialog.close();
else elements.dialog.removeAttribute('open');
}
setDropdownOpen(false);
focusSafely(elements?.infoButton);
}
function bindStorageToggles() {
document.querySelectorAll('.progress-storage-toggle').forEach(toggle => {
if (toggle.dataset.wegwichtelStorageBound === 'true') return;
toggle.dataset.wegwichtelStorageBound = 'true';
toggle.addEventListener('change', event => {
const enabled = setProgressStorageEnabled(event.currentTarget.checked);
syncStorageToggles();
document.dispatchEvent(new CustomEvent('wegwichtel:progress-storage-changed', {
detail: { enabled }
}));
});
});
}
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);
});
}
if (elements.dialog.dataset.wegwichtelLegalUiDialogBound !== 'true') {
elements.dialog.dataset.wegwichtelLegalUiDialogBound = 'true';
elements.dialog.addEventListener('pointerup', event => {
if (event.target !== elements.dialog || !isActivation(event)) return;
event.preventDefault();
event.stopPropagation();
closeDialog();
});
elements.dialog.addEventListener('cancel', event => {
event.preventDefault();
closeDialog();
});
}
}
function init() {
elements = ensureInfoUi();
bindDialog();
bindStorageToggles();
syncStorageToggles();
ns.LegalUi = Object.freeze({
open: openDialog,
close: closeDialog,
toggleMenu: toggleDropdown,
closeMenu: () => setDropdownOpen(false),
syncStorageToggles
});
}
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);
}(globalThis.Wegwichtel ??= {}));