diff --git a/package.json b/package.json index f4eb2c5..3324ff7 100644 --- a/package.json +++ b/package.json @@ -7,4 +7,4 @@ "devDependencies": { "marked": "^16.3.0" } -} \ No newline at end of file +} diff --git a/web/js/tree.js b/web/js/tree.js index 1efef34..7b60074 100644 --- a/web/js/tree.js +++ b/web/js/tree.js @@ -1,5 +1,7 @@ import { getSortedKeys } from './sort.js'; +const STORAGE_KEY = 'tree-state'; + let treeContainer = null; let onSelectionChangeHandler = null; @@ -124,6 +126,7 @@ function createTreeNode(name, value, path) { event.stopPropagation(); childrenContainer.classList.toggle('collapsed'); toggle.textContent = childrenContainer.classList.contains('collapsed') ? '▶' : '▼'; + saveTreeState(); }); container.appendChild(toggle); @@ -213,6 +216,7 @@ function handleCheckboxChange(event) { if (typeof onSelectionChangeHandler === 'function') { onSelectionChangeHandler(); } + saveTreeState(); } function updateParentCheckboxes(checkbox) { @@ -250,3 +254,76 @@ function updateParentCheckboxes(checkbox) { parentItem = currentItem.parentElement.closest('.tree-item'); } } + +function saveTreeState() { + if (!treeContainer) { + return; + } + + const checkedPaths = Array.from( + treeContainer.querySelectorAll('input[type="checkbox"]:checked[data-has-files="1"]') + ).map(cb => cb.dataset.path).filter(Boolean); + + const collapsedPaths = Array.from( + treeContainer.querySelectorAll('.tree-children.collapsed') + ).map(children => { + const cb = children.parentElement.querySelector(':scope > label > input[type="checkbox"]'); + return cb?.dataset.path ?? null; + }).filter(Boolean); + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify({ checkedPaths, collapsedPaths })); + } catch { + // Storage unavailable or quota exceeded — silently ignore + } +} + +export function restoreTreeState() { + if (!treeContainer) { + return; + } + + let state; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return; + } + state = JSON.parse(raw); + } catch { + return; + } + + if (!state || typeof state !== 'object') { + return; + } + + if (Array.isArray(state.checkedPaths) && state.checkedPaths.length > 0) { + const checkedSet = new Set(state.checkedPaths); + treeContainer.querySelectorAll('input[type="checkbox"][data-has-files="1"]').forEach(cb => { + cb.checked = checkedSet.has(cb.dataset.path); + }); + treeContainer.querySelectorAll('input[type="checkbox"][data-has-files="1"]:checked').forEach(cb => { + updateParentCheckboxes(cb); + }); + } + + if (Array.isArray(state.collapsedPaths)) { + const collapsedSet = new Set(state.collapsedPaths); + treeContainer.querySelectorAll('.tree-children').forEach(childrenDiv => { + const cb = childrenDiv.parentElement.querySelector(':scope > label > input[type="checkbox"]'); + if (!cb) { + return; + } + const toggle = childrenDiv.parentElement.querySelector(':scope > .tree-toggle'); + const shouldBeCollapsed = collapsedSet.has(cb.dataset.path); + const isCollapsed = childrenDiv.classList.contains('collapsed'); + if (shouldBeCollapsed !== isCollapsed) { + childrenDiv.classList.toggle('collapsed'); + if (toggle) { + toggle.textContent = shouldBeCollapsed ? '▶' : '▼'; + } + } + }); + } +} diff --git a/web/script.js b/web/script.js index a6d1ace..bca5edb 100644 --- a/web/script.js +++ b/web/script.js @@ -1,5 +1,5 @@ import { loadIssuesForCheckedPaths } from './js/issues.js'; -import { getCheckedFileRefs, initializeTree, renderProductTree } from './js/tree.js'; +import { getCheckedFileRefs, initializeTree, renderProductTree, restoreTreeState } from './js/tree.js'; import { applyIssueSearchFilter, getIssueTypeFilters, initializeUI } from './js/ui.js'; document.addEventListener('DOMContentLoaded', () => { @@ -24,6 +24,8 @@ function loadProductTree() { .then(data => { productsData = expandFilePrefixLevel(data); renderProductTree(productsData); + restoreTreeState(); + refreshIssuesForCurrentSelection(); }) .catch(error => console.error('Error loading products:', error)); }