feat(obsidian): add collapsible folder tree for browsing notes

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!
This commit is contained in:
Claude 2025-11-14 04:29:56 +00:00
parent 154145a518
commit 5cb4aeb599
No known key found for this signature in database
2 changed files with 89 additions and 20 deletions

View file

@ -8282,7 +8282,7 @@
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
<script defer src="modules/io/export.js?v=1.108.11"></script>
<script defer src="modules/io/obsidian-bridge.js?v=1.108.13.3"></script>
<script defer src="modules/ui/obsidian-notes-editor.js?v=1.108.13.3"></script>
<script defer src="modules/ui/obsidian-notes-editor.js?v=1.108.13.4"></script>
<script defer src="modules/ui/obsidian-config.js?v=1.108.13"></script>
<script defer src="modules/renderers/draw-features.js?v=1.108.2"></script>

View file

@ -310,27 +310,14 @@ async function promptCreateNewNote(elementId, elementType, coordinates) {
return;
}
resultsDiv.innerHTML = allNotes
.map(
(note, index) => `
<div class="browse-result" data-index="${index}" style="
padding: 8px;
margin: 4px 0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background: white;
" onmouseover="this.style.background='#e8e8e8'" onmouseout="this.style.background='white'">
<div style="font-weight: bold;">${note.name}</div>
<div style="font-size: 0.85em; color: #666;">${note.path}</div>
</div>
`
)
.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 = `<p style='color: red;'>Failed to load notes: ${error.message}</p>`;
}
@ -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 += `
<div style="margin-left: ${indentPx}px;">
<div style="padding: 4px; cursor: pointer; user-select: none;">
<span class="tree-folder-toggle" style="display: inline-block; width: 16px; font-size: 12px;"></span>
<span style="font-weight: bold;">📁 ${folderName}</span>
</div>
<div class="tree-folder-content" style="display: block;">
${renderFolderTree(folderData, allNotes, indent + 1)}
</div>
</div>
`;
}
// Render files in current folder
html += renderFiles(node.files || [], indent);
return html;
}
function renderFiles(files, indent) {
const indentPx = indent * 20;
return files
.map(
file => `
<div class="tree-file" data-index="${file.index}" style="
margin-left: ${indentPx}px;
padding: 4px 8px;
cursor: pointer;
border-radius: 3px;
" onmouseover="this.style.background='#e8e8e8'" onmouseout="this.style.background='transparent'">
<span style="font-size: 12px;">📄</span> ${file.name.replace(".md", "")}
</div>
`
)
.join("");
}
function getElementData(elementId, elementType) {
// Extract element data based on type
if (elementType === "burg") {