diff --git a/web/js/components/app-filter-drawer.js b/web/js/components/app-filter-drawer.js index 8c8f4de..9c1e042 100644 --- a/web/js/components/app-filter-drawer.js +++ b/web/js/components/app-filter-drawer.js @@ -194,7 +194,7 @@ export function initFilterDrawer(options = {}) { } }); - document.addEventListener('keydown', (event) => { + const onKeyDown = (event) => { if (!isOpen) {return;} if (event.key === 'Escape') { event.preventDefault(); @@ -225,7 +225,8 @@ export function initFilterDrawer(options = {}) { event.preventDefault(); first.focus(); } - }); + }; + document.addEventListener('keydown', onKeyDown); const setApplyEnabled = (enabled) => { if (!applyButton) {return;} @@ -275,6 +276,13 @@ export function initFilterDrawer(options = {}) { setApplyCount(0); setTriggerCount(0); + const destroy = () => { + document.removeEventListener('keydown', onKeyDown); + if (isOpen) { + close('discard'); + } + }; + return { open, close, @@ -282,6 +290,7 @@ export function initFilterDrawer(options = {}) { drawer, setApplyEnabled, setApplyCount, - setTriggerCount + setTriggerCount, + destroy }; } diff --git a/web/js/components/app-password-toggle.js b/web/js/components/app-password-toggle.js index 616053c..f7ea2a8 100644 --- a/web/js/components/app-password-toggle.js +++ b/web/js/components/app-password-toggle.js @@ -34,7 +34,10 @@ export function initPasswordToggles() { toggle.type = 'button'; toggle.className = 'login-password-toggle'; toggle.setAttribute('aria-label', showLabel); - toggle.innerHTML = ''; + const icon = document.createElement('i'); + icon.className = 'bi bi-eye'; + icon.setAttribute('aria-hidden', 'true'); + toggle.appendChild(icon); input.replaceWith(wrapper); wrapper.appendChild(input); @@ -46,9 +49,7 @@ export function initPasswordToggles() { toggle.addEventListener('click', () => { const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; - toggle.innerHTML = isPassword - ? '' - : ''; + icon.className = isPassword ? 'bi bi-eye-slash' : 'bi bi-eye'; toggle.setAttribute('aria-label', isPassword ? hideLabel : showLabel); input.focus(); }); diff --git a/web/js/components/app-tabs.js b/web/js/components/app-tabs.js index 60ecb59..f02ca4c 100644 --- a/web/js/components/app-tabs.js +++ b/web/js/components/app-tabs.js @@ -15,11 +15,13 @@ */ export function initTabs(root = document) { const containers = root.querySelectorAll('[data-tabs]'); + const instances = []; containers.forEach((container) => { if (container.dataset.tabsBound === '1') {return;} container.dataset.tabsBound = '1'; container.dataset.tabsReady = '0'; + const cleanupFns = []; const tabs = Array.from(container.querySelectorAll('[data-tab]')); const panels = Array.from(container.querySelectorAll('[data-tab-panel]')); @@ -218,7 +220,7 @@ export function initTabs(root = document) { // Intercept submit buttons — both inside the form and external ones linked via form="id". // We use document-level click (capture) so we catch ALL submit triggers before native validation. - document.addEventListener('click', (e) => { + const onFormClickCapture = (e) => { const btn = e.target.closest('button, [type="submit"]'); if (!btn) {return;} @@ -257,9 +259,9 @@ export function initTabs(root = document) { requestAnimationFrame(() => { form.reportValidity(); }); - }, true); + }; - form.addEventListener('input', (e) => { + const onFormInput = (e) => { const panel = e.target.closest('[data-tab-panel]'); if (!panel) {return;} const tabName = panel.dataset.tabPanel; @@ -269,29 +271,41 @@ export function initTabs(root = document) { if (Array.from(fields).every(f => !belongsToForm(f) || f.disabled || f.checkValidity())) { tab.classList.remove('has-invalid'); } - }); + }; - form.addEventListener('change', (e) => { - const panel = e.target.closest('[data-tab-panel]'); - if (!panel) {return;} - const tabName = panel.dataset.tabPanel; - const tab = tabs.find(t => t.dataset.tab === tabName); - if (!tab) {return;} - const fields = panel.querySelectorAll(inputSelector); - if (Array.from(fields).every(f => !belongsToForm(f) || f.disabled || f.checkValidity())) { - tab.classList.remove('has-invalid'); - } + document.addEventListener('click', onFormClickCapture, true); + form.addEventListener('input', onFormInput); + form.addEventListener('change', onFormInput); + + cleanupFns.push(() => { + document.removeEventListener('click', onFormClickCapture, true); + form.removeEventListener('input', onFormInput); + form.removeEventListener('change', onFormInput); }); } // Listen for popstate (back/forward navigation) - window.addEventListener('popstate', () => { + const onPopState = () => { const newTab = getUrlParam(urlParamName); if (newTab && panels.some(p => p.dataset.tabPanel === newTab)) { activateTab(newTab); } - }); + }; + window.addEventListener('popstate', onPopState); + cleanupFns.push(() => window.removeEventListener('popstate', onPopState)); + + const destroy = () => { + cleanupFns.forEach((fn) => fn()); + cleanupFns.length = 0; + delete container.dataset.tabsBound; + delete container.dataset.tabsReady; + }; + + container._tabsApi = { activateTab, destroy }; + instances.push(container._tabsApi); }); + + return instances; } // ------------------------------------------------------------------ @@ -359,7 +373,10 @@ function createChevronBtn(direction, iconClass) { btn.className = `app-tabs-chevron app-tabs-chevron--${direction}`; btn.setAttribute('aria-hidden', 'true'); btn.setAttribute('tabindex', '-1'); - btn.innerHTML = ``; + const icon = document.createElement('i'); + icon.className = `bi ${iconClass}`; + icon.setAttribute('aria-hidden', 'true'); + btn.appendChild(icon); return btn; } diff --git a/web/js/components/app-toggle-aside-panels.js b/web/js/components/app-toggle-aside-panels.js index b8a0c1a..00e12b5 100644 --- a/web/js/components/app-toggle-aside-panels.js +++ b/web/js/components/app-toggle-aside-panels.js @@ -202,7 +202,7 @@ export function initAsidePanels(options = {}) { }); }); - document.addEventListener('keydown', (event) => { + const onKeyDown = (event) => { if (!(event.metaKey || event.ctrlKey)) {return;} if (event.altKey) {return;} if (isEditableTarget(event.target)) {return;} @@ -212,8 +212,14 @@ export function initAsidePanels(options = {}) { if (index < 0 || index >= shortcutItems.length) {return;} event.preventDefault(); shortcutItems[index].click(); - }); + }; + document.addEventListener('keydown', onKeyDown); + const destroy = () => { + document.removeEventListener('keydown', onKeyDown); + }; + + return { setActive, destroy }; } if (document.readyState === 'loading') {