Files
breadcrumb-the-shire/web/js/components/app-toggle-aside-panels.js
fs d9f07dcd63 fix: web/ quick wins — CSS cleanup, ARIA tabs, and file header comments
CSS:
- Remove duplicate li::before block in app-search.css
- Fix typo: search-reuslt → search-result in app-search.css
- Remove commented-out CSS rules in app-flash.css
- Add descriptive header comment to all 27 CSS component files

JS:
- Complete WAI-ARIA Tabs pattern: generate IDs, add aria-controls on
  tab buttons and aria-labelledby on tab panels (app-tabs.js)
- Add JSDoc header comments to ~33 JS files (core + components)
- Add explanatory block comment to app-boot.js (why classic IIFE)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:21:37 +01:00

229 lines
7.6 KiB
JavaScript

/**
* Aside panel switcher — opens/closes named sidebar panels.
*/
import { requireEl, warnOnce } from '../core/app-dom.js';
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
export function initAsidePanels(options = {}) {
const {
barSelector = '.aside-icon-bar',
panelSelector = '.app-sidebar-panel',
titleSelector = '.app-sidebar-title',
toolsSelector = '[data-aside-tools]',
revealSidebarOnActivate = true,
persistSidebarStateOnActivate = true
} = options;
const bar = requireEl(barSelector, { module: 'aside-panels' });
if (!bar) {return;}
const tabs = Array.from(bar.querySelectorAll('[data-aside-target]'));
if (!tabs.length) {
warnOnce('UI_EL_MISSING', 'No aside tabs found', { module: 'aside-panels' });
return;
}
const shortcutItems = Array.from(bar.querySelectorAll('[data-aside-target], [data-aside-shortcut]'));
const panels = Array.from(document.querySelectorAll(panelSelector));
if (!panels.length) {
warnOnce('UI_EL_MISSING', 'No aside panels found', { module: 'aside-panels' });
}
const titleEl = document.querySelector(titleSelector);
const toolsHost = document.querySelector(toolsSelector);
const sidebarController = window.AppSidebar || null;
const defaultTitle = titleEl?.dataset.asideTitleDefault || titleEl?.textContent || '';
const storageKey = bar.dataset.asideStorage || '';
const isMac = /(Mac|iPhone|iPad|iPod)/i.test(navigator.platform || navigator.userAgent);
const shortcutPrefix = isMac ? '⌘' : 'Ctrl+';
const getPanelKey = (panel) => panel?.dataset?.asidePanel || '';
const getTabKey = (tab) => tab?.dataset?.asideTarget || '';
const getPanelTools = (panel) => panel?.querySelector('[data-aside-panel-tools]') || null;
const getTabHref = (tab) => tab?.dataset?.asideHref || '';
const isEditableTarget = (target) => {
if (!target) {return false;}
if (target.isContentEditable) {return true;}
const tag = target.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
};
const panelDetailsRegistry = new WeakMap();
const findActiveDetailsKeys = (panel) => Array.from(panel.querySelectorAll('details[data-details-key]'))
.filter((details) => details.querySelector('a.active, a[aria-current="page"]'))
.map((details) => details.dataset.detailsKey || '')
.filter(Boolean);
const initPanelDetails = (panel) => {
if (!panel) {return;}
const existing = panelDetailsRegistry.get(panel);
if (existing) {
existing.apply();
return;
}
const storageKey = panel.dataset.asideDetailsStorage || '';
if (!storageKey) {return;}
const openActive = panel.dataset.asideDetailsOpenActive === '1';
const detailsState = initPersistedDetailsGroup({
root: panel,
storageKey,
ensureOpenKeys: openActive ? () => findActiveDetailsKeys(panel) : []
});
if (!detailsState) {
return;
}
panelDetailsRegistry.set(panel, detailsState);
};
const readStored = () => {
if (!storageKey) {return null;}
try {
return window.localStorage.getItem(storageKey);
} catch (error) {
return null;
}
};
const writeStored = (key) => {
if (!storageKey) {return;}
try {
window.localStorage.setItem(storageKey, key);
} catch (error) {
// ignore storage errors
}
};
const syncTools = (panel) => {
if (!toolsHost) {return;}
toolsHost.innerHTML = '';
const tools = getPanelTools(panel);
if (!tools) {return;}
toolsHost.innerHTML = tools.innerHTML;
};
const openSidebar = (persist) => {
if (!revealSidebarOnActivate || !sidebarController) {return;}
if (typeof sidebarController.isHidden === 'function' && sidebarController.isHidden()) {
sidebarController.show({ persist });
}
if (!sidebarController.isCollapsed()) {return;}
sidebarController.open({ persist });
};
const setActive = (key, { fromUser = false } = {}) => {
if (!key) {return;}
let activePanel = null;
panels.forEach((panel) => {
const isActive = getPanelKey(panel) === key;
if (isActive) {activePanel = panel;}
panel.hidden = !isActive;
panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
});
tabs.forEach((tab) => {
const isActive = getTabKey(tab) === key;
tab.classList.toggle('active', isActive);
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
tab.setAttribute('tabindex', isActive ? '0' : '-1');
});
if (titleEl) {
const nextTitle = activePanel?.dataset?.asideTitle || defaultTitle;
if (nextTitle) {
titleEl.textContent = nextTitle;
}
}
if (activePanel) {
syncTools(activePanel);
initPanelDetails(activePanel);
}
writeStored(key);
if (fromUser) {
openSidebar(persistSidebarStateOnActivate);
}
};
const getInitialKey = () => {
const stored = readStored();
if (stored && panels.some((panel) => getPanelKey(panel) === stored)) {
return stored;
}
const activeTab = tabs.find((tab) => tab.classList.contains('active'));
if (activeTab) {return getTabKey(activeTab);}
const visiblePanel = panels.find((panel) => !panel.hidden);
if (visiblePanel) {return getPanelKey(visiblePanel);}
return getTabKey(tabs[0]);
};
setActive(getInitialKey(), { fromUser: false });
document.documentElement.classList.remove('aside-pending');
window.AppAsidePanels = {
setActive,
open: (key) => setActive(key, { fromUser: true })
};
shortcutItems.forEach((item, idx) => {
const base = item.dataset.tooltipBase || item.getAttribute('data-tooltip') || item.getAttribute('aria-label') || '';
if (!base) {return;}
const shortcutKey = idx === 9 ? '0' : String(idx + 1);
item.dataset.tooltipBase = base;
item.setAttribute('data-tooltip', `${base} (${shortcutPrefix}${shortcutKey})`);
});
tabs.forEach((tab) => {
tab.addEventListener('click', (event) => {
event.preventDefault();
const key = getTabKey(tab);
const isActive = tab.classList.contains('active');
const href = getTabHref(tab);
if (href) {
const base = document.baseURI || window.location.origin;
const targetUrl = new URL(href, base);
const currentUrl = new URL(window.location.href);
const isSame = targetUrl.pathname === currentUrl.pathname && targetUrl.search === currentUrl.search;
if (!isSame) {
setActive(key, { fromUser: true });
window.location.href = targetUrl.toString();
return;
}
}
if (sidebarController && sidebarController.isCollapsed()) {
sidebarController.open({ persist: persistSidebarStateOnActivate });
setActive(key, { fromUser: true });
return;
}
if (isActive && sidebarController) {
if (sidebarController.isHidden?.()) {
sidebarController.show({ persist: true });
} else if (typeof sidebarController.hide === 'function') {
sidebarController.hide({ persist: true });
}
return;
}
setActive(key, { fromUser: true });
});
});
document.addEventListener('keydown', (event) => {
if (!(event.metaKey || event.ctrlKey)) {return;}
if (event.altKey) {return;}
if (isEditableTarget(event.target)) {return;}
const key = event.key;
if (!/^[0-9]$/.test(key)) {return;}
const index = key === '0' ? 9 : (Number.parseInt(key, 10) - 1);
if (index < 0 || index >= shortcutItems.length) {return;}
event.preventDefault();
shortcutItems[index].click();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initAsidePanels());
} else {
initAsidePanels();
}