diff --git a/assets/tree-sidebar.js b/assets/tree-sidebar.js index 707ab01..079daf7 100644 --- a/assets/tree-sidebar.js +++ b/assets/tree-sidebar.js @@ -84,7 +84,8 @@ var childrenEl = null var loaded = false var isOpen = false - var isLeaf = false + var isLeaf = !entry.hasChildren + if (isLeaf) chevron.classList.add("is-leaf") function ensureChildrenEl() { if (!childrenEl) { diff --git a/tree.go b/tree.go index 1fe623a..40d9035 100644 --- a/tree.go +++ b/tree.go @@ -12,6 +12,10 @@ import ( type treeEntry struct { Name string `json:"name"` Kind string `json:"kind"` + // HasChildren reports whether a folder has any visible entries (under the + // same foldersOnly filter), so the client can hide the expand chevron on + // empty folders without a round-trip. Always false for files. + HasChildren bool `json:"hasChildren"` // Children is populated only along the expandTo chain (see handleTree); // omitted otherwise so the flat picker listing keeps its original shape. Children []treeEntry `json:"children,omitempty"` @@ -81,7 +85,11 @@ func listTreeEntries(fsPath string, foldersOnly bool) ([]treeEntry, error) { continue } if e.IsDir() { - folders = append(folders, treeEntry{Name: name, Kind: "folder"}) + folders = append(folders, treeEntry{ + Name: name, + Kind: "folder", + HasChildren: folderHasChildren(filepath.Join(fsPath, name), foldersOnly), + }) } else { if foldersOnly || name == "index.md" { continue @@ -94,6 +102,33 @@ func listTreeEntries(fsPath string, foldersOnly bool) ([]treeEntry, error) { return append(folders, files...), nil } +// folderHasChildren reports whether fsPath contains at least one entry that +// listTreeEntries would list. It reads the directory in small batches and +// stops at the first match, so large folders stay cheap. Unreadable folders +// report false. +func folderHasChildren(fsPath string, foldersOnly bool) bool { + f, err := os.Open(fsPath) + if err != nil { + return false + } + defer f.Close() + for { + batch, err := f.ReadDir(32) + for _, e := range batch { + name := e.Name() + if strings.HasPrefix(name, ".") { + continue + } + if e.IsDir() || (!foldersOnly && name != "index.md") { + return true + } + } + if err != nil { + return false + } + } +} + // treePathSegments splits a wiki path into its non-empty segments. func treePathSegments(p string) []string { var segs []string