From ea59a325441d90ef32bb5459d519c5b1a2f5a526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Randall=20Havens=20=E2=96=B3=20The=20Empathic=20Tec?= =?UTF-8?q?hnologist=20=E2=9F=81=20Doctor=20Who=2042?= Date: Sat, 8 Nov 2025 11:23:44 -0600 Subject: [PATCH] Update app.js --- public/app.js | 281 +++++++++++++++++++------------------------------- 1 file changed, 105 insertions(+), 176 deletions(-) diff --git a/public/app.js b/public/app.js index 00ce5a1..fcb3c3e 100755 --- a/public/app.js +++ b/public/app.js @@ -1,191 +1,120 @@ -// Elements -const sidebar = document.getElementById("sidebar"); -const treeEl = document.getElementById("tree"); -const metaEl = document.getElementById("meta"); -const mdView = document.getElementById("mdView"); -const htmlView = document.getElementById("htmlView"); -const errorBox = document.getElementById("errorBox"); -const sortSel = document.getElementById("sortSel"); -const filterSel = document.getElementById("filterSel"); -const searchBox = document.getElementById("searchBox"); -const navToggle = document.getElementById("navToggle"); -const backdrop = document.getElementById("backdrop"); +let INDEX, CURRENT_PATH=null, PATH_TO_EL=new Map(); +const treeEl=document.getElementById("tree"); +const mdView=document.getElementById("mdView"); +const iframe=document.getElementById("htmlView"); +const metaLine=document.getElementById("meta"); +const sortSel=document.getElementById("sort"); +const filterSel=document.getElementById("filter"); +const searchBox=document.getElementById("search"); +const prevBtn=document.getElementById("prev"); +const nextBtn=document.getElementById("next"); -// State -let INDEX = null; -let CURRENT_PATH = null; -let PATH_TO_EL = new Map(); - -// Drawer controls (mobile) -navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); -backdrop.addEventListener("click", () => sidebar.classList.remove("open")); - -// -------- Boot -------- -window.addEventListener("DOMContentLoaded", async () => { - await loadIndex(); - if (!INDEX) return; - - sortSel.addEventListener("change", rebuildTree); - filterSel.addEventListener("change", rebuildTree); - searchBox.addEventListener("input", rebuildTree); - - window.addEventListener("popstate", () => { - const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; - if (hp) openPath(hp, {push:false}); - }); - - const initial = location.hash.startsWith("#=") ? location.hash.slice(2) : null; - if (initial) openPath(initial, {push:false}); - else autoOpenLatest(); -}); - -// -------- Data loading -------- -async function loadIndex() { - try{ - const res = await fetch("index.json",{cache:"no-store"}); - INDEX = await res.json(); - rebuildTree(); - }catch(e){ - treeEl.innerHTML = "

index.json missing. run the build.

"; +async function loadIndex(){ + const res=await fetch("/index.json",{cache:"no-store"}); + INDEX=await res.json(); + populateFilters(); + rebuildTree(); + window.addEventListener("popstate",()=>{const hp=location.hash.slice(2);if(hp)openPath(hp);}); + const init=location.hash.slice(2)||INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0].path; + openPath(init); +} +function populateFilters(){ + const cats=new Set(INDEX.sections); + filterSel.innerHTML=''; + for(const c of cats){const o=document.createElement("option");o.value=c;o.textContent=c;filterSel.appendChild(o);} +} +function rebuildTree(){ + treeEl.innerHTML=""; + const filter=filterSel.value; + const sort=sortSel.value; + const query=searchBox.value.toLowerCase(); + for(const dir of INDEX.tree){ + if(filter!=="all"&&dir.name!==filter)continue; + treeEl.appendChild(renderNode(dir,sort,query)); } } - -// -------- Tree building / sorting / filtering -------- -function rebuildTree() { - if (!INDEX) return; - PATH_TO_EL.clear(); - treeEl.innerHTML = ""; - - const sort = sortSel.value; - const filter = filterSel.value; - const query = searchBox.value.trim().toLowerCase(); - - const roots = INDEX.tree - .filter(d => filter==="all" || d.name===filter) - .map(d => deepClone(d)); - - roots.forEach(r => { - applySort(r, sort); - const filtered = applySearch(r, query); - if (filtered) treeEl.appendChild(renderNode(filtered)); - }); - - // Auto-expand top dirs - treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open")); -} - -function deepClone(obj){ return JSON.parse(JSON.stringify(obj)); } -function getDate(n){ return n.mtime || 0; } - -function applySort(dir, sort) { - if (dir.type !== "dir") return; - const cmp = - sort==="alpha" ? (a,b)=> (a.title||a.name).localeCompare(b.title||b.name) : - sort==="old" ? (a,b)=> getDate(a)-getDate(b) : - (a,b)=> getDate(b)-getDate(a); - dir.children.sort((a,b)=>{ - if (a.type!==b.type){ return a.type==="dir" ? -1 : 1; } // dirs first - return cmp(a,b); - }); - dir.children.forEach(c => c.type==="dir" && applySort(c, sort)); -} - -function applySearch(node, q) { - if (!q) return node; - if (node.type==="file"){ - const t = (node.title||node.name).toLowerCase(); - return t.includes(q)? node : null; +function renderNode(node,sort,query){ + if(node.type==="dir"){ + const div=document.createElement("div"); + div.className="dir";div.setAttribute("aria-expanded","false"); + const lbl=document.createElement("span"); + lbl.className="label";lbl.textContent=node.name; + lbl.addEventListener("click",()=>{ + const idx=node.children.find(c=>/^index\.(md|html)$/.test(c.name)); + if(idx)openPath(idx.path); + else div.classList.toggle("open"); + }); + div.appendChild(lbl); + const kids=document.createElement("div");kids.className="children"; + const sorted=[...node.children].sort((a,b)=>{ + if(sort==="name")return a.name.localeCompare(b.name); + return sort==="old"?a.mtime-b.mtime:b.mtime-a.mtime; + }); + for(const c of sorted){ + if(c.type==="file"){ + if(query&&!c.title.toLowerCase().includes(query))continue; + const a=document.createElement("a"); + a.className="file";a.textContent=c.title; + a.addEventListener("click",e=>{e.preventDefault();openPath(c.path);}); + PATH_TO_EL.set(c.path,a); + kids.appendChild(a); + }else kids.appendChild(renderNode(c,sort,query)); + } + div.appendChild(kids); + return div; } - const kids = node.children.map(c=>applySearch(c,q)).filter(Boolean); - if (!kids.length) return null; - node.children = kids; - return node; + return document.createTextNode(""); } -// Recursive renderer -function renderNode(node){ - if (node.type==="dir"){ - const wrap = document.createElement("div"); - wrap.className = "dir"; - wrap.setAttribute("role","treeitem"); - const lbl = document.createElement("div"); - lbl.className = "label"; - lbl.textContent = node.name; - lbl.addEventListener("click", () => wrap.classList.toggle("open")); - const kids = document.createElement("div"); - kids.className = "children"; - node.children.forEach(c => kids.appendChild(renderNode(c))); - wrap.append(lbl, kids); - return wrap; - } else { - const a = document.createElement("a"); - a.className = "file"; - a.setAttribute("role","treeitem"); - a.href = `#=${node.path}`; - a.innerHTML = `${node.pinned?'PIN ':''}${escapeHtml(node.title||node.name)}`; - a.addEventListener("click", e => { e.preventDefault(); openPath(node.path); }); - PATH_TO_EL.set(node.path, a); - return a; - } -} - -function escapeHtml(s){ return s.replace(/[&<>"']/g,c=>({ "&":"&","<":"<",">":">","\"":""","'":"'" }[c])); } - -// -------- Opening / rendering files -------- -function autoOpenLatest(){ - if (!INDEX?.flat?.length) return; - const latest = [...INDEX.flat].sort((a,b)=>getDate(b)-getDate(a))[0]; - if (latest) openPath(latest.path,{push:false}); -} - -async function openPath(path,{push=true}={}){ - if (!INDEX) return; - if (path===CURRENT_PATH) return; - const f = INDEX.flat.find(x=>x.path===path); - if (!f){ showError("File not found."); return; } - - CURRENT_PATH = path; - if (push && location.hash!==`#=${path}`) history.pushState(null,"",`#=${path}`); - - hideError(); +async function openPath(path){ + if(path===CURRENT_PATH)return; + CURRENT_PATH=path; + if(location.hash!==`#=${path}`)history.pushState(null,"",`#=${path}`); + let f=INDEX.flat.find(x=>x.path===path); + if(!f){metaLine.textContent="Not found";return;} + metaLine.textContent=`${f.pinned?"📌 ":""}${new Date(f.mtime).toISOString().slice(0,10)} • ${f.name}`; + if(f.ext===".md")await renderMarkdown(f.path); + else renderHTML(f.path); setActive(path); - metaEl.textContent = `${f.pinned?"Pinned • ":""}${new Date(getDate(f)).toISOString().slice(0,10)} • ${f.title||f.name}`; - - if (f.ext === ".md") { - await renderMarkdown(path); - } else { - renderHTML(path); - } - - // close drawer on mobile - if (window.innerWidth < 900) sidebar.classList.remove("open"); + updatePager(); + if(window.innerWidth<900)document.querySelector(".sidebar").classList.remove("open"); } - async function renderMarkdown(path){ - htmlView.style.display="none"; - mdView.style.display="block"; - try{ - const res = await fetch(path,{cache:"no-store"}); - if (!res.ok) throw new Error(res.statusText); - const txt = await res.text(); - const html = window.DOMPurify?.sanitize(window.marked?.parse(txt) || txt) || txt; - mdView.innerHTML = html; - }catch(e){ - showError("Failed to load Markdown."); - } + const res=await fetch(path); + if(!res.ok){mdView.innerHTML="

File not found

";return;} + const text=await res.text(); + const html=(window.marked?window.marked.parse(text):text); + const safe=(window.DOMPurify?window.DOMPurify.sanitize(html):html); + mdView.innerHTML=safe; + iframe.style.display="none";mdView.style.display="block"; } - function renderHTML(path){ - mdView.style.display="none"; - htmlView.style.display="block"; - htmlView.src = path; + iframe.src=path; + iframe.style.display="block";mdView.style.display="none"; } - function setActive(path){ document.querySelectorAll(".file.active").forEach(el=>el.classList.remove("active")); - const el = PATH_TO_EL.get(path); - if (el) el.classList.add("active"); + const el=PATH_TO_EL.get(path); + if(el){el.classList.add("active");let p=el.parentElement;while(p&&p!==treeEl){if(p.classList.contains("children"))p.parentElement.classList.add("open");p=p.parentElement;}} } - -function showError(msg){ errorBox.textContent = msg; errorBox.hidden = false; } -function hideError(){ errorBox.hidden = true; } \ No newline at end of file +function updatePager(){ + const list=INDEX.flat.filter(f=>f.ext===".md"||f.ext===".html").sort((a,b)=>b.mtime-a.mtime); + const i=list.findIndex(x=>x.path===CURRENT_PATH); + prevBtn.disabled=i<=0;nextBtn.disabled=i>=list.length-1; + prevBtn.onclick=()=>{if(i>0)openPath(list[i-1].path);}; + nextBtn.onclick=()=>{if(i{clearTimeout(searchTimer);searchTimer=setTimeout(rebuildTree,300);}); +sortSel.addEventListener("change",rebuildTree); +filterSel.addEventListener("change",rebuildTree); +document.body.addEventListener("click",e=>{ + const a=e.target.closest("a[href]"); + if(!a)return; + const href=a.getAttribute("href"); + if(href.startsWith("/")&&!href.startsWith("//")&&!a.target){ + e.preventDefault(); + openPath(href.replace(/^\//,"")); + } +}); +window.addEventListener("DOMContentLoaded",loadIndex); \ No newline at end of file