update legal documents and minor code smells
Sonarqube Scanner / Build and analyze (push) Failing after 21s
Sonarqube Scanner / Build and analyze (push) Failing after 21s
This commit is contained in:
+385
-298
@@ -1,20 +1,32 @@
|
||||
globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
(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']
|
||||
]);
|
||||
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) {
|
||||
@@ -30,54 +42,62 @@
|
||||
}
|
||||
|
||||
function qs(selector, root = document) {
|
||||
return root && typeof root.querySelector === 'function' ? root.querySelector(selector) : null;
|
||||
return typeof root?.querySelector === 'function' ? root.querySelector(selector) : null;
|
||||
}
|
||||
|
||||
function qsa(selector, root = document) {
|
||||
if (root && typeof root.querySelectorAll === 'function') return Array.from(root.querySelectorAll(selector));
|
||||
if (typeof root?.querySelectorAll === 'function') return Array.from(root.querySelectorAll(selector));
|
||||
if (root !== document && typeof document.querySelectorAll === 'function') return Array.from(document.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]) => {
|
||||
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);
|
||||
});
|
||||
Object.entries(attributes).forEach(([name, value]) => assignAttribute(element, name, value));
|
||||
if (text) element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
function setAttribute(element, name, value) {
|
||||
if (!element || typeof element.setAttribute !== 'function') return;
|
||||
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 (!element || !element.classList) return;
|
||||
if (typeof element.classList.toggle === 'function') {
|
||||
if (typeof element?.classList?.toggle === 'function') {
|
||||
element.classList.toggle(className, enabled);
|
||||
return;
|
||||
}
|
||||
if (enabled && typeof element.classList.add === 'function') element.classList.add(className);
|
||||
if (!enabled && typeof element.classList.remove === 'function') element.classList.remove(className);
|
||||
}
|
||||
|
||||
function append(parent, child) {
|
||||
if (!parent || !child || typeof parent.appendChild !== 'function') return false;
|
||||
if (!child || typeof parent?.appendChild !== 'function') return false;
|
||||
parent.appendChild(child);
|
||||
return true;
|
||||
}
|
||||
@@ -95,238 +115,293 @@
|
||||
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() {
|
||||
let control = qs('.info-control');
|
||||
let infoButton = byId('info-button') || qs('.info-button');
|
||||
let dropdown = byId('info-dropdown') || qs('.info-dropdown');
|
||||
|
||||
if (!control && typeof document.createElement === 'function') {
|
||||
control = createElement('div', { className: 'info-control' });
|
||||
prependToBody(control);
|
||||
}
|
||||
|
||||
if (!infoButton && control) {
|
||||
infoButton = createElement('button', {
|
||||
id: 'info-button',
|
||||
className: 'info-button',
|
||||
type: 'button',
|
||||
'aria-label': 'Informationsmenü öffnen',
|
||||
title: 'Informationen'
|
||||
});
|
||||
append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i'));
|
||||
append(control, infoButton);
|
||||
}
|
||||
|
||||
if (!dropdown && control) {
|
||||
dropdown = createElement('nav', {
|
||||
id: 'info-dropdown',
|
||||
className: 'info-dropdown',
|
||||
'aria-label': 'Rechtliche und technische Informationen'
|
||||
});
|
||||
append(control, dropdown);
|
||||
}
|
||||
|
||||
if (infoButton) {
|
||||
setAttribute(infoButton, 'type', 'button');
|
||||
setAttribute(infoButton, 'aria-haspopup', 'true');
|
||||
setAttribute(infoButton, 'aria-expanded', 'false');
|
||||
setAttribute(infoButton, 'aria-controls', 'info-dropdown');
|
||||
if (typeof infoButton.getAttribute !== 'function' || !infoButton.getAttribute('aria-label')) {
|
||||
setAttribute(infoButton, 'aria-label', 'Informationsmenü öffnen');
|
||||
}
|
||||
if (!String(infoButton.textContent ?? '').trim()) {
|
||||
append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i'));
|
||||
}
|
||||
}
|
||||
|
||||
if (dropdown) {
|
||||
setAttribute(dropdown, 'aria-label', 'Rechtliche und technische Informationen');
|
||||
dropdown.hidden = true;
|
||||
|
||||
menuEntries.forEach(([target, label]) => {
|
||||
let button = qs(`[data-info-target="${target}"]`, dropdown)
|
||||
|| qsa('[data-info-target]', dropdown).find(item => item.dataset?.infoTarget === target);
|
||||
if (!button) {
|
||||
button = createElement('button', {
|
||||
type: 'button',
|
||||
dataset: { infoTarget: target }
|
||||
}, label);
|
||||
append(dropdown, button);
|
||||
}
|
||||
if (!button) return;
|
||||
setAttribute(button, 'type', 'button');
|
||||
button.dataset.infoTarget = target;
|
||||
if (!String(button.textContent ?? '').trim()) button.textContent = label;
|
||||
});
|
||||
}
|
||||
|
||||
const control = ensureControlContainer();
|
||||
const infoButton = ensureInfoButton(control);
|
||||
const dropdown = ensureDropdown(control);
|
||||
configureInfoButton(infoButton);
|
||||
configureDropdown(dropdown);
|
||||
return { control, infoButton, dropdown };
|
||||
}
|
||||
|
||||
function ensureInfoPage(content, name) {
|
||||
let page = qs(`[data-info-page="${name}"]`, content) || qs(`[data-info-page="${name}"]`);
|
||||
if (!page && content) {
|
||||
page = createElement('article', {
|
||||
className: 'info-page',
|
||||
dataset: { infoPage: name }
|
||||
});
|
||||
append(page, createElement('p', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`));
|
||||
append(content, page);
|
||||
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': 'Informationsmenü öffnen',
|
||||
title: 'Informationen'
|
||||
});
|
||||
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': 'Rechtliche und technische Informationen'
|
||||
});
|
||||
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', 'Informationsmenü öffnen');
|
||||
}
|
||||
|
||||
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', 'Rechtliche und technische Informationen');
|
||||
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);
|
||||
}
|
||||
if (page) {
|
||||
page.dataset.infoPage = name;
|
||||
if (name !== 'privacy') page.hidden = true;
|
||||
}
|
||||
return page;
|
||||
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() {
|
||||
let dialog = byId('info-dialog') || qs('#info-dialog');
|
||||
let title = byId('info-dialog-title') || qs('#info-dialog-title', dialog);
|
||||
let backButton = byId('info-back') || qs('#info-back', dialog);
|
||||
let legacyMenu = byId('info-menu') || qs('#info-menu', dialog);
|
||||
let closeButton = byId('info-close') || qs('#info-close', dialog);
|
||||
let content = qs('.info-dialog-content', dialog) || qs('.info-dialog-content');
|
||||
|
||||
if (!dialog && typeof document.createElement === 'function') {
|
||||
dialog = createElement('dialog', {
|
||||
id: 'info-dialog',
|
||||
className: 'info-dialog',
|
||||
'aria-labelledby': 'info-dialog-title'
|
||||
});
|
||||
append(document.body, dialog);
|
||||
}
|
||||
|
||||
if (dialog && (!title || !closeButton || !content) && typeof document.createElement === 'function') {
|
||||
let shell = qs('.info-dialog-shell', dialog);
|
||||
if (!shell) {
|
||||
shell = createElement('div', { className: 'info-dialog-shell' });
|
||||
append(dialog, shell);
|
||||
}
|
||||
|
||||
let header = qs('.info-dialog-header', shell);
|
||||
if (!header) {
|
||||
header = createElement('header', { className: 'info-dialog-header' });
|
||||
append(shell, header);
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
title = createElement('h2', { id: 'info-dialog-title' }, pageTitles.privacy);
|
||||
append(header, title);
|
||||
}
|
||||
|
||||
if (!closeButton) {
|
||||
closeButton = createElement('button', {
|
||||
id: 'info-close',
|
||||
type: 'button',
|
||||
'aria-label': 'Informationsfenster schließen',
|
||||
title: 'Schließen'
|
||||
}, '×');
|
||||
append(header, closeButton);
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
content = createElement('div', { className: 'info-dialog-content' });
|
||||
append(shell, content);
|
||||
}
|
||||
}
|
||||
|
||||
if (dialog) setAttribute(dialog, 'aria-labelledby', 'info-dialog-title');
|
||||
if (backButton) {
|
||||
setAttribute(backButton, 'type', 'button');
|
||||
setAttribute(backButton, 'aria-label', 'Zurück zum Informationsmenü');
|
||||
backButton.hidden = true;
|
||||
}
|
||||
if (legacyMenu) legacyMenu.hidden = true;
|
||||
if (closeButton) {
|
||||
setAttribute(closeButton, 'type', 'button');
|
||||
setAttribute(closeButton, 'aria-label', 'Informationsfenster schließen');
|
||||
if (!String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×';
|
||||
}
|
||||
|
||||
if (content) Object.keys(pageTitles).forEach(name => ensureInfoPage(content, name));
|
||||
|
||||
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', 'Zurück zum Informationsmenü');
|
||||
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': 'Informationsfenster schließen',
|
||||
title: 'Schließen'
|
||||
}, '×');
|
||||
append(header, closeButton);
|
||||
return configureCloseButton(closeButton);
|
||||
}
|
||||
|
||||
function configureCloseButton(closeButton) {
|
||||
setAttribute(closeButton, 'type', 'button');
|
||||
setAttribute(closeButton, 'aria-label', 'Informationsfenster schließen');
|
||||
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', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`));
|
||||
append(content, page);
|
||||
return page;
|
||||
}
|
||||
|
||||
function ensureInfoUi() {
|
||||
return {
|
||||
...ensureInfoControl(),
|
||||
...ensureInfoDialog()
|
||||
};
|
||||
return { ...ensureInfoControl(), ...ensureInfoDialog() };
|
||||
}
|
||||
|
||||
function isActivation(event) {
|
||||
if (!event) return false;
|
||||
if (event.type === 'pointerup') {
|
||||
if (event.isPrimary === false) return false;
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return false;
|
||||
return true;
|
||||
}
|
||||
if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' ';
|
||||
return false;
|
||||
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) {
|
||||
const key = handler.name ? `wegwichtelLegalUiBound${handler.name}` : 'wegwichtelLegalUiBound';
|
||||
if (element.dataset[key] === 'true') return;
|
||||
element.dataset[key] = 'true';
|
||||
}
|
||||
element.addEventListener('pointerup', event => {
|
||||
if (!isActivation(event)) return;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
||||
handler(event);
|
||||
});
|
||||
element.addEventListener('keydown', event => {
|
||||
if (!isActivation(event)) return;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
||||
handler(event);
|
||||
});
|
||||
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);
|
||||
elements.dropdown.hidden = !dropdownOpen;
|
||||
toggleClass(elements.dropdown, 'is-open', dropdownOpen);
|
||||
setAttribute(elements.infoButton, 'aria-expanded', dropdownOpen ? 'true' : 'false');
|
||||
cancelSchedule(dropdownCloseTimer);
|
||||
dropdownCloseTimer = null;
|
||||
if (dropdownOpen) openDropdown();
|
||||
else closeDropdown();
|
||||
}
|
||||
|
||||
function hidePages() {
|
||||
qsa('[data-info-page]').forEach(item => {
|
||||
item.hidden = true;
|
||||
});
|
||||
function openDropdown() {
|
||||
setHidden(elements.dropdown, false);
|
||||
runNextFrame(() => toggleClass(elements.dropdown, 'is-open', true));
|
||||
}
|
||||
|
||||
function openLegacyMenu() {
|
||||
if (!elements?.dialog) return;
|
||||
hidePages();
|
||||
if (elements.title) elements.title.textContent = 'Informationen';
|
||||
if (elements.legacyMenu) elements.legacyMenu.hidden = false;
|
||||
if (elements.backButton) elements.backButton.hidden = true;
|
||||
if (!elements.dialog.open) {
|
||||
if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal();
|
||||
else setAttribute(elements.dialog, 'open', '');
|
||||
}
|
||||
focusSafely(elements.closeButton ?? elements.dialog);
|
||||
function closeDropdown() {
|
||||
toggleClass(elements.dropdown, 'is-open', false);
|
||||
dropdownCloseTimer = schedule(() => {
|
||||
if (!dropdownOpen) setHidden(elements.dropdown, true);
|
||||
}, DROPDOWN_ANIMATION_MS);
|
||||
}
|
||||
|
||||
function handleInfoButton() {
|
||||
if (elements?.dropdown) {
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
return;
|
||||
}
|
||||
openLegacyMenu();
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}
|
||||
|
||||
function elementContains(element, target) {
|
||||
if (!element || !target) return false;
|
||||
if (element === target) return true;
|
||||
return typeof element.contains === 'function' && element.contains(target);
|
||||
return Boolean(element && target && (element === target || element.contains?.(target)));
|
||||
}
|
||||
|
||||
function isInsideInfoControl(target) {
|
||||
@@ -334,39 +409,38 @@
|
||||
}
|
||||
|
||||
function isProgressStorageEnabled() {
|
||||
if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.isProgressStorageEnabled !== 'function') return false;
|
||||
return ns.PrivacyStorage.isProgressStorageEnabled();
|
||||
return ns?.PrivacyStorage?.isProgressStorageEnabled?.() === true;
|
||||
}
|
||||
|
||||
function setProgressStorageEnabled(enabled) {
|
||||
if (!ns.PrivacyStorage || typeof ns.PrivacyStorage.setProgressStorageEnabled !== 'function') return false;
|
||||
return ns.PrivacyStorage.setProgressStorageEnabled(enabled);
|
||||
return ns?.PrivacyStorage?.setProgressStorageEnabled?.(enabled) === true;
|
||||
}
|
||||
|
||||
function syncStorageToggles() {
|
||||
const enabled = isProgressStorageEnabled();
|
||||
qsa('.progress-storage-toggle').forEach(toggle => {
|
||||
toggle.checked = enabled;
|
||||
});
|
||||
qsa('.cookie-toggle').forEach(toggle => {
|
||||
toggle.checked = false;
|
||||
});
|
||||
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 = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
|
||||
const pageName = pageNameOrDefault(name);
|
||||
const page = qs(`[data-info-page="${pageName}"]`, elements?.content) || qs(`[data-info-page="${pageName}"]`);
|
||||
if (!page) return;
|
||||
|
||||
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen';
|
||||
qsa('[data-info-page]').forEach(item => {
|
||||
item.hidden = item !== page;
|
||||
});
|
||||
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] ?? 'Informationen';
|
||||
}
|
||||
|
||||
function focusSafely(element) {
|
||||
if (!element || typeof element.focus !== 'function') return;
|
||||
if (typeof element?.focus !== 'function') return;
|
||||
try {
|
||||
element.focus({ preventScroll: true });
|
||||
} catch {
|
||||
@@ -376,85 +450,99 @@
|
||||
|
||||
function openDialog(name = 'privacy') {
|
||||
if (!elements?.dialog) return;
|
||||
const pageName = Object.prototype.hasOwnProperty.call(pageTitles, name) ? name : 'privacy';
|
||||
setDropdownOpen(false);
|
||||
if (elements?.legacyMenu) elements.legacyMenu.hidden = true;
|
||||
if (elements?.backButton) elements.backButton.hidden = !elements?.legacyMenu;
|
||||
showPage(pageName);
|
||||
setHidden(elements.legacyMenu, true);
|
||||
setHidden(elements.backButton, true);
|
||||
showPage(pageNameOrDefault(name));
|
||||
syncStorageToggles();
|
||||
if (!elements.dialog.open) {
|
||||
if (typeof elements.dialog.showModal === 'function') elements.dialog.showModal();
|
||||
else setAttribute(elements.dialog, 'open', '');
|
||||
}
|
||||
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() {
|
||||
if (elements?.dialog) {
|
||||
if (typeof elements.dialog.close === 'function') elements.dialog.close();
|
||||
else if (typeof elements.dialog.removeAttribute === 'function') elements.dialog.removeAttribute('open');
|
||||
}
|
||||
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('.progress-storage-toggle').forEach(toggle => {
|
||||
if (toggle.dataset?.wegwichtelStorageBound === 'true') return;
|
||||
if (toggle.dataset) toggle.dataset.wegwichtelStorageBound = 'true';
|
||||
toggle.addEventListener('change', event => {
|
||||
const source = event.currentTarget || event.target || toggle;
|
||||
const enabled = setProgressStorageEnabled(Boolean(source.checked));
|
||||
syncStorageToggles();
|
||||
document.dispatchEvent(new CustomEvent('wegwichtel:progress-storage-changed', {
|
||||
detail: { enabled }
|
||||
}));
|
||||
});
|
||||
});
|
||||
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() {
|
||||
if (elements?.dropdown) return qsa('[data-info-target]', elements.dropdown);
|
||||
if (elements?.legacyMenu) return qsa('[data-info-target]', elements.legacyMenu);
|
||||
return qsa('[data-info-target]');
|
||||
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, openLegacyMenu);
|
||||
activate(elements?.backButton, () => setDropdownOpen(true));
|
||||
infoTargetButtons().forEach(button => activate(button, () => openDialog(button.dataset?.infoTarget)));
|
||||
bindDocumentEvents();
|
||||
bindDialogEvents();
|
||||
}
|
||||
|
||||
infoTargetButtons().forEach(button => {
|
||||
activate(button, () => openDialog(button.dataset?.infoTarget));
|
||||
});
|
||||
function bindDocumentEvents() {
|
||||
if (documentEventsBound) return;
|
||||
documentEventsBound = true;
|
||||
document.addEventListener('pointerup', closeDropdownOnOutsidePointer);
|
||||
document.addEventListener('keydown', closeDropdownOnEscape);
|
||||
}
|
||||
|
||||
if (!documentEventsBound) {
|
||||
documentEventsBound = 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;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
setDropdownOpen(false);
|
||||
focusSafely(elements?.infoButton);
|
||||
});
|
||||
}
|
||||
function closeDropdownOnOutsidePointer(event) {
|
||||
if (dropdownOpen && !isInsideInfoControl(event.target)) setDropdownOpen(false);
|
||||
}
|
||||
|
||||
if (elements?.dialog && elements.dialog.dataset?.wegwichtelLegalUiDialogBound !== 'true') {
|
||||
if (elements.dialog.dataset) elements.dialog.dataset.wegwichtelLegalUiDialogBound = 'true';
|
||||
elements.dialog.addEventListener('pointerup', event => {
|
||||
if (event.target !== elements.dialog || !isActivation(event)) return;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
||||
closeDialog();
|
||||
});
|
||||
elements.dialog.addEventListener('cancel', event => {
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
closeDialog();
|
||||
});
|
||||
}
|
||||
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() {
|
||||
@@ -462,7 +550,6 @@
|
||||
bindDialog();
|
||||
bindStorageToggles();
|
||||
syncStorageToggles();
|
||||
|
||||
ns.LegalUi = Object.freeze({
|
||||
open: openDialog,
|
||||
close: closeDialog,
|
||||
@@ -473,4 +560,4 @@
|
||||
}
|
||||
|
||||
ready(init);
|
||||
}(globalThis.Wegwichtel = globalThis.Wegwichtel || {}));
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
+141
-185
@@ -1,11 +1,11 @@
|
||||
globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const SETTINGS_KEY = 'wegwichtel.privacy.v1';
|
||||
const PRIVACY_KEY = 'wegwichtel.privacy.v1';
|
||||
const PROGRESS_KEY = 'wegwichtel.route-progress.v1';
|
||||
const COOKIE_PREFIX = 'wegwichtel_';
|
||||
const MAX_PROGRESS_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const STORE_VERSION = 1;
|
||||
|
||||
function storage() {
|
||||
try {
|
||||
@@ -15,28 +15,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function readJson(key, fallback) {
|
||||
const backend = storage();
|
||||
if (!backend) return fallback;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return fallback;
|
||||
try {
|
||||
const raw = backend.getItem(key);
|
||||
return raw ? JSON.parse(raw) : fallback;
|
||||
const raw = store.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
const value = JSON.parse(raw);
|
||||
return value && typeof value === 'object' ? value : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, value) {
|
||||
const backend = storage();
|
||||
if (!backend) return false;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return false;
|
||||
try {
|
||||
backend.setItem(key, JSON.stringify(value));
|
||||
store.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -44,208 +44,164 @@
|
||||
}
|
||||
|
||||
function removeItem(key) {
|
||||
const backend = storage();
|
||||
if (!backend) return;
|
||||
|
||||
const store = storage();
|
||||
if (!store) return false;
|
||||
try {
|
||||
backend.removeItem(key);
|
||||
store.removeItem(key);
|
||||
return true;
|
||||
} catch {
|
||||
// Private browsing modes may reject storage access. The app remains usable without persistence.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSettings() {
|
||||
return { progressStorage: false, updatedAt: null };
|
||||
}
|
||||
|
||||
function settings() {
|
||||
const value = readJson(SETTINGS_KEY, defaultSettings());
|
||||
function normalizeSettings(value) {
|
||||
return {
|
||||
progressStorage: value?.progressStorage === true,
|
||||
updatedAt: typeof value?.updatedAt === 'string' ? value.updatedAt : null
|
||||
cookies: false,
|
||||
updatedAt: Number(value?.updatedAt) || now()
|
||||
};
|
||||
}
|
||||
|
||||
function saveSettings(progressStorage) {
|
||||
return writeJson(SETTINGS_KEY, {
|
||||
progressStorage: progressStorage === true,
|
||||
updatedAt: nowIso()
|
||||
});
|
||||
function readSettings() {
|
||||
return normalizeSettings(readJson(PRIVACY_KEY, {}));
|
||||
}
|
||||
|
||||
function defaultProgressStore() {
|
||||
return { version: STORE_VERSION, currentRouteId: null, routes: {} };
|
||||
function writeSettings(settings) {
|
||||
return writeJson(PRIVACY_KEY, normalizeSettings({ ...settings, updatedAt: now() }));
|
||||
}
|
||||
|
||||
function progressStore() {
|
||||
const value = readJson(PROGRESS_KEY, defaultProgressStore());
|
||||
if (value?.version !== STORE_VERSION || typeof value?.routes !== 'object' || value.routes === null) {
|
||||
return defaultProgressStore();
|
||||
}
|
||||
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
currentRouteId: value.currentRouteId ?? null,
|
||||
routes: value.routes
|
||||
};
|
||||
}
|
||||
|
||||
function persistProgressStore(value) {
|
||||
if (!settings().progressStorage) return false;
|
||||
return writeJson(PROGRESS_KEY, value);
|
||||
}
|
||||
|
||||
function uniqueIds(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : [])
|
||||
.map(value => Number(value))
|
||||
.filter(Number.isFinite))];
|
||||
}
|
||||
|
||||
function routeRecord(route, previous = {}) {
|
||||
const timestamp = nowIso();
|
||||
return {
|
||||
routeId: route.id,
|
||||
routeName: String(route.name ?? previous.routeName ?? ''),
|
||||
routeState: previous.routeState ?? 'idle',
|
||||
routeProgressIndex: Number.isInteger(previous.routeProgressIndex)
|
||||
? previous.routeProgressIndex
|
||||
: null,
|
||||
visitedPoiIds: uniqueIds(previous.visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(previous.viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(previous.playedAudioPoiIds),
|
||||
startedAt: previous.startedAt ?? timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
function updateRoute(routeId, updater) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(routeId);
|
||||
const current = store.routes[key];
|
||||
if (!current) return null;
|
||||
|
||||
const updated = updater({ ...current });
|
||||
updated.updatedAt = nowIso();
|
||||
store.currentRouteId = routeId;
|
||||
store.routes[key] = updated;
|
||||
persistProgressStore(store);
|
||||
return updated;
|
||||
}
|
||||
|
||||
function markPoi(routeId, field, poiId) {
|
||||
const normalizedId = Number(poiId);
|
||||
if (!Number.isFinite(normalizedId)) return null;
|
||||
|
||||
return updateRoute(routeId, record => {
|
||||
record[field] = uniqueIds([...(record[field] ?? []), normalizedId]);
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
function clearApplicationCookies() {
|
||||
const cookieText = globalThis.document?.cookie;
|
||||
if (!cookieText) return;
|
||||
|
||||
cookieText.split(';').forEach(item => {
|
||||
const name = item.split('=', 1)[0].trim();
|
||||
if (!name.startsWith(COOKIE_PREFIX)) return;
|
||||
globalThis.document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`;
|
||||
});
|
||||
function isProgressStorageEnabled() {
|
||||
return readSettings().progressStorage === true;
|
||||
}
|
||||
|
||||
function setProgressStorageEnabled(enabled) {
|
||||
const active = enabled === true;
|
||||
const saved = saveSettings(active);
|
||||
if (!active) removeItem(PROGRESS_KEY);
|
||||
return saved && settings().progressStorage;
|
||||
const nextValue = Boolean(enabled);
|
||||
writeSettings({ progressStorage: nextValue, cookies: false });
|
||||
if (!nextValue) removeItem(PROGRESS_KEY);
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function cookiesEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function routeId(route) {
|
||||
const id = route?.id ?? route?.routeId ?? route;
|
||||
return id === null || id === undefined ? null : String(id);
|
||||
}
|
||||
|
||||
function asNumberOrNull(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function numericIdSet(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return [...new Set(values.map(Number).filter(Number.isFinite))];
|
||||
}
|
||||
|
||||
function readProgressStore() {
|
||||
const data = readJson(PROGRESS_KEY, {});
|
||||
const entries = data?.routes && typeof data.routes === 'object' ? data.routes : data;
|
||||
return entries && typeof entries === 'object' ? entries : {};
|
||||
}
|
||||
|
||||
function writeProgressStore(routes) {
|
||||
return writeJson(PROGRESS_KEY, {
|
||||
version: 1,
|
||||
updatedAt: now(),
|
||||
routes
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSavedRouteState(value) {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
return {
|
||||
routeId: value.routeId === null || value.routeId === undefined ? null : String(value.routeId),
|
||||
routeState: value.routeState === 'running' || value.routeState === 'paused' ? value.routeState : 'idle',
|
||||
routeProgressIndex: Number.isInteger(value.routeProgressIndex) ? value.routeProgressIndex : null,
|
||||
visitedPoiIds: numericIdSet(value.visitedPoiIds),
|
||||
viewedPoiIds: numericIdSet(value.viewedPoiIds),
|
||||
playedAudioPoiIds: numericIdSet(value.playedAudioPoiIds),
|
||||
savedAt: Number(value.savedAt) || Number(value.updatedAt) || 0,
|
||||
updatedAt: Number(value.updatedAt) || Number(value.savedAt) || 0
|
||||
};
|
||||
}
|
||||
|
||||
function isExpired(value, referenceTime = now()) {
|
||||
const saved = Number(value?.updatedAt) || Number(value?.savedAt) || 0;
|
||||
return saved <= 0 || referenceTime - saved > MAX_PROGRESS_AGE_MS;
|
||||
}
|
||||
|
||||
function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds = [], viewedPoiIds = [], playedAudioPoiIds = []) {
|
||||
if (!isProgressStorageEnabled()) return false;
|
||||
const id = routeId(route);
|
||||
if (!id) return false;
|
||||
const routes = readProgressStore();
|
||||
const timestamp = now();
|
||||
routes[id] = {
|
||||
routeId: id,
|
||||
routeName: route?.name ?? '',
|
||||
routeState: routeState === 'running' || routeState === 'paused' ? routeState : 'idle',
|
||||
routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null,
|
||||
visitedPoiIds: numericIdSet(visitedPoiIds),
|
||||
viewedPoiIds: numericIdSet(viewedPoiIds),
|
||||
playedAudioPoiIds: numericIdSet(playedAudioPoiIds),
|
||||
savedAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
return writeProgressStore(routes);
|
||||
}
|
||||
|
||||
function activateRoute(route) {
|
||||
if (!settings().progressStorage || route?.id == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(route.id);
|
||||
const record = routeRecord(route, store.routes[key]);
|
||||
store.currentRouteId = route.id;
|
||||
store.routes[key] = record;
|
||||
persistProgressStore(store);
|
||||
return record;
|
||||
if (!isProgressStorageEnabled()) return null;
|
||||
const id = routeId(route);
|
||||
if (!id) return null;
|
||||
const routes = cleanupExpiredEntries().routes;
|
||||
const saved = normalizeSavedRouteState(routes[id]);
|
||||
return saved && !isExpired(saved) ? saved : null;
|
||||
}
|
||||
|
||||
function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds, viewedPoiIds, playedAudioPoiIds) {
|
||||
if (route?.id == null) return null;
|
||||
|
||||
return updateRoute(route.id, record => ({
|
||||
...record,
|
||||
routeName: String(route.name ?? record.routeName ?? ''),
|
||||
routeState: String(routeState ?? 'idle'),
|
||||
routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null,
|
||||
visitedPoiIds: uniqueIds(visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(playedAudioPoiIds)
|
||||
}));
|
||||
}
|
||||
|
||||
function loadRoute(routeId) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
return progressStore().routes[String(routeId)] ?? null;
|
||||
}
|
||||
|
||||
function nextTurn() {
|
||||
return new Promise(resolve => globalThis.setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
async function cleanupExpired(onProgress = () => {}) {
|
||||
if (!settings().progressStorage) {
|
||||
removeItem(PROGRESS_KEY);
|
||||
return { removed: 0, total: 0 };
|
||||
}
|
||||
|
||||
const store = progressStore();
|
||||
const cutoff = Date.now() - MAX_PROGRESS_AGE_MS;
|
||||
const expiredKeys = Object.entries(store.routes)
|
||||
.filter(([, route]) => {
|
||||
const timestamp = Date.parse(route?.updatedAt ?? '');
|
||||
return !Number.isFinite(timestamp) || timestamp <= cutoff;
|
||||
})
|
||||
.map(([key]) => key);
|
||||
|
||||
if (!expiredKeys.length) return { removed: 0, total: 0 };
|
||||
|
||||
function cleanupExpiredEntries(progressCallback) {
|
||||
const referenceTime = now();
|
||||
const routes = readProgressStore();
|
||||
const entries = Object.entries(routes);
|
||||
const kept = {};
|
||||
let removed = 0;
|
||||
onProgress(removed, expiredKeys.length);
|
||||
|
||||
for (const key of expiredKeys) {
|
||||
delete store.routes[key];
|
||||
if (String(store.currentRouteId) === key) store.currentRouteId = null;
|
||||
removed += 1;
|
||||
persistProgressStore(store);
|
||||
onProgress(removed, expiredKeys.length);
|
||||
await nextTurn();
|
||||
}
|
||||
entries.forEach(([id, value], index) => {
|
||||
const normalized = normalizeSavedRouteState(value);
|
||||
if (normalized && !isExpired(normalized, referenceTime)) kept[id] = normalized;
|
||||
else removed += 1;
|
||||
reportCleanupProgress(progressCallback, index + 1, entries.length, removed);
|
||||
});
|
||||
|
||||
return { removed, total: expiredKeys.length };
|
||||
if (removed > 0) writeProgressStore(kept);
|
||||
return { removed, total: entries.length, routes: kept };
|
||||
}
|
||||
|
||||
clearApplicationCookies();
|
||||
function reportCleanupProgress(callback, processed, total, removed) {
|
||||
if (typeof callback !== 'function') return;
|
||||
callback({ processed, total, removed });
|
||||
}
|
||||
|
||||
async function cleanupExpiredProgress(progressCallback) {
|
||||
return cleanupExpiredEntries(progressCallback);
|
||||
}
|
||||
|
||||
ns.PrivacyStorage = Object.freeze({
|
||||
SETTINGS_KEY,
|
||||
PRIVACY_KEY,
|
||||
PROGRESS_KEY,
|
||||
MAX_PROGRESS_AGE_MS,
|
||||
cookiesEnabled: () => false,
|
||||
clearApplicationCookies,
|
||||
isProgressStorageEnabled: () => settings().progressStorage,
|
||||
setProgressStorageEnabled,
|
||||
cleanupExpired,
|
||||
activateRoute,
|
||||
loadRoute,
|
||||
cleanExpiredProgress: cleanupExpiredProgress,
|
||||
cleanupExpiredProgress,
|
||||
cookiesEnabled: () => false,
|
||||
getPrivacySettings: readSettings,
|
||||
isProgressStorageEnabled,
|
||||
removeExpiredProgress: cleanupExpiredProgress,
|
||||
removeExpiredRouteProgress: cleanupExpiredProgress,
|
||||
saveRouteState,
|
||||
markVisited: (routeId, poiId) => markPoi(routeId, 'visitedPoiIds', poiId),
|
||||
markViewed: (routeId, poiId) => markPoi(routeId, 'viewedPoiIds', poiId),
|
||||
markAudioPlayed: (routeId, poiId) => markPoi(routeId, 'playedAudioPoiIds', poiId),
|
||||
clearProgress: () => removeItem(PROGRESS_KEY)
|
||||
setProgressStorageEnabled
|
||||
});
|
||||
}(globalThis.Wegwichtel ??= {}));
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
Reference in New Issue
Block a user