Update app.js

This commit is contained in:
Mark Randall Havens △ The Empathic Technologist ⟁ Doctor Who 42 2025-11-08 11:23:44 -06:00 committed by GitHub
parent aa45352f0d
commit ea59a32544
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,191 +1,120 @@
// Elements let INDEX, CURRENT_PATH=null, PATH_TO_EL=new Map();
const sidebar = document.getElementById("sidebar"); const treeEl=document.getElementById("tree");
const treeEl = document.getElementById("tree"); const mdView=document.getElementById("mdView");
const metaEl = document.getElementById("meta"); const iframe=document.getElementById("htmlView");
const mdView = document.getElementById("mdView"); const metaLine=document.getElementById("meta");
const htmlView = document.getElementById("htmlView"); const sortSel=document.getElementById("sort");
const errorBox = document.getElementById("errorBox"); const filterSel=document.getElementById("filter");
const sortSel = document.getElementById("sortSel"); const searchBox=document.getElementById("search");
const filterSel = document.getElementById("filterSel"); const prevBtn=document.getElementById("prev");
const searchBox = document.getElementById("searchBox"); const nextBtn=document.getElementById("next");
const navToggle = document.getElementById("navToggle");
const backdrop = document.getElementById("backdrop");
// State async function loadIndex(){
let INDEX = null; const res=await fetch("/index.json",{cache:"no-store"});
let CURRENT_PATH = null; INDEX=await res.json();
let PATH_TO_EL = new Map(); populateFilters();
rebuildTree();
// Drawer controls (mobile) window.addEventListener("popstate",()=>{const hp=location.hash.slice(2);if(hp)openPath(hp);});
navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); const init=location.hash.slice(2)||INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0].path;
backdrop.addEventListener("click", () => sidebar.classList.remove("open")); openPath(init);
}
// -------- Boot -------- function populateFilters(){
window.addEventListener("DOMContentLoaded", async () => { const cats=new Set(INDEX.sections);
await loadIndex(); filterSel.innerHTML='<option value="all">All</option>';
if (!INDEX) return; for(const c of cats){const o=document.createElement("option");o.value=c;o.textContent=c;filterSel.appendChild(o);}
}
sortSel.addEventListener("change", rebuildTree); function rebuildTree(){
filterSel.addEventListener("change", rebuildTree); treeEl.innerHTML="";
searchBox.addEventListener("input", rebuildTree); const filter=filterSel.value;
const sort=sortSel.value;
window.addEventListener("popstate", () => { const query=searchBox.value.toLowerCase();
const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; for(const dir of INDEX.tree){
if (hp) openPath(hp, {push:false}); if(filter!=="all"&&dir.name!==filter)continue;
}); treeEl.appendChild(renderNode(dir,sort,query));
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 = "<p style='color:#ff7a7a'>index.json missing. run the build.</p>";
} }
} }
function renderNode(node,sort,query){
// -------- Tree building / sorting / filtering -------- if(node.type==="dir"){
function rebuildTree() { const div=document.createElement("div");
if (!INDEX) return; div.className="dir";div.setAttribute("aria-expanded","false");
PATH_TO_EL.clear(); const lbl=document.createElement("span");
treeEl.innerHTML = ""; lbl.className="label";lbl.textContent=node.name;
lbl.addEventListener("click",()=>{
const sort = sortSel.value; const idx=node.children.find(c=>/^index\.(md|html)$/.test(c.name));
const filter = filterSel.value; if(idx)openPath(idx.path);
const query = searchBox.value.trim().toLowerCase(); else div.classList.toggle("open");
});
const roots = INDEX.tree div.appendChild(lbl);
.filter(d => filter==="all" || d.name===filter) const kids=document.createElement("div");kids.className="children";
.map(d => deepClone(d)); const sorted=[...node.children].sort((a,b)=>{
if(sort==="name")return a.name.localeCompare(b.name);
roots.forEach(r => { return sort==="old"?a.mtime-b.mtime:b.mtime-a.mtime;
applySort(r, sort); });
const filtered = applySearch(r, query); for(const c of sorted){
if (filtered) treeEl.appendChild(renderNode(filtered)); if(c.type==="file"){
}); if(query&&!c.title.toLowerCase().includes(query))continue;
const a=document.createElement("a");
// Auto-expand top dirs a.className="file";a.textContent=c.title;
treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open")); a.addEventListener("click",e=>{e.preventDefault();openPath(c.path);});
} PATH_TO_EL.set(c.path,a);
kids.appendChild(a);
function deepClone(obj){ return JSON.parse(JSON.stringify(obj)); } }else kids.appendChild(renderNode(c,sort,query));
function getDate(n){ return n.mtime || 0; } }
div.appendChild(kids);
function applySort(dir, sort) { return div;
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;
} }
const kids = node.children.map(c=>applySearch(c,q)).filter(Boolean); return document.createTextNode("");
if (!kids.length) return null;
node.children = kids;
return node;
} }
// Recursive renderer async function openPath(path){
function renderNode(node){ if(path===CURRENT_PATH)return;
if (node.type==="dir"){ CURRENT_PATH=path;
const wrap = document.createElement("div"); if(location.hash!==`#=${path}`)history.pushState(null,"",`#=${path}`);
wrap.className = "dir"; let f=INDEX.flat.find(x=>x.path===path);
wrap.setAttribute("role","treeitem"); if(!f){metaLine.textContent="Not found";return;}
const lbl = document.createElement("div"); metaLine.textContent=`${f.pinned?"📌 ":""}${new Date(f.mtime).toISOString().slice(0,10)}${f.name}`;
lbl.className = "label"; if(f.ext===".md")await renderMarkdown(f.path);
lbl.textContent = node.name; else renderHTML(f.path);
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?'<span class="pin">PIN</span> ':''}${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=>({ "&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;" }[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();
setActive(path); setActive(path);
metaEl.textContent = `${f.pinned?"Pinned • ":""}${new Date(getDate(f)).toISOString().slice(0,10)}${f.title||f.name}`; updatePager();
if(window.innerWidth<900)document.querySelector(".sidebar").classList.remove("open");
if (f.ext === ".md") {
await renderMarkdown(path);
} else {
renderHTML(path);
}
// close drawer on mobile
if (window.innerWidth < 900) sidebar.classList.remove("open");
} }
async function renderMarkdown(path){ async function renderMarkdown(path){
htmlView.style.display="none"; const res=await fetch(path);
mdView.style.display="block"; if(!res.ok){mdView.innerHTML="<p>File not found</p>";return;}
try{ const text=await res.text();
const res = await fetch(path,{cache:"no-store"}); const html=(window.marked?window.marked.parse(text):text);
if (!res.ok) throw new Error(res.statusText); const safe=(window.DOMPurify?window.DOMPurify.sanitize(html):html);
const txt = await res.text(); mdView.innerHTML=safe;
const html = window.DOMPurify?.sanitize(window.marked?.parse(txt) || txt) || txt; iframe.style.display="none";mdView.style.display="block";
mdView.innerHTML = html;
}catch(e){
showError("Failed to load Markdown.");
}
} }
function renderHTML(path){ function renderHTML(path){
mdView.style.display="none"; iframe.src=path;
htmlView.style.display="block"; iframe.style.display="block";mdView.style.display="none";
htmlView.src = path;
} }
function setActive(path){ function setActive(path){
document.querySelectorAll(".file.active").forEach(el=>el.classList.remove("active")); document.querySelectorAll(".file.active").forEach(el=>el.classList.remove("active"));
const el = PATH_TO_EL.get(path); const el=PATH_TO_EL.get(path);
if (el) el.classList.add("active"); 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 updatePager(){
function showError(msg){ errorBox.textContent = msg; errorBox.hidden = false; } const list=INDEX.flat.filter(f=>f.ext===".md"||f.ext===".html").sort((a,b)=>b.mtime-a.mtime);
function hideError(){ errorBox.hidden = true; } 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<list.length-1)openPath(list[i+1].path);};
}
let searchTimer;
searchBox.addEventListener("input",()=>{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);