From b14941b3e0f7689794aa7cedfba5f474fba0e2c8 Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 7 Sep 2026 16:23:38 +0200 Subject: [PATCH] Adjust sidebar tree --- assets/tree-sidebar.js | 379 +++++++++++++++++++++-------------------- tree.go | 22 ++- 2 files changed, 207 insertions(+), 194 deletions(-) diff --git a/assets/tree-sidebar.js b/assets/tree-sidebar.js index 0d4413b..707ab01 100644 --- a/assets/tree-sidebar.js +++ b/assets/tree-sidebar.js @@ -1,200 +1,215 @@ -(function () { - // This script owns the .tree-scroll child of the aside, not the aside itself. - var container = document.querySelector('aside.tree-sidebar .tree-scroll'); - if (!container) return; +;(function () { + // This script owns the .tree-scroll child of the aside, not the aside itself. + var container = document.querySelector("aside.tree-sidebar .tree-scroll") + if (!container) return - function joinPath(parent, name) { - if (parent === '/' || parent === '') return '/' + name; - return parent.replace(/\/+$/, '') + '/' + name; + function joinPath(parent, name) { + if (parent === "/" || parent === "") return "/" + name + return parent.replace(/\/+$/, "") + "/" + name + } + + function encodeSegments(p) { + return p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/") + } + + function folderHref(p) { + if (p === "/" || p === "") return "/" + return "/" + encodeSegments(p) + "/" + } + + function fetchFolder(path, expandTo) { + var url = folderHref(path) + "?tree=1&foldersOnly=1" + if (expandTo) url += "&expandTo=" + encodeURIComponent(expandTo) + return fetch(url, { credentials: "same-origin" }).then(function (r) { + if (!r.ok) throw new Error("HTTP " + r.status) + return r.json() + }) + } + + // Current page as a canonical, DECODED wiki path ("/" for root, no trailing + // slash otherwise). pathname is percent-encoded; the tree's entry names (and + // thus fullPath) are decoded, so we must decode here too or paths containing + // spaces / non-ASCII never match a row and the chain never expands. + var activePath = (function () { + var p = window.location.pathname.replace(/\/+$/, "") + if (p === "") return "/" + try { + return decodeURIComponent(p) + } catch (e) { + return p + } + })() + var activeRow = null + + function disabledRow(text) { + var row = document.createElement("div") + row.className = "tree-row is-disabled" + row.textContent = text + return row + } + + // renderInto fills containerEl with rows for `entries` (children of + // parentPath). Folders carrying a `children` array (the pre-expanded + // ancestor chain from ?expandTo) are opened immediately and recurse. + function renderInto(containerEl, parentPath, entries) { + entries.forEach(function (entry) { + var built = buildRow(parentPath, entry) + containerEl.appendChild(built.row) + if (built.preExpand) built.open() + }) + } + + function buildRow(parentPath, entry) { + var fullPath = joinPath(parentPath, entry.name) + var row = document.createElement("div") + row.className = "tree-row" + + var chevron = document.createElement("span") + chevron.className = "tree-chevron" + chevron.textContent = "▸" + row.appendChild(chevron) + + var link = document.createElement("a") + link.className = "tree-name tree-folder" + link.textContent = entry.name + link.href = folderHref(fullPath) + row.appendChild(link) + + if (fullPath === activePath) { + row.classList.add("is-active") + activeRow = row } - function encodeSegments(p) { - return p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'); + var preChildren = entry.children ? entry.children : null + var childrenEl = null + var loaded = false + var isOpen = false + var isLeaf = false + + function ensureChildrenEl() { + if (!childrenEl) { + childrenEl = document.createElement("div") + childrenEl.className = "tree-children" + } + return childrenEl } - function folderHref(p) { - if (p === '/' || p === '') return '/'; - return '/' + encodeSegments(p) + '/'; + function markLeaf() { + isLeaf = true + isOpen = false + chevron.textContent = "▸" + chevron.classList.add("is-leaf") + if (childrenEl && childrenEl.parentNode) { + childrenEl.parentNode.removeChild(childrenEl) + } } - function fileHref(p) { - return '/' + encodeSegments(p); + function loadChildren() { + var el = ensureChildrenEl() + el.textContent = "" + el.appendChild(disabledRow("…")) + fetchFolder(fullPath) + .then(function (resp) { + el.textContent = "" + loaded = true + if (!resp.entries || resp.entries.length === 0) { + markLeaf() + return + } + renderInto(el, fullPath, resp.entries) + }) + .catch(function () { + el.textContent = "" + var err = document.createElement("div") + err.className = "tree-row" + err.textContent = "(failed — tap to retry)" + err.addEventListener("click", function (e) { + e.stopPropagation() + loadChildren() + }) + el.appendChild(err) + }) } - function fetchFolder(path, expandTo) { - var url = folderHref(path) + '?tree=1'; - if (expandTo) url += '&expandTo=' + encodeURIComponent(expandTo); - return fetch(url, { credentials: 'same-origin' }).then(function (r) { - if (!r.ok) throw new Error('HTTP ' + r.status); - return r.json(); - }); - } - - // Current page as a canonical, DECODED wiki path ("/" for root, no trailing - // slash otherwise). pathname is percent-encoded; the tree's entry names (and - // thus fullPath) are decoded, so we must decode here too or paths containing - // spaces / non-ASCII never match a row and the chain never expands. - var activePath = (function () { - var p = window.location.pathname.replace(/\/+$/, ''); - if (p === '') return '/'; - try { return decodeURIComponent(p); } catch (e) { return p; } - })(); - var activeRow = null; - - function disabledRow(text) { - var row = document.createElement('div'); - row.className = 'tree-row is-disabled'; - row.textContent = text; - return row; - } - - // renderInto fills containerEl with rows for `entries` (children of - // parentPath). Folders carrying a `children` array (the pre-expanded - // ancestor chain from ?expandTo) are opened immediately and recurse. - function renderInto(containerEl, parentPath, entries) { - if (!entries || entries.length === 0) { - containerEl.appendChild(disabledRow('(empty)')); - return; + function open() { + if (isOpen || isLeaf) return + var el = ensureChildrenEl() + row.parentNode.insertBefore(el, row.nextSibling) + chevron.textContent = "▾" // ▾ + isOpen = true + if (!loaded) { + if (preChildren) { + renderInto(el, fullPath, preChildren) + loaded = true + } else { + loadChildren() } - entries.forEach(function (entry) { - var built = buildRow(parentPath, entry); - containerEl.appendChild(built.row); - if (built.preExpand) built.open(); - }); + } } - function buildRow(parentPath, entry) { - var fullPath = joinPath(parentPath, entry.name); - var isFolder = entry.kind === 'folder'; - - var row = document.createElement('div'); - row.className = 'tree-row'; - - var chevron = document.createElement('span'); - chevron.className = 'tree-chevron'; - if (isFolder) chevron.textContent = '▸'; // ▸ - else chevron.classList.add('is-leaf'); - row.appendChild(chevron); - - - var link = document.createElement('a'); - link.className = 'tree-name ' + (isFolder ? 'tree-folder' : 'tree-file'); - link.textContent = entry.name; - link.href = isFolder ? folderHref(fullPath) : fileHref(fullPath); - row.appendChild(link); - - if (isFolder && fullPath === activePath) { - row.classList.add('is-active'); - activeRow = row; - } - - var preChildren = (isFolder && entry.children) ? entry.children : null; - var childrenEl = null; - var loaded = false; - var isOpen = false; - - function ensureChildrenEl() { - if (!childrenEl) { - childrenEl = document.createElement('div'); - childrenEl.className = 'tree-children'; - } - return childrenEl; - } - - function loadChildren() { - var el = ensureChildrenEl(); - el.textContent = ''; - el.appendChild(disabledRow('…')); - fetchFolder(fullPath).then(function (resp) { - el.textContent = ''; - renderInto(el, fullPath, resp.entries); - loaded = true; - }).catch(function () { - el.textContent = ''; - var err = document.createElement('div'); - err.className = 'tree-row'; - err.textContent = '(failed — tap to retry)'; - err.addEventListener('click', function (e) { - e.stopPropagation(); - loadChildren(); - }); - el.appendChild(err); - }); - } - - function open() { - if (!isFolder || isOpen) return; - var el = ensureChildrenEl(); - row.parentNode.insertBefore(el, row.nextSibling); - chevron.textContent = '▾'; // ▾ - isOpen = true; - if (!loaded) { - if (preChildren) { - renderInto(el, fullPath, preChildren); - loaded = true; - } else { - loadChildren(); - } - } - } - - function close() { - if (!isFolder || !isOpen) return; - if (childrenEl && childrenEl.parentNode) { - childrenEl.parentNode.removeChild(childrenEl); - } - chevron.textContent = '▸'; - isOpen = false; - } - - // Clicking the name link navigates (folder) or opens the file; clicking - // anywhere else on the row toggles expansion for folders. - row.addEventListener('click', function (e) { - if (e.target.closest('a.tree-name')) return; - if (!isFolder) return; - if (isOpen) close(); else open(); - }); - - return { row: row, open: open, preExpand: !!preChildren }; + function close() { + if (!isOpen) return + if (childrenEl && childrenEl.parentNode) { + childrenEl.parentNode.removeChild(childrenEl) + } + chevron.textContent = "▸" + isOpen = false } - function render() { - container.textContent = ''; - var listing = document.createElement('div'); - container.appendChild(listing); - listing.appendChild(disabledRow('…')); + // Clicking the name link navigates (folder) or opens the file; clicking + // anywhere else on the row toggles expansion for folders. + row.addEventListener("click", function (e) { + if (e.target.closest("a.tree-name")) return + if (isOpen) close() + else open() + }) - fetchFolder('/', activePath === '/' ? '' : activePath).then(function (resp) { - listing.textContent = ''; - renderInto(listing, '/', resp.entries); - if (activeRow) { - try { activeRow.scrollIntoView({ block: 'nearest' }); } catch (e) {} - } - }).catch(function () { - listing.textContent = ''; - var err = document.createElement('div'); - err.className = 'tree-row'; - err.textContent = '(failed — tap to retry)'; - err.addEventListener('click', function () { render(); }); - listing.appendChild(err); - }); - } + return { row: row, open: open, preExpand: !!preChildren } + } - function setupMenuItem() { - var specials = document.querySelector('.actions-special'); - if (!specials) return; + function render() { + container.textContent = "" + var listing = document.createElement("div") + container.appendChild(listing) + listing.appendChild(disabledRow("…")) - var item = document.createElement("button"); - item.type = "button"; - item.className = "btn btn-block"; - item.title = "Folders"; - item.textContent = "FOLDERS"; - item.addEventListener("click", function () { - if (typeof openOverlay === "function") openOverlay(container); - }); - specials.appendChild(item); - } + fetchFolder("/", activePath === "/" ? "" : activePath) + .then(function (resp) { + listing.textContent = "" + renderInto(listing, "/", resp.entries) + if (activeRow) { + try { + activeRow.scrollIntoView({ block: "nearest" }) + } catch (e) {} + } + }) + .catch(function () { + listing.textContent = "" + var err = document.createElement("div") + err.className = "tree-row" + err.textContent = "(failed — tap to retry)" + err.addEventListener("click", function () { + render() + }) + listing.appendChild(err) + }) + } - render(); - setupMenuItem(); -})(); + function setupMenuItem() { + var specials = document.querySelector(".actions-special") + if (!specials) return + + var item = document.createElement("button") + item.type = "button" + item.className = "btn btn-block" + item.title = "Folders" + item.textContent = "FOLDERS" + item.addEventListener("click", function () { + if (typeof openOverlay === "function") openOverlay(container) + }) + specials.appendChild(item) + } + + render() + setupMenuItem() +})() diff --git a/tree.go b/tree.go index 3226522..1fe623a 100644 --- a/tree.go +++ b/tree.go @@ -40,18 +40,16 @@ func (h *handler) handleTree(w http.ResponseWriter, r *http.Request, urlPath, fs return } - entries, err := listTreeEntries(fsPath) + foldersOnly := r.URL.Query().Has("foldersOnly") + + entries, err := listTreeEntries(fsPath, foldersOnly) if err != nil { http.Error(w, "read failed", http.StatusInternalServerError) return } - // expandTo asks for a nested listing: each folder along the ancestor chain - // carries its own children, recursively, down to the target. Used by the - // tree sidebar to render the current page's chain in one request. The flat - // picker omits expandTo and is unaffected. if expandTo := r.URL.Query().Get("expandTo"); expandTo != "" { - expandTreeChain(fsPath, entries, treePathSegments(expandTo)) + expandTreeChain(fsPath, entries, treePathSegments(expandTo), foldersOnly) } resp := treeResponse{Path: canonicalTreePath(urlPath), Entries: entries} @@ -70,8 +68,8 @@ func canonicalTreePath(urlPath string) string { // listTreeEntries returns the immediate children of fsPath, filtering hidden // entries and index.md. Folders are listed before files; both groups are -// sorted alphabetically. -func listTreeEntries(fsPath string) ([]treeEntry, error) { +// sorted alphabetically. foldersOnly will not list any individual files at all. +func listTreeEntries(fsPath string, foldersOnly bool) ([]treeEntry, error) { raw, err := os.ReadDir(fsPath) if err != nil { return nil, err @@ -85,7 +83,7 @@ func listTreeEntries(fsPath string) ([]treeEntry, error) { if e.IsDir() { folders = append(folders, treeEntry{Name: name, Kind: "folder"}) } else { - if name == "index.md" { + if foldersOnly || name == "index.md" { continue } files = append(files, treeEntry{Name: name, Kind: "file"}) @@ -113,7 +111,7 @@ func treePathSegments(p string) []string { // far as it still exists. Segments only ever match real directory names from // listTreeEntries (no "." or ".." entries), so this cannot traverse outside the // listed tree. -func expandTreeChain(fsPath string, entries []treeEntry, segs []string) { +func expandTreeChain(fsPath string, entries []treeEntry, segs []string, foldersOnly bool) { if len(segs) == 0 { return } @@ -122,12 +120,12 @@ func expandTreeChain(fsPath string, entries []treeEntry, segs []string) { continue } childFs := filepath.Join(fsPath, segs[0]) - kids, err := listTreeEntries(childFs) + kids, err := listTreeEntries(childFs, foldersOnly) if err != nil { return } entries[i].Children = kids - expandTreeChain(childFs, entries[i].Children, segs[1:]) + expandTreeChain(childFs, entries[i].Children, segs[1:], foldersOnly) return } }