fix event assignment order
Sonarqube Scanner / Build and analyze (push) Canceled after 0s

This commit is contained in:
2026-08-29 17:13:33 +02:00
parent cbe50b1a28
commit bba13d6907
+316 -70
View File
@@ -7,24 +7,225 @@
imprint: 'Impressum', imprint: 'Impressum',
technology: 'Cookies und Technik' technology: 'Cookies und Technik'
}); });
const menuEntries = Object.freeze([
['privacy', 'Datenschutzerklärung'],
['imprint', 'Impressum'],
['technology', 'Cookies und verwendete JavaScripts']
]);
const dialog = document.getElementById('info-dialog'); let elements = null;
const title = document.getElementById('info-dialog-title')
?? dialog?.querySelector('h1, h2, .info-dialog-title')
?? null;
const dropdown = document.getElementById('info-dropdown')
?? document.querySelector('.info-dropdown')
?? null;
const infoButton = document.getElementById('info-button')
?? document.querySelector('[aria-controls="info-dropdown"], .info-button')
?? null;
const closeButton = document.getElementById('info-close')
?? dialog?.querySelector('[data-info-close], .info-close, .info-dialog-close, button[aria-label="Dialog schließen"], button[aria-label="Schließen"]')
?? null;
let dropdownOpen = false; let dropdownOpen = false;
let dropdownCloseTimer = null; 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) { function isActivation(event) {
if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' '; if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' ';
if (event.type !== 'pointerup') return false; if (event.type !== 'pointerup') return false;
@@ -34,6 +235,8 @@
function activate(element, handler) { function activate(element, handler) {
if (!element || typeof element.addEventListener !== 'function') return; if (!element || typeof element.addEventListener !== 'function') return;
if (element.dataset.wegwichtelLegalUiBound === 'true') return;
element.dataset.wegwichtelLegalUiBound = 'true';
element.addEventListener('pointerup', event => { element.addEventListener('pointerup', event => {
if (!isActivation(event)) return; if (!isActivation(event)) return;
event.preventDefault(); event.preventDefault();
@@ -73,22 +276,23 @@
} }
function setDropdownOpen(open) { function setDropdownOpen(open) {
if (!dropdown || !infoButton) return; if (!elements?.dropdown || !elements?.infoButton) return;
dropdownOpen = open; dropdownOpen = open;
infoButton.setAttribute('aria-expanded', open ? 'true' : 'false'); elements.infoButton.setAttribute('aria-expanded', open ? 'true' : 'false');
elements.infoButton.setAttribute('aria-label', open ? 'Informationsmenü schließen' : 'Informationsmenü öffnen');
cancelSchedule(dropdownCloseTimer); cancelSchedule(dropdownCloseTimer);
dropdownCloseTimer = null; dropdownCloseTimer = null;
if (open) { if (open) {
dropdown.hidden = false; elements.dropdown.hidden = false;
runNextFrame(() => setClass(dropdown, 'is-open', true)); runNextFrame(() => setClass(elements.dropdown, 'is-open', true));
return; return;
} }
setClass(dropdown, 'is-open', false); setClass(elements.dropdown, 'is-open', false);
dropdownCloseTimer = schedule(() => { dropdownCloseTimer = schedule(() => {
if (dropdownOpen) return; if (dropdownOpen || !elements?.dropdown) return;
dropdown.hidden = true; elements.dropdown.hidden = true;
}, DROPDOWN_ANIMATION_MS); }, DROPDOWN_ANIMATION_MS);
} }
@@ -103,7 +307,7 @@
} }
function isInsideInfoControl(target) { function isInsideInfoControl(target) {
return elementContains(infoButton, target) || elementContains(dropdown, target); return elementContains(elements?.infoButton, target) || elementContains(elements?.dropdown, target);
} }
function isProgressStorageEnabled() { function isProgressStorageEnabled() {
@@ -127,46 +331,53 @@
} }
function showPage(name) { function showPage(name) {
const page = document.querySelector(`[data-info-page="${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 (!page) return;
if (title) title.textContent = pageTitles[name] ?? 'Informationen'; if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen';
document.querySelectorAll('[data-info-page]').forEach(item => { document.querySelectorAll('[data-info-page]').forEach(item => {
item.hidden = item !== page; item.hidden = item !== page;
}); });
const content = document.querySelector('.info-dialog-content'); if (elements?.content) elements.content.scrollTop = 0;
if (content) content.scrollTop = 0;
} }
function focusSafely(element) { function focusSafely(element) {
if (!element || typeof element.focus !== 'function') return; if (!element || typeof element.focus !== 'function') return;
element.focus({ preventScroll: true }); try {
element.focus({ preventScroll: true });
} catch {
element.focus();
}
} }
function openDialog(name = 'privacy') { function openDialog(name = 'privacy') {
if (!dialog) return; if (!elements?.dialog) return;
const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy'; const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
setDropdownOpen(false); setDropdownOpen(false);
showPage(pageName); showPage(pageName);
syncStorageToggles(); syncStorageToggles();
if (!dialog.open) { if (!elements.dialog.open) {
if (typeof dialog.showModal === 'function') dialog.showModal(); if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal();
else dialog.setAttribute('open', ''); else elements.dialog.setAttribute('open', '');
} }
focusSafely(closeButton ?? dialog); focusSafely(elements.closeButton ?? elements.dialog);
} }
function closeDialog() { function closeDialog() {
if (dialog) { if (elements?.dialog) {
if (typeof dialog.close === 'function') dialog.close(); if (typeof elements.dialog.close === 'function') elements.dialog.close();
else dialog.removeAttribute('open'); else elements.dialog.removeAttribute('open');
} }
setDropdownOpen(false); setDropdownOpen(false);
focusSafely(infoButton); focusSafely(elements?.infoButton);
} }
function bindStorageToggles() { function bindStorageToggles() {
document.querySelectorAll('.progress-storage-toggle').forEach(toggle => { document.querySelectorAll('.progress-storage-toggle').forEach(toggle => {
if (toggle.dataset.wegwichtelStorageBound === 'true') return;
toggle.dataset.wegwichtelStorageBound = 'true';
toggle.addEventListener('change', event => { toggle.addEventListener('change', event => {
const enabled = setProgressStorageEnabled(event.currentTarget.checked); const enabled = setProgressStorageEnabled(event.currentTarget.checked);
syncStorageToggles(); syncStorageToggles();
@@ -177,44 +388,79 @@
}); });
} }
activate(infoButton, toggleDropdown); function bindDialog() {
activate(closeButton, closeDialog); activate(elements.infoButton, toggleDropdown);
document.querySelectorAll('[data-info-target]').forEach(button => { activate(elements.closeButton, closeDialog);
activate(button, () => openDialog(button.dataset.infoTarget)); elements.dropdown.querySelectorAll('[data-info-target]').forEach(button => {
}); activate(button, () => openDialog(button.dataset.infoTarget));
document.addEventListener('pointerup', event => {
if (!dropdownOpen || isInsideInfoControl(event.target)) return;
setDropdownOpen(false);
});
document.addEventListener('keydown', event => {
if (!dropdownOpen || event.key !== 'Escape' || dialog?.open) return;
event.preventDefault();
setDropdownOpen(false);
focusSafely(infoButton);
});
if (dialog) {
dialog.addEventListener('pointerup', event => {
if (event.target !== dialog || !isActivation(event)) return;
event.preventDefault();
event.stopPropagation();
closeDialog();
}); });
dialog.addEventListener('cancel', event => {
event.preventDefault(); if (!document.documentElement.dataset.wegwichtelLegalUiDocumentBound) {
closeDialog(); 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
}); });
} }
bindStorageToggles();
syncStorageToggles();
ns.LegalUi = Object.freeze({ ns.LegalUi = Object.freeze({
open: openDialog, open: name => ready(() => {
close: closeDialog, if (!elements) init();
toggleMenu: toggleDropdown, openDialog(name);
closeMenu: () => setDropdownOpen(false), }),
syncStorageToggles 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 ??= {})); }(globalThis.Wegwichtel ??= {}));