major update
This commit is contained in:
86
web/js/components/app-active-filter-chips.js
Normal file
86
web/js/components/app-active-filter-chips.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
|
||||
export function initActiveFilterChips(options = {}) {
|
||||
const {
|
||||
container,
|
||||
removeButtonLabel = 'Remove filter',
|
||||
clearAllLabel = 'Clear all filters',
|
||||
onRemove = null,
|
||||
onClearAll = null
|
||||
} = options;
|
||||
|
||||
const root = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bindEvents = () => {
|
||||
const removeButtons = Array.from(root.querySelectorAll('[data-active-filter-remove]'));
|
||||
removeButtons.forEach((button) => {
|
||||
if (button.dataset.bound === '1') {return;}
|
||||
button.dataset.bound = '1';
|
||||
button.addEventListener('click', () => {
|
||||
if (typeof onRemove !== 'function') {return;}
|
||||
onRemove({
|
||||
id: button.dataset.activeFilterId || '',
|
||||
param: button.dataset.activeFilterParam || '',
|
||||
value: button.dataset.activeFilterValue || '',
|
||||
valueType: button.dataset.activeFilterValueType || 'scalar',
|
||||
meta: button.dataset.activeFilterMeta || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const clearButton = root.querySelector('[data-active-filter-clear]');
|
||||
if (clearButton && clearButton.dataset.bound !== '1') {
|
||||
clearButton.dataset.bound = '1';
|
||||
clearButton.addEventListener('click', () => {
|
||||
if (typeof onClearAll === 'function') {
|
||||
onClearAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const render = (chips = []) => {
|
||||
if (!Array.isArray(chips) || chips.length === 0) {
|
||||
root.innerHTML = '';
|
||||
root.hidden = true;
|
||||
return;
|
||||
}
|
||||
root.hidden = false;
|
||||
|
||||
const list = chips.map((chip) => {
|
||||
const id = String(chip?.id ?? '');
|
||||
const param = String(chip?.param ?? '');
|
||||
const value = String(chip?.value ?? '');
|
||||
const valueType = String(chip?.valueType ?? 'scalar');
|
||||
const label = String(chip?.label ?? '');
|
||||
const meta = String(chip?.meta ?? '');
|
||||
return `<li>
|
||||
<button type="button" class="app-filter-chip" data-active-filter-remove data-active-filter-id="${escapeHtml(id)}" data-active-filter-param="${escapeHtml(param)}" data-active-filter-value="${escapeHtml(value)}" data-active-filter-value-type="${escapeHtml(valueType)}" data-active-filter-meta="${escapeHtml(meta)}" aria-label="${escapeHtml(removeButtonLabel)}">
|
||||
<span>${escapeHtml(label)}</span>
|
||||
<i class="bi bi-x-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
const clearChip = `<li>
|
||||
<button type="button" class="app-filter-chip app-filter-chip-clear" data-active-filter-clear aria-label="${escapeHtml(clearAllLabel)}">
|
||||
<span>${escapeHtml(clearAllLabel)}</span>
|
||||
<i class="bi bi-x-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</li>`;
|
||||
|
||||
root.innerHTML = `<div class="app-active-filters-inner"><ul class="app-filter-chip-list">${list}${clearChip}</ul></div>`;
|
||||
bindEvents();
|
||||
};
|
||||
|
||||
return { render };
|
||||
}
|
||||
70
web/js/components/app-confirm-actions.js
Normal file
70
web/js/components/app-confirm-actions.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const CONFIRM_ATTR = 'data-confirm-message';
|
||||
|
||||
const isSubmitControl = (element) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
if (element instanceof HTMLButtonElement) {
|
||||
const type = String(element.getAttribute('type') || 'submit').trim().toLowerCase();
|
||||
return type === '' || type === 'submit';
|
||||
}
|
||||
if (element instanceof HTMLInputElement) {
|
||||
const type = String(element.type || '').trim().toLowerCase();
|
||||
return type === 'submit' || type === 'image';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const submitterByForm = new Map();
|
||||
|
||||
const onClickCapture = (event) => {
|
||||
const trigger = event.target instanceof HTMLElement ? event.target.closest('a, button, input') : null;
|
||||
if (!(trigger instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubmitControl(trigger)) {
|
||||
const form = trigger.form instanceof HTMLFormElement ? trigger.form : null;
|
||||
if (form) {
|
||||
submitterByForm.set(form, trigger);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = String(trigger.getAttribute(CONFIRM_ATTR) || '').trim();
|
||||
if (confirmMessage === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitCapture = (event) => {
|
||||
const form = event.target instanceof HTMLFormElement ? event.target : null;
|
||||
if (!(form instanceof HTMLFormElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitter = event.submitter instanceof HTMLElement
|
||||
? event.submitter
|
||||
: (submitterByForm.get(form) || null);
|
||||
submitterByForm.delete(form);
|
||||
|
||||
const submitterMessage = String(submitter?.getAttribute(CONFIRM_ATTR) || '').trim();
|
||||
const formMessage = String(form.getAttribute(CONFIRM_ATTR) || '').trim();
|
||||
const confirmMessage = submitterMessage !== '' ? submitterMessage : formMessage;
|
||||
|
||||
if (confirmMessage === '') {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', onClickCapture, true);
|
||||
document.addEventListener('submit', onSubmitCapture, true);
|
||||
@@ -1,23 +1,6 @@
|
||||
const readStored = (storageKey) => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
|
||||
|
||||
const writeStored = (storageKey, openKeys) => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(openKeys));
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
const controllerRegistry = new WeakMap();
|
||||
|
||||
const initDetailsGroup = (group) => {
|
||||
if (!group || group.dataset.detailsStorageBound === '1') {
|
||||
@@ -29,42 +12,15 @@ const initDetailsGroup = (group) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const managedDetails = Array.from(group.querySelectorAll('details[data-details-key]'));
|
||||
const alwaysOpenDetails = Array.from(group.querySelectorAll('details[data-details-always-open]'));
|
||||
|
||||
if (!managedDetails.length && !alwaysOpenDetails.length) {
|
||||
const controller = initPersistedDetailsGroup({
|
||||
root: group,
|
||||
storageKey,
|
||||
});
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedOpenKeys = readStored(storageKey);
|
||||
if (storedOpenKeys.length > 0) {
|
||||
managedDetails.forEach((details) => {
|
||||
const key = (details.dataset.detailsKey || '').trim();
|
||||
if (key !== '') {
|
||||
details.open = storedOpenKeys.includes(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
alwaysOpenDetails.forEach((details) => {
|
||||
details.open = true;
|
||||
details.addEventListener('toggle', () => {
|
||||
if (!details.open) {
|
||||
details.open = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
managedDetails.forEach((details) => {
|
||||
details.addEventListener('toggle', () => {
|
||||
const openKeys = managedDetails
|
||||
.filter((item) => item.open)
|
||||
.map((item) => (item.dataset.detailsKey || '').trim())
|
||||
.filter(Boolean);
|
||||
writeStored(storageKey, openKeys);
|
||||
});
|
||||
});
|
||||
|
||||
controllerRegistry.set(group, controller);
|
||||
group.dataset.detailsStorageBound = '1';
|
||||
};
|
||||
|
||||
|
||||
284
web/js/components/app-filter-drawer.js
Normal file
284
web/js/components/app-filter-drawer.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initFilterDrawer(options = {}) {
|
||||
const {
|
||||
root = document,
|
||||
drawerSelector = '[data-filter-drawer]',
|
||||
openSelector = '[data-filter-drawer-open]',
|
||||
closeSelector = '[data-filter-drawer-close]',
|
||||
applySelector = '[data-filter-drawer-apply]',
|
||||
resetSelector = '[data-filter-drawer-reset]',
|
||||
onOpen = null,
|
||||
onClose = null,
|
||||
onApply = null,
|
||||
onReset = null,
|
||||
onDiscard = null
|
||||
} = options;
|
||||
|
||||
const host = root && typeof root.querySelector === 'function' ? root : document;
|
||||
const drawer = host.querySelector(drawerSelector);
|
||||
if (!drawer) {
|
||||
warnOnce('UI_EL_MISSING', `Missing filter drawer: ${drawerSelector}`, { module: 'filter-drawer' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const openButtons = Array.from(host.querySelectorAll(openSelector));
|
||||
const closeButtons = Array.from(drawer.querySelectorAll(closeSelector));
|
||||
const applyButton = drawer.querySelector(applySelector);
|
||||
const resetButton = drawer.querySelector(resetSelector);
|
||||
const applyBaseLabel = applyButton ? String(applyButton.textContent || '').trim() : '';
|
||||
const openButtonBaseLabels = new WeakMap();
|
||||
|
||||
const panel = drawer.querySelector('[data-filter-drawer-panel]') || drawer;
|
||||
|
||||
let isOpen = false;
|
||||
let lockedScrollY = 0;
|
||||
let lastTrigger = null;
|
||||
|
||||
if (!drawer.hasAttribute('role')) {
|
||||
drawer.setAttribute('role', 'dialog');
|
||||
}
|
||||
if (!drawer.hasAttribute('aria-modal')) {
|
||||
drawer.setAttribute('aria-modal', 'true');
|
||||
}
|
||||
drawer.setAttribute('aria-hidden', drawer.hidden ? 'true' : 'false');
|
||||
if (!panel.hasAttribute('tabindex')) {
|
||||
panel.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
const lockPageScroll = () => {
|
||||
const body = document.body;
|
||||
if (body.dataset.filterDrawerLock === '1') {return;}
|
||||
|
||||
lockedScrollY = window.scrollY || window.pageYOffset || 0;
|
||||
body.dataset.filterDrawerLock = '1';
|
||||
body.style.position = 'fixed';
|
||||
body.style.top = `-${lockedScrollY}px`;
|
||||
body.style.left = '0';
|
||||
body.style.right = '0';
|
||||
body.style.width = '100%';
|
||||
};
|
||||
|
||||
const unlockPageScroll = () => {
|
||||
const body = document.body;
|
||||
if (body.dataset.filterDrawerLock !== '1') {return;}
|
||||
|
||||
body.style.position = '';
|
||||
body.style.top = '';
|
||||
body.style.left = '';
|
||||
body.style.right = '';
|
||||
body.style.width = '';
|
||||
delete body.dataset.filterDrawerLock;
|
||||
window.scrollTo(0, lockedScrollY);
|
||||
};
|
||||
|
||||
const getFocusableElements = () => {
|
||||
const selectors = [
|
||||
'a[href]',
|
||||
'area[href]',
|
||||
'input:not([type="hidden"]):not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'button:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])'
|
||||
];
|
||||
|
||||
return Array.from(panel.querySelectorAll(selectors.join(','))).filter((element) => {
|
||||
if (!element) {return false;}
|
||||
if (element.getAttribute('aria-hidden') === 'true') {return false;}
|
||||
if (element instanceof HTMLElement && element.hidden) {return false;}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const focusFirstField = () => {
|
||||
const focusable = getFocusableElements()[0] || panel;
|
||||
if (focusable && typeof focusable.focus === 'function') {
|
||||
focusable.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const setOpen = (nextOpen) => {
|
||||
isOpen = nextOpen;
|
||||
drawer.hidden = !nextOpen;
|
||||
drawer.setAttribute('aria-hidden', nextOpen ? 'false' : 'true');
|
||||
drawer.classList.toggle('is-open', nextOpen);
|
||||
document.body.classList.toggle('filter-drawer-open', nextOpen);
|
||||
if (nextOpen) {
|
||||
lockPageScroll();
|
||||
} else {
|
||||
unlockPageScroll();
|
||||
}
|
||||
openButtons.forEach((button) => {
|
||||
button.setAttribute('aria-expanded', nextOpen ? 'true' : 'false');
|
||||
});
|
||||
if (!nextOpen) {
|
||||
if (lastTrigger && typeof lastTrigger.focus === 'function' && document.contains(lastTrigger)) {
|
||||
lastTrigger.focus();
|
||||
}
|
||||
lastTrigger = null;
|
||||
if (typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof onOpen === 'function') {
|
||||
onOpen();
|
||||
}
|
||||
focusFirstField();
|
||||
};
|
||||
|
||||
const open = (trigger = null) => {
|
||||
if (isOpen) {return;}
|
||||
lastTrigger = trigger && typeof trigger.focus === 'function'
|
||||
? trigger
|
||||
: (document.activeElement instanceof HTMLElement ? document.activeElement : null);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const close = (mode = 'discard') => {
|
||||
if (!isOpen) {return;}
|
||||
if (mode === 'discard' && typeof onDiscard === 'function') {
|
||||
onDiscard();
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
openButtons.forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
open(button);
|
||||
});
|
||||
});
|
||||
|
||||
closeButtons.forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
close('discard');
|
||||
});
|
||||
});
|
||||
|
||||
if (applyButton) {
|
||||
applyButton.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
if (applyButton.disabled) {
|
||||
return;
|
||||
}
|
||||
let shouldClose = true;
|
||||
if (typeof onApply === 'function') {
|
||||
const result = await onApply();
|
||||
shouldClose = result !== false;
|
||||
}
|
||||
if (shouldClose) {
|
||||
close('apply');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
if (typeof onReset === 'function') {
|
||||
await onReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawer.addEventListener('click', (event) => {
|
||||
if (!isOpen) {return;}
|
||||
if (!panel.contains(event.target)) {
|
||||
close('discard');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!isOpen) {return;}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
close('discard');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Tab') {return;}
|
||||
const focusable = getFocusableElements();
|
||||
if (!focusable.length) {
|
||||
event.preventDefault();
|
||||
panel.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = document.activeElement;
|
||||
if (event.shiftKey) {
|
||||
if (active === first || !panel.contains(active)) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (active === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const setApplyEnabled = (enabled) => {
|
||||
if (!applyButton) {return;}
|
||||
const nextEnabled = enabled === true;
|
||||
applyButton.disabled = !nextEnabled;
|
||||
applyButton.setAttribute('aria-disabled', nextEnabled ? 'false' : 'true');
|
||||
};
|
||||
|
||||
const setApplyCount = (count) => {
|
||||
if (!applyButton) {return;}
|
||||
const parsed = Number.parseInt(String(count ?? '0'), 10);
|
||||
const normalized = Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
applyButton.textContent = normalized > 0
|
||||
? `${applyBaseLabel} (${normalized})`
|
||||
: applyBaseLabel;
|
||||
};
|
||||
|
||||
const setTriggerCount = (count) => {
|
||||
const parsed = Number.parseInt(String(count ?? '0'), 10);
|
||||
const normalized = Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
|
||||
openButtons.forEach((button) => {
|
||||
const baseLabel = openButtonBaseLabels.get(button)
|
||||
|| String(button.getAttribute('aria-label') || button.textContent || '').trim();
|
||||
|
||||
if (!baseLabel) {return;}
|
||||
if (!openButtonBaseLabels.has(button)) {
|
||||
openButtonBaseLabels.set(button, baseLabel);
|
||||
}
|
||||
|
||||
const nextLabel = normalized > 0 ? `${baseLabel} (${normalized})` : baseLabel;
|
||||
const icon = button.querySelector('i');
|
||||
if (icon) {
|
||||
while (icon.nextSibling) {
|
||||
icon.parentNode?.removeChild(icon.nextSibling);
|
||||
}
|
||||
icon.insertAdjacentText('afterend', ` ${nextLabel}`);
|
||||
} else {
|
||||
button.textContent = nextLabel;
|
||||
}
|
||||
button.setAttribute('aria-label', nextLabel);
|
||||
button.setAttribute('data-tooltip', nextLabel);
|
||||
});
|
||||
};
|
||||
|
||||
setApplyEnabled(false);
|
||||
setApplyCount(0);
|
||||
setTriggerCount(0);
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
isOpen: () => isOpen,
|
||||
drawer,
|
||||
setApplyEnabled,
|
||||
setApplyCount,
|
||||
setTriggerCount
|
||||
};
|
||||
}
|
||||
@@ -86,7 +86,24 @@ export const initMultiSelect = async (target = '[data-multi-select]') => {
|
||||
syncInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
isSyncing = false;
|
||||
};
|
||||
instance.element.addEventListener('multi-select:change', syncValues);
|
||||
let boundElement = null;
|
||||
const bindSyncListener = () => {
|
||||
if (!instance?.element || instance.element === boundElement) {
|
||||
return;
|
||||
}
|
||||
instance.element.addEventListener('multi-select:change', syncValues);
|
||||
boundElement = instance.element;
|
||||
};
|
||||
bindSyncListener();
|
||||
if (typeof instance.refresh === 'function' && instance._syncRefreshWrapped !== true) {
|
||||
const originalRefresh = instance.refresh.bind(instance);
|
||||
instance.refresh = (...args) => {
|
||||
const result = originalRefresh(...args);
|
||||
bindSyncListener();
|
||||
return result;
|
||||
};
|
||||
instance._syncRefreshWrapped = true;
|
||||
}
|
||||
const values = instance.selectedValues || [];
|
||||
syncInput.value = Array.isArray(values) ? values.join(',') : '';
|
||||
syncInput.addEventListener('change', () => {
|
||||
|
||||
11
web/js/components/app-standard-detail-page.js
Normal file
11
web/js/components/app-standard-detail-page.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { autoInitStandardDetailPages } from '../core/app-detail-page-factory.js';
|
||||
|
||||
export function initStandardDetailPages(root = document) {
|
||||
return autoInitStandardDetailPages(root);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initStandardDetailPages());
|
||||
} else {
|
||||
initStandardDetailPages();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
|
||||
|
||||
export function initAsidePanels(options = {}) {
|
||||
const {
|
||||
@@ -43,51 +44,34 @@ export function initAsidePanels(options = {}) {
|
||||
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 || panel.dataset.detailsBound === '1') {return;}
|
||||
if (!panel) {return;}
|
||||
const existing = panelDetailsRegistry.get(panel);
|
||||
if (existing) {
|
||||
existing.apply();
|
||||
return;
|
||||
}
|
||||
|
||||
const storageKey = panel.dataset.asideDetailsStorage || '';
|
||||
if (!storageKey) {return;}
|
||||
const detailsEls = Array.from(panel.querySelectorAll('details[data-details-key]'));
|
||||
if (!detailsEls.length) {return;}
|
||||
|
||||
const readStored = () => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (value) => {
|
||||
try {
|
||||
const list = Array.isArray(value) ? value : [];
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(list));
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const applyStored = () => {
|
||||
const stored = readStored();
|
||||
if (!stored.length) {return;}
|
||||
detailsEls.forEach((details) => {
|
||||
details.open = stored.includes(details.dataset.detailsKey || '');
|
||||
});
|
||||
};
|
||||
|
||||
detailsEls.forEach((details) => {
|
||||
details.addEventListener('toggle', () => {
|
||||
const openKeys = detailsEls
|
||||
.filter((item) => item.open)
|
||||
.map((item) => item.dataset.detailsKey || '')
|
||||
.filter(Boolean);
|
||||
writeStored(openKeys);
|
||||
});
|
||||
const openActive = panel.dataset.asideDetailsOpenActive === '1';
|
||||
const detailsState = initPersistedDetailsGroup({
|
||||
root: panel,
|
||||
storageKey,
|
||||
ensureOpenKeys: openActive ? () => findActiveDetailsKeys(panel) : []
|
||||
});
|
||||
|
||||
applyStored();
|
||||
panel.dataset.detailsBound = '1';
|
||||
if (!detailsState) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelDetailsRegistry.set(panel, detailsState);
|
||||
};
|
||||
|
||||
const readStored = () => {
|
||||
|
||||
Reference in New Issue
Block a user