Files
kashilo/js/components/app-header.js
2026-01-28 15:29:36 +01:00

209 lines
8.7 KiB
JavaScript

import { i18n, t } from '../i18n.js';
import { router } from '../router.js';
class AppHeader extends HTMLElement {
constructor() {
super();
this.langDropdownOpen = false;
this.handleOutsideClick = this.handleOutsideClick.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
}
connectedCallback() {
this.render();
this.setupEventListeners();
document.addEventListener('click', this.handleOutsideClick);
document.addEventListener('keydown', this.handleKeydown);
}
disconnectedCallback() {
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleKeydown);
}
handleOutsideClick() {
if (this.langDropdownOpen) {
this.closeDropdown();
}
}
handleKeydown(e) {
if (!this.langDropdownOpen) return;
const items = Array.from(this.querySelectorAll('.dropdown-item'));
const currentIndex = items.findIndex(item => item === document.activeElement);
switch (e.key) {
case 'Escape':
e.preventDefault();
this.closeDropdown();
this.querySelector('#lang-toggle')?.focus();
break;
case 'ArrowDown':
e.preventDefault();
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
items[nextIndex]?.focus();
break;
case 'ArrowUp':
e.preventDefault();
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
items[prevIndex]?.focus();
break;
}
}
closeDropdown() {
this.langDropdownOpen = false;
const langDropdown = this.querySelector('#lang-dropdown');
const langToggle = this.querySelector('#lang-toggle');
langDropdown?.classList.remove('open');
langToggle?.setAttribute('aria-expanded', 'false');
}
openDropdown() {
this.langDropdownOpen = true;
const langDropdown = this.querySelector('#lang-dropdown');
const langToggle = this.querySelector('#lang-toggle');
langDropdown?.classList.add('open');
langToggle?.setAttribute('aria-expanded', 'true');
this.querySelector('.dropdown-item.active')?.focus();
}
render() {
this.innerHTML = /* html */`
<div class="header-inner container">
<a href="#/" class="logo" aria-label="dgray.io Home">
<img src="assets/logo-light.svg" alt="dgray.io" class="logo-img logo-light" width="100" height="28">
<img src="assets/logo-dark.svg" alt="dgray.io" class="logo-img logo-dark" width="100" height="28">
</a>
<div class="header-actions">
<a href="#/create" class="btn btn-primary btn-create" title="${t('header.createListing')}" data-i18n-title="header.createListing">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
<span class="btn-create-text" data-i18n="header.createListing">${t('header.createListing')}</span>
</a>
<button
class="btn btn-icon btn-outline"
id="theme-toggle"
data-i18n-title="header.toggleTheme"
title="${t('header.toggleTheme')}"
>
<svg class="icon-sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg class="icon-moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<div class="dropdown" id="lang-dropdown">
<button
class="btn btn-outline"
id="lang-toggle"
aria-haspopup="listbox"
aria-expanded="${this.langDropdownOpen}"
aria-label="${t('header.selectLanguage')}"
>
<span id="current-lang">${i18n.getLocale().toUpperCase()}</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="dropdown-menu" role="listbox" aria-label="${t('header.selectLanguage')}">
${i18n.getSupportedLocales().map(locale => `
<button
class="dropdown-item ${locale === i18n.getLocale() ? 'active' : ''}"
data-locale="${locale}"
role="option"
aria-selected="${locale === i18n.getLocale()}"
>
${i18n.getLocaleDisplayName(locale)}
</button>
`).join('')}
</div>
</div>
</div>
</div>
`;
}
setupEventListeners() {
const themeToggle = this.querySelector('#theme-toggle');
themeToggle.addEventListener('click', () => this.toggleTheme());
const langDropdown = this.querySelector('#lang-dropdown');
const langToggle = this.querySelector('#lang-toggle');
langToggle.addEventListener('click', (e) => {
e.stopPropagation();
if (this.langDropdownOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
});
langDropdown.querySelectorAll('[data-locale]').forEach(btn => {
btn.addEventListener('click', async () => {
await i18n.setLocale(btn.dataset.locale);
this.closeDropdown();
this.render();
this.setupEventListeners();
});
});
this.updateThemeIcon();
}
toggleTheme() {
const currentTheme = document.documentElement.dataset.theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let newTheme;
if (!currentTheme) {
newTheme = prefersDark ? 'light' : 'dark';
} else if (currentTheme === 'dark') {
newTheme = 'light';
} else {
newTheme = 'dark';
}
document.documentElement.dataset.theme = newTheme;
localStorage.setItem('theme', newTheme);
this.updateThemeIcon();
}
updateThemeIcon() {
const theme = document.documentElement.dataset.theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = theme === 'dark' || (!theme && prefersDark);
const sunIcon = this.querySelector('.icon-sun');
const moonIcon = this.querySelector('.icon-moon');
if (sunIcon && moonIcon) {
sunIcon.style.display = isDark ? 'block' : 'none';
moonIcon.style.display = isDark ? 'none' : 'block';
}
}
updateTranslations() {
i18n.updateDOM();
this.querySelector('#current-lang').textContent = i18n.getLocale().toUpperCase();
}
}
customElements.define('app-header', AppHeader);