From 5cb4aeb5998e445ad72f2b4d764a7be6e237a874 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 04:29:56 +0000 Subject: [PATCH] feat(obsidian): add collapsible folder tree for browsing notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced flat list with hierarchical folder tree structure for better navigation when browsing vault notes. **Features:** - Collapsible folders with ▼/▶ toggle arrows - Proper indentation showing folder hierarchy - 📁 folder and 📄 file icons - Click folder name to expand/collapse - Click file to select it - Handles root-level files and nested folders - Hover highlights for files **Functions added:** - buildFolderTree(): Converts flat note list to tree structure - renderFolderTree(): Recursively renders folders with nesting - renderFiles(): Renders files at current folder level Perfect for vaults organized like: ``` State1/ Province1/ City1.md City2.md Province2/ City3.md State2/ Province3/ City4.md ``` Much easier to navigate than a flat list of 100+ notes! --- index.html | 2 +- modules/ui/obsidian-notes-editor.js | 107 +++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/index.html b/index.html index 68ad5fae..1e60bf17 100644 --- a/index.html +++ b/index.html @@ -8282,7 +8282,7 @@ - + diff --git a/modules/ui/obsidian-notes-editor.js b/modules/ui/obsidian-notes-editor.js index 60b93386..07500096 100644 --- a/modules/ui/obsidian-notes-editor.js +++ b/modules/ui/obsidian-notes-editor.js @@ -310,27 +310,14 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { return; } - resultsDiv.innerHTML = allNotes - .map( - (note, index) => ` -
-
${note.name}
-
${note.path}
-
- ` - ) - .join(""); + // Build folder tree + const tree = buildFolderTree(allNotes); + resultsDiv.innerHTML = renderFolderTree(tree, allNotes); - // Add click handlers - document.querySelectorAll(".browse-result").forEach((el, index) => { + // Add click handlers to files + document.querySelectorAll(".tree-file").forEach(el => { el.addEventListener("click", async () => { + const index = parseInt(el.dataset.index); $("#alert").dialog("close"); try { const note = allNotes[index]; @@ -346,6 +333,17 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { } }); }); + + // Add click handlers to folder toggles + document.querySelectorAll(".tree-folder-toggle").forEach(el => { + el.addEventListener("click", e => { + e.stopPropagation(); + const folder = el.parentElement.nextElementSibling; + const isCollapsed = folder.style.display === "none"; + folder.style.display = isCollapsed ? "block" : "none"; + el.textContent = isCollapsed ? "▼" : "▶"; + }); + }); } catch (error) { resultsDiv.innerHTML = `

Failed to load notes: ${error.message}

`; } @@ -359,6 +357,77 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { }); } +function buildFolderTree(notes) { + const root = {folders: {}, files: []}; + + notes.forEach((note, index) => { + const parts = note.path.split("/"); + const fileName = parts[parts.length - 1]; + + if (parts.length === 1) { + // Root level file + root.files.push({name: fileName, index, path: note.path}); + } else { + // Navigate/create folder structure + let current = root; + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + if (!current.folders[folderName]) { + current.folders[folderName] = {folders: {}, files: []}; + } + current = current.folders[folderName]; + } + // Add file to final folder + current.files.push({name: fileName, index, path: note.path}); + } + }); + + return root; +} + +function renderFolderTree(node, allNotes, indent = 0) { + let html = ""; + const indentPx = indent * 20; + + // Render folders + for (const [folderName, folderData] of Object.entries(node.folders || {})) { + html += ` +
+
+ + 📁 ${folderName} +
+
+ ${renderFolderTree(folderData, allNotes, indent + 1)} +
+
+ `; + } + + // Render files in current folder + html += renderFiles(node.files || [], indent); + + return html; +} + +function renderFiles(files, indent) { + const indentPx = indent * 20; + return files + .map( + file => ` +
+ 📄 ${file.name.replace(".md", "")} +
+ ` + ) + .join(""); +} + function getElementData(elementId, elementType) { // Extract element data based on type if (elementType === "burg") {