349 lines
14 KiB
JavaScript
349 lines
14 KiB
JavaScript
/* =============================================
|
|
HELLION NEWTAB — settings.js
|
|
Settings Panel, Theme-Modal, Accordion, Toggles
|
|
============================================= */
|
|
|
|
// ---- A11Y: Fokus-Management fuer Modals ----
|
|
// Merkt sich das vor dem Oeffnen fokussierte Element, damit wir es beim
|
|
// Schliessen restaurieren koennen. Pro offenem Modal eine Closure-Variable.
|
|
const _focusReturn = { settings: null, theme: null };
|
|
|
|
/** Liefert die fokussierbaren Elemente innerhalb eines Containers. */
|
|
function _focusable(container) {
|
|
return Array.from(container.querySelectorAll(
|
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
)).filter(el => el.offsetParent !== null);
|
|
}
|
|
|
|
/** Tab/Shift+Tab im Container einfangen + Escape schliesst. */
|
|
function _makeTrap(container, closeFn) {
|
|
return function trap(e) {
|
|
if (e.key === 'Escape') { e.preventDefault(); closeFn(); return; }
|
|
if (e.key !== 'Tab') return;
|
|
const items = _focusable(container);
|
|
if (items.length === 0) return;
|
|
const first = items[0];
|
|
const last = items[items.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault(); last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault(); first.focus();
|
|
}
|
|
};
|
|
}
|
|
|
|
// ---- SETTINGS PANEL ----
|
|
// Hinweis: withViewTransition (Phase 4) bleibt fuer das Fade erhalten; das
|
|
// Fokus-Management (merken, Falle, Rueckgabe) liegt bewusst ausserhalb des
|
|
// Transition-Callbacks. activeElement wird vor der Mutation gelesen.
|
|
let _settingsTrap = null;
|
|
function openSettings() {
|
|
const panel = document.getElementById('settingsPanel');
|
|
_focusReturn.settings = document.activeElement;
|
|
withViewTransition(() => {
|
|
panel.classList.add('open');
|
|
document.getElementById('settingsOverlay').classList.add('active');
|
|
});
|
|
panel.setAttribute('aria-hidden', 'false');
|
|
_settingsTrap = _makeTrap(panel, closeSettings);
|
|
document.addEventListener('keydown', _settingsTrap);
|
|
const first = _focusable(panel)[0];
|
|
if (first) first.focus();
|
|
}
|
|
function closeSettings() {
|
|
const panel = document.getElementById('settingsPanel');
|
|
withViewTransition(() => {
|
|
panel.classList.remove('open');
|
|
document.getElementById('settingsOverlay').classList.remove('active');
|
|
});
|
|
panel.setAttribute('aria-hidden', 'true');
|
|
if (_settingsTrap) { document.removeEventListener('keydown', _settingsTrap); _settingsTrap = null; }
|
|
if (_focusReturn.settings) { _focusReturn.settings.focus(); _focusReturn.settings = null; }
|
|
}
|
|
|
|
// ---- THEME MODAL ----
|
|
let _themeTrap = null;
|
|
function openThemeModal() {
|
|
const overlay = document.getElementById('themeOverlay');
|
|
const modal = document.getElementById('themeModal');
|
|
_focusReturn.theme = document.activeElement;
|
|
withViewTransition(() => {
|
|
overlay.classList.add('active');
|
|
});
|
|
modal.setAttribute('aria-hidden', 'false');
|
|
_themeTrap = _makeTrap(modal, closeThemeModal);
|
|
document.addEventListener('keydown', _themeTrap);
|
|
const first = _focusable(modal)[0];
|
|
if (first) first.focus();
|
|
}
|
|
function closeThemeModal() {
|
|
const overlay = document.getElementById('themeOverlay');
|
|
const modal = document.getElementById('themeModal');
|
|
withViewTransition(() => {
|
|
overlay.classList.remove('active');
|
|
});
|
|
modal.setAttribute('aria-hidden', 'true');
|
|
if (_themeTrap) { document.removeEventListener('keydown', _themeTrap); _themeTrap = null; }
|
|
if (_focusReturn.theme) { _focusReturn.theme.focus(); _focusReturn.theme = null; }
|
|
}
|
|
|
|
/**
|
|
* Wechselt das Theme mit nativem Cross-Fade (View Transitions API).
|
|
* Wrap sitzt bewusst hier am User-Ausloeser, NICHT in applyTheme(),
|
|
* sonst fadet jeder neue Tab beim Initial-Load (settings.js:101).
|
|
* Feature-Detection-Fallback: aeltere Browser (z.B. Firefox < 144)
|
|
* schalten instant um, ohne Bruch.
|
|
* @param {string} name - Theme-Name
|
|
*/
|
|
function switchTheme(name) {
|
|
const swap = () => applyTheme(name, false); // false: Theme-BG anwenden (kein User-bgUrl-Schutz hier noetig, bgUrl wurde geleert)
|
|
withViewTransition(swap);
|
|
}
|
|
|
|
/**
|
|
* Prueft ob eine Background-URL sicher fuer CSS-Einbettung ist.
|
|
* Erlaubt nur blob: und data:image/ Protokolle (aus File Upload).
|
|
* @param {string} url
|
|
* @returns {boolean}
|
|
*/
|
|
function isValidBgUrl(url) {
|
|
return typeof url === 'string' && url.length > 0 &&
|
|
(url.startsWith('blob:') || url.startsWith('data:image/'));
|
|
}
|
|
|
|
// ---- ACCORDION ----
|
|
function initAccordion() {
|
|
const defaultOpen = new Set(['widgets']);
|
|
const sections = document.querySelectorAll('.settings-section[data-section]');
|
|
|
|
sections.forEach(section => {
|
|
const name = section.dataset.section;
|
|
const title = section.querySelector('.settings-section-title');
|
|
|
|
if (defaultOpen.has(name)) {
|
|
section.classList.add('open');
|
|
}
|
|
|
|
title.addEventListener('click', () => {
|
|
section.classList.toggle('open');
|
|
});
|
|
|
|
title.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
section.classList.toggle('open');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---- APPLY SETTINGS ----
|
|
function applySettings() {
|
|
const body = document.body;
|
|
body.classList.toggle('compact', settings.compact);
|
|
body.classList.toggle('shorten-titles', settings.shortenTitles);
|
|
body.classList.toggle('show-desc', settings.showDesc);
|
|
|
|
document.getElementById('settingCompact').checked = settings.compact;
|
|
document.getElementById('settingShorten').checked = settings.shortenTitles;
|
|
document.getElementById('settingNewTab').checked = settings.newTab;
|
|
document.getElementById('settingShowDesc').checked = settings.showDesc;
|
|
document.getElementById('settingHideExtra').checked = settings.hideExtra;
|
|
document.getElementById('settingVisibleCount').value = String(settings.visibleCount);
|
|
document.getElementById('visibleCountRow').classList.toggle('dim', !settings.hideExtra);
|
|
|
|
// showSearch: undefined (alter Save) → true
|
|
if (settings.showSearch === undefined) settings.showSearch = true;
|
|
const searchWrapper = document.getElementById('searchBarWrapper');
|
|
if (searchWrapper) searchWrapper.classList.toggle('hidden', !settings.showSearch);
|
|
const showSearchEl = document.getElementById('settingShowSearch');
|
|
if (showSearchEl) showSearchEl.checked = settings.showSearch;
|
|
|
|
// Image-Ref Toggle
|
|
if (settings.imageRefEnabled === undefined) settings.imageRefEnabled = false;
|
|
const imgRefCheckbox = document.getElementById('settingImageRef');
|
|
if (imgRefCheckbox) imgRefCheckbox.checked = settings.imageRefEnabled;
|
|
const imgRefBtn = document.querySelector('[data-action="image-ref"]');
|
|
if (imgRefBtn) imgRefBtn.classList.toggle('hidden', !settings.imageRefEnabled);
|
|
|
|
// Toolbar-Position
|
|
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
|
|
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
|
if (toolbarPosEl) toolbarPosEl.value = settings.toolbarPos || 'right';
|
|
|
|
// Sprache (Dropdown-Wert setzen — I18n.init() übernimmt die eigentliche Anwendung)
|
|
const langEl = document.getElementById('settingLanguage');
|
|
if (langEl) langEl.value = settings.language || 'auto';
|
|
|
|
applyTheme(settings.theme || 'nebula', !!settings.bgUrl);
|
|
|
|
if (settings.bgUrl && isValidBgUrl(settings.bgUrl)) {
|
|
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
|
|
} else if (settings.bgUrl) {
|
|
settings.bgUrl = '';
|
|
}
|
|
}
|
|
|
|
// ---- BIND EVENTS ----
|
|
function bindSettingsEvents() {
|
|
// Settings Panel
|
|
document.getElementById('settingsOverlay').addEventListener('click', closeSettings);
|
|
document.getElementById('btnCloseSettings').addEventListener('click', closeSettings);
|
|
document.getElementById('btnSettings').addEventListener('click', openSettings);
|
|
|
|
// Theme Modal
|
|
document.getElementById('btnTheme').addEventListener('click', openThemeModal);
|
|
document.getElementById('btnCloseTheme').addEventListener('click', closeThemeModal);
|
|
document.getElementById('themeOverlay').addEventListener('click', e => {
|
|
if (e.target === document.getElementById('themeOverlay')) closeThemeModal();
|
|
});
|
|
|
|
// Theme-Picker (Cards im Theme-Modal)
|
|
const themeCards = document.querySelectorAll('.theme-card');
|
|
function selectThemeCard(card) {
|
|
const name = card.dataset.value;
|
|
if (!name || name === settings.theme) return Promise.resolve();
|
|
settings.theme = name;
|
|
settings.bgUrl = '';
|
|
document.getElementById('bgUrlInput').value = '';
|
|
// aria-pressed synchron halten — applyTheme/switchTheme pflegt nur die .active-Klasse, nicht ARIA
|
|
themeCards.forEach(c => c.setAttribute('aria-pressed', c === card ? 'true' : 'false'));
|
|
switchTheme(name); // WICHTIG: switchTheme aus Phase 4 (View-Transition-Wrapper), NICHT applyTheme direkt — sonst geht der Theme-Fade verloren
|
|
return saveSettings();
|
|
}
|
|
themeCards.forEach(card => {
|
|
card.addEventListener('click', () => selectThemeCard(card));
|
|
card.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
selectThemeCard(card);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Accordion initialisieren
|
|
initAccordion();
|
|
|
|
// Toggles
|
|
const toggleMap = {
|
|
settingCompact: v => { settings.compact = v; document.body.classList.toggle('compact', v); },
|
|
settingShorten: v => { settings.shortenTitles = v; document.body.classList.toggle('shorten-titles', v); },
|
|
settingNewTab: v => { settings.newTab = v; },
|
|
settingShowDesc: v => { settings.showDesc = v; document.body.classList.toggle('show-desc', v); },
|
|
settingHideExtra: v => {
|
|
settings.hideExtra = v;
|
|
document.getElementById('visibleCountRow').classList.toggle('dim', !v);
|
|
renderBoards();
|
|
},
|
|
settingShowSearch: v => {
|
|
settings.showSearch = v;
|
|
document.getElementById('searchBarWrapper').classList.toggle('hidden', !v);
|
|
},
|
|
settingImageRef: v => {
|
|
settings.imageRefEnabled = v;
|
|
const imgBtn = document.querySelector('[data-action="image-ref"]');
|
|
if (imgBtn) imgBtn.classList.toggle('hidden', !v);
|
|
}
|
|
};
|
|
|
|
Object.entries(toggleMap).forEach(([id, fn]) => {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.addEventListener('change', async e => {
|
|
fn(e.target.checked);
|
|
await saveSettings();
|
|
});
|
|
}
|
|
});
|
|
|
|
document.getElementById('settingVisibleCount').addEventListener('change', async e => {
|
|
settings.visibleCount = parseInt(e.target.value, 10);
|
|
await saveSettings();
|
|
renderBoards();
|
|
});
|
|
|
|
// Background URL (im Theme-Modal)
|
|
document.getElementById('btnChangeBg').addEventListener('click', () => {
|
|
document.getElementById('bgInputRow').classList.toggle('hidden');
|
|
});
|
|
document.getElementById('btnApplyBg').addEventListener('click', async () => {
|
|
const url = document.getElementById('bgUrlInput').value.trim();
|
|
if (url && !isValidBgUrl(url)) {
|
|
await HellionDialog.alert(t('settings.bg_invalid_url'), { type: 'danger', title: t('settings.bg_invalid_url.title') });
|
|
return;
|
|
}
|
|
settings.bgUrl = url;
|
|
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
|
|
await saveSettings();
|
|
document.getElementById('bgInputRow').classList.add('hidden');
|
|
});
|
|
|
|
// Background File Upload (im Theme-Modal)
|
|
document.getElementById('btnBgFile').addEventListener('click', () => {
|
|
document.getElementById('bgFileInput').click();
|
|
});
|
|
document.getElementById('bgFileInput').addEventListener('change', e => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = async ev => {
|
|
if (!isValidBgUrl(ev.target.result)) return;
|
|
settings.bgUrl = ev.target.result;
|
|
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
|
|
await saveSettings();
|
|
};
|
|
reader.onerror = () => {
|
|
HellionDialog.alert(t('settings.file_read_error'), { type: 'danger', title: t('settings.file_read_error.title') });
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
// Sprach-Einstellung
|
|
const languageEl = document.getElementById('settingLanguage');
|
|
if (languageEl) {
|
|
languageEl.value = settings.language || 'auto';
|
|
languageEl.addEventListener('change', async (e) => {
|
|
settings.language = e.target.value;
|
|
setLanguage(e.target.value);
|
|
await saveSettings();
|
|
});
|
|
}
|
|
|
|
// Toolbar-Position Setting
|
|
const toolbarPosEl = document.getElementById('settingToolbarPos');
|
|
if (toolbarPosEl) {
|
|
toolbarPosEl.value = settings.toolbarPos || 'right';
|
|
toolbarPosEl.addEventListener('change', async (e) => {
|
|
settings.toolbarPos = e.target.value;
|
|
document.body.classList.toggle('toolbar-left', e.target.value === 'left');
|
|
await saveSettings();
|
|
});
|
|
}
|
|
|
|
// Onboarding wiederholen
|
|
document.getElementById('btnRestartOnboarding').addEventListener('click', () => {
|
|
closeSettings();
|
|
Onboarding.start();
|
|
});
|
|
|
|
// Reset All
|
|
document.getElementById('btnResetAll').addEventListener('click', async () => {
|
|
const ok = await HellionDialog.confirm(
|
|
t('settings.reset_confirm'),
|
|
{ type: 'danger', title: t('settings.reset_confirm.title'), confirmText: t('settings.reset_confirm.button') }
|
|
);
|
|
if (!ok) return;
|
|
boards = [];
|
|
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
|
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
|
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
|
|
imageRefEnabled: false, language: 'auto' };
|
|
await saveBoards();
|
|
await saveSettings();
|
|
setLanguage('auto');
|
|
applySettings();
|
|
renderBoards();
|
|
closeSettings();
|
|
});
|
|
}
|