This commit is contained in:
+316
-70
@@ -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 ??= {}));
|
||||||
|
|||||||
Reference in New Issue
Block a user