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') {