From 76658d7f53cf3ebbedbf748ad94a6a4ec56ca622 Mon Sep 17 00:00:00 2001 From: "Eric Chen (from Dev Box)" Date: Fri, 5 Jun 2026 05:20:35 -0500 Subject: [PATCH] Restore collapsed-by-default location dropdown The location/section dropdown now defaults to collapsed, expanding only the notebook and section groups along the path to the currently selected section. This restores the original picker behavior and makes long notebook lists navigable again. - flattenSections/flattenSectionGroups report whether their subtree holds the selected section so each heading collapses unless it is on the path - refreshSectionVisibility recomputes row visibility in a single pass that honors nested collapse state - default to the first available section (and reveal its path) when there is no stored selection or it no longer exists Co-Authored-By: Claude Opus 4.8 (1M context) --- src/scripts/renderer.ts | 118 +++++++++++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/src/scripts/renderer.ts b/src/scripts/renderer.ts index 637f0152..8f0f468c 100644 --- a/src/scripts/renderer.ts +++ b/src/scripts/renderer.ts @@ -523,13 +523,15 @@ function populateSectionDropdown() { curSectionId = cur.section ? cur.section.id : ""; } catch (e) { /* ignore */ } } + // Fall back to the first section so the dropdown opens to a visible, expanded path. + let hasStoredSelection = !!curSectionId && sectionExists(notebooks, curSectionId); + if (!hasStoredSelection) { + curSectionId = findFirstSectionId(notebooks); + } flattenSections(notebooks, curSectionId); - // If no section was selected, select the first one - if (!selectedSectionId) { - let first = sectionList.querySelector("li") as HTMLElement; - if (first) { - selectSection(first.getAttribute("data-id") || "", first.textContent || ""); - } + if (!hasStoredSelection && selectedSectionId) { + let sel = sectionList.querySelector(".section-item-selected") as HTMLElement; + if (sel) { selectSection(sel.getAttribute("data-id") || "", sel.title || sel.textContent || ""); } } } catch (e) { sectionSelected.textContent = loc("WebClipper.SectionPicker.NotebookLoadFailureMessage", "Error loading notebooks"); @@ -537,33 +539,66 @@ function populateSectionDropdown() { } } +// Collapse every heading except those on the path to the selected section; +// returns true when this subtree contains it so ancestors can stay expanded. function flattenSections(notebooks: any[], preselectedId: string) { for (let nb of notebooks) { - addNotebookHeading(nb.name); + let heading = addNotebookHeading(nb.name); + let onPath = false; if (nb.sections) { for (let sec of nb.sections) { addSectionItem(sec.id, sec.name, nb.name + " > " + sec.name, preselectedId, 1); + if (preselectedId && sec.id === preselectedId) { onPath = true; } } } if (nb.sectionGroups) { - flattenSectionGroups(nb.sectionGroups, nb.name, preselectedId, 1); + if (flattenSectionGroups(nb.sectionGroups, nb.name, preselectedId, 1)) { onPath = true; } } + setHeadingCollapsed(heading, !onPath); } + refreshSectionVisibility(); } -function flattenSectionGroups(groups: any[], parentPath: string, preselectedId: string, depth: number) { +function flattenSectionGroups(groups: any[], parentPath: string, preselectedId: string, depth: number): boolean { + let anyOnPath = false; for (let group of groups) { let path = parentPath + " > " + group.name; - addGroupHeading(group.name, depth); + let heading = addGroupHeading(group.name, depth); + let onPath = false; if (group.sections) { for (let sec of group.sections) { addSectionItem(sec.id, sec.name, path + " > " + sec.name, preselectedId, depth + 1); + if (preselectedId && sec.id === preselectedId) { onPath = true; } } } if (group.sectionGroups) { - flattenSectionGroups(group.sectionGroups, path, preselectedId, depth + 1); + if (flattenSectionGroups(group.sectionGroups, path, preselectedId, depth + 1)) { onPath = true; } } + setHeadingCollapsed(heading, !onPath); + if (onPath) { anyOnPath = true; } } + return anyOnPath; +} + +function findFirstSectionId(nodes: any[]): string { + for (let node of nodes) { + if (node.sections) { + for (let sec of node.sections) { if (sec.id) { return sec.id; } } + } + if (node.sectionGroups) { + let id = findFirstSectionId(node.sectionGroups); + if (id) { return id; } + } + } + return ""; +} + +function sectionExists(nodes: any[], id: string): boolean { + for (let node of nodes) { + if (node.sections?.some((sec: any) => sec.id === id)) { return true; } + if (node.sectionGroups && sectionExists(node.sectionGroups, id)) { return true; } + } + return false; } // Walk to the next/previous visible row in the section list. @@ -578,6 +613,33 @@ function focusAdjacentSectionRow(from: HTMLElement, dir: 1 | -1) { } } +// Sets collapsed state only; callers run refreshSectionVisibility() afterward. +function setHeadingCollapsed(li: HTMLElement, collapsed: boolean) { + li.classList.toggle("collapsed", collapsed); + li.setAttribute("aria-expanded", collapsed ? "false" : "true"); + let arrow = li.querySelector(".collapse-arrow") as HTMLImageElement | null; + if (arrow) { arrow.src = collapsed ? "images/arrow_right.png" : "images/arrow_down.png"; } +} + +// Hide rows under any collapsed ancestor, tracking the shallowest collapsed depth. +function refreshSectionVisibility() { + let hideDeeperThan = Infinity; + let rows = sectionList.children; + for (let i = 0; i < rows.length; i++) { + let row = rows[i] as HTMLElement; + let depth = parseInt(row.getAttribute("data-depth") || "0", 10); + if (depth > hideDeeperThan) { + row.style.display = "none"; + continue; + } + row.style.display = ""; + hideDeeperThan = Infinity; + if (row.classList.contains("section-heading") && row.classList.contains("collapsed")) { + hideDeeperThan = depth; + } + } +} + function makeCollapsibleHeading(li: HTMLLIElement, depth: number) { li.setAttribute("data-depth", "" + depth); // Reachable via arrow-key nav within the listbox; tab order is the dropdown trigger @@ -593,19 +655,8 @@ function makeCollapsibleHeading(li: HTMLLIElement, depth: number) { li.insertBefore(arrow, li.firstChild); li.setAttribute("aria-expanded", "true"); let toggle = () => { - let collapsed = li.classList.toggle("collapsed"); - li.setAttribute("aria-expanded", collapsed ? "false" : "true"); - arrow.src = collapsed ? "images/arrow_right.png" : "images/arrow_down.png"; - // Toggle visibility of sibling items until next heading at same or shallower depth - let next = li.nextElementSibling as HTMLElement; - while (next) { - if (next.classList.contains("section-heading")) { - let nextDepth = parseInt(next.getAttribute("data-depth") || "0", 10); - if (nextDepth <= depth) { break; } // same or higher level — stop - } - next.style.display = collapsed ? "none" : ""; - next = next.nextElementSibling as HTMLElement; - } + setHeadingCollapsed(li, !li.classList.contains("collapsed")); + refreshSectionVisibility(); }; li.addEventListener("click", toggle); li.addEventListener("keydown", (e) => { @@ -644,6 +695,7 @@ function addNotebookHeading(name: string) { li.appendChild(span); makeCollapsibleHeading(li, 0); sectionList.appendChild(li); + return li; } function addGroupHeading(name: string, depth: number) { @@ -659,12 +711,14 @@ function addGroupHeading(name: string, depth: number) { li.appendChild(span); makeCollapsibleHeading(li, depth); sectionList.appendChild(li); + return li; } function addSectionItem(id: string, displayName: string, fullPath: string, preselectedId: string, depth: number) { let li = document.createElement("li"); li.className = "section-item"; li.setAttribute("data-id", id); + li.setAttribute("data-depth", "" + depth); li.setAttribute("role", "option"); li.setAttribute("tabindex", "-1"); setRowIndent(li, depth); @@ -877,15 +931,13 @@ async function fetchFreshNotebooks() { let previousSectionId = selectedSectionId; sectionList.innerHTML = ""; selectedSectionId = ""; - flattenSections(freshNotebooks, previousSectionId); - - // If previously selected section no longer exists, select the first one - let sectionStillExists = !!selectedSectionId; - if (!selectedSectionId) { - let first = sectionList.querySelector("li") as HTMLElement; - if (first) { - selectSection(first.getAttribute("data-id") || "", first.textContent || ""); - } + // Keep the prior selection when it still exists; otherwise fall back to the first section. + let sectionStillExists = !!previousSectionId && sectionExists(freshNotebooks, previousSectionId); + let targetSectionId = sectionStillExists ? previousSectionId : findFirstSectionId(freshNotebooks); + flattenSections(freshNotebooks, targetSectionId); + if (!sectionStillExists && selectedSectionId) { + let sel = sectionList.querySelector(".section-item-selected") as HTMLElement; + if (sel) { selectSection(sel.getAttribute("data-id") || "", sel.title || sel.textContent || ""); } } getNotebooksEvent.setStatus(Status.Succeeded); getNotebooksEvent.setCustomProperty(PropertyName.Custom.CurrentSectionStillExists, sectionStillExists);