Update app.js

This commit is contained in:
Mark Randall Havens △ The Empathic Technologist ⟁ Doctor Who 42 2025-11-08 11:29:40 -06:00 committed by GitHub
parent 9b61e82745
commit 595a40ae14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,120 +1,119 @@
let INDEX, CURRENT_PATH=null, PATH_TO_EL=new Map(); let INDEX, CURRENT_PATH = null, PATH_TO_EL = new Map();
const treeEl=document.getElementById("tree"); const treeEl = document.getElementById("tree");
const mdView=document.getElementById("mdView"); const mdView = document.getElementById("mdView");
const iframe=document.getElementById("htmlView"); const htmlView = document.getElementById("htmlView");
const metaLine=document.getElementById("meta"); const metaLine = document.getElementById("meta");
const sortSel=document.getElementById("sort"); const sortSel = document.getElementById("sort");
const filterSel=document.getElementById("filter"); const filterSel = document.getElementById("filter");
const searchBox=document.getElementById("search"); const searchBox = document.getElementById("search");
const prevBtn=document.getElementById("prev"); const prevBtn = document.getElementById("prev");
const nextBtn=document.getElementById("next"); const nextBtn = document.getElementById("next");
async function loadIndex(){ async function loadIndex() {
const res=await fetch("/index.json",{cache:"no-store"}); const res = await fetch("/index.json", { cache: "no-store" });
INDEX=await res.json(); INDEX = await res.json();
populateFilters(); populateFilters();
rebuildTree(); rebuildTree();
window.addEventListener("popstate",()=>{const hp=location.hash.slice(2);if(hp)openPath(hp);}); window.addEventListener("popstate", () => { const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; if (hp) openPath(hp); });
const init=location.hash.slice(2)||INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0].path; const init = location.hash.startsWith("#=") ? location.hash.slice(2) : INDEX.flat.sort((a, b) => b.mtime - a.mtime)[0]?.path;
openPath(init); openPath(init);
} }
function populateFilters(){ function populateFilters() {
const cats=new Set(INDEX.sections); filterSel.innerHTML = '<option value="all">All</option>';
filterSel.innerHTML='<option value="all">All</option>'; for (const cat of INDEX.sections) {
for(const c of cats){const o=document.createElement("option");o.value=c;o.textContent=c;filterSel.appendChild(o);} const opt = document.createElement("option");
} opt.value = opt.textContent = cat;
function rebuildTree(){ filterSel.appendChild(opt);
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));
} }
} }
function renderNode(node,sort,query){ function rebuildTree() {
if(node.type==="dir"){ treeEl.innerHTML = "";
const div=document.createElement("div"); PATH_TO_EL.clear();
div.className="dir";div.setAttribute("aria-expanded","false"); const filter = filterSel.value;
const lbl=document.createElement("span"); const sort = sortSel.value;
lbl.className="label";lbl.textContent=node.name; const query = searchBox.value.trim().toLowerCase();
lbl.addEventListener("click",()=>{ const root = { type: "dir", children: INDEX.tree };
const idx=node.children.find(c=>/^index\.(md|html)$/.test(c.name)); const pruned = filterTree(root, f => (filter === "all" || f.path.split("/")[0] === filter) && (!query || (f.title || f.name).toLowerCase().includes(query)));
if(idx)openPath(idx.path); sortDir(pruned, sort);
for (const c of pruned.children) treeEl.appendChild(renderNode(c));
treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open")); // Auto-open tops
}
function filterTree(node, keep) {
if (node.type === "file") return keep(node) ? node : null;
const kids = node.children.map(c => filterTree(c, keep)).filter(Boolean);
return kids.length ? { ...node, children: kids } : null;
}
function sortDir(node, sort) {
const cmp = sort === "name" ? (a, b) => a.name.localeCompare(b.name) :
sort === "old" ? (a, b) => a.mtime - b.mtime : (a, b) => b.mtime - a.mtime;
node.children.sort((a, b) => (a.type === "dir" && b.type !== "dir") ? -1 : (a.type !== "dir" && b.type === "dir") ? 1 : cmp(a, b));
node.children.forEach(c => c.type === "dir" && sortDir(c, sort));
}
function renderNode(node) {
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 => c.type === "file" && /^index\.(md|html)$/i.test(c.name));
if (idx) openPath(idx.path);
else div.classList.toggle("open"); else div.classList.toggle("open");
}); });
div.appendChild(lbl); div.appendChild(lbl);
const kids=document.createElement("div");kids.className="children"; const kids = document.createElement("div");
const sorted=[...node.children].sort((a,b)=>{ kids.className = "children";
if(sort==="name")return a.name.localeCompare(b.name); node.children.forEach(c => kids.appendChild(renderNode(c)));
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); div.appendChild(kids);
return div; return div;
} }
return document.createTextNode(""); const a = document.createElement("a");
a.className = "file";
a.innerHTML = `${node.pinned ? '<span class="pin">📌</span>' : ''}${iconForExt(node.ext)} ${node.title} <span class="meta">(${fmtDate(node.mtime)} · ${node.name})</span>`;
a.addEventListener("click", e => { e.preventDefault(); openPath(node.path); });
PATH_TO_EL.set(node.path, a);
return a;
} }
function iconForExt(ext) { return ext === ".md" ? "📝" : "🧩"; }
async function openPath(path){ function fmtDate(ms) { return new Date(ms).toISOString().slice(0, 10); }
if(path===CURRENT_PATH)return; function findDir(path) {
CURRENT_PATH=path; path = path.replace(/\/$/, '');
if(location.hash!==`#=${path}`)history.pushState(null,"",`#=${path}`); function search(node) {
let f=INDEX.flat.find(x=>x.path===path); if (node.type === "dir" && node.path === path) return node;
if(!f){metaLine.textContent="Not found";return;} for (const c of node.children || []) {
metaLine.textContent=`${f.pinned?"📌 ":""}${new Date(f.mtime).toISOString().slice(0,10)}${f.name}`; const found = search(c);
if(f.ext===".md")await renderMarkdown(f.path); if (found) return found;
}
}
return search({ children: INDEX.tree });
}
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) {
const dir = findDir(path);
if (dir) {
const idx = dir.children.find(c => c.type === "file" && /^index\.(md|html)$/i.test(c.name));
if (idx) return openPath(idx.path);
}
metaLine.textContent = "Path not found: " + path;
return;
}
metaLine.textContent = `${f.pinned ? "📌 " : ""}${fmtDate(f.mtime)}${f.name}`;
if (f.ext === ".md") await renderMarkdown(f.path);
else renderHTML(f.path); else renderHTML(f.path);
setActive(path); setActive(path);
updatePager(); updatePager();
if(window.innerWidth<900)document.querySelector(".sidebar").classList.remove("open"); if (window.innerWidth < 900) document.querySelector(".sidebar").classList.remove("open");
} }
async function renderMarkdown(path){ async function renderMarkdown(path) {
const res=await fetch(path); const res = await fetch("/" + path);
if(!res.ok){mdView.innerHTML="<p>File not found</p>";return;} if (!res.ok) { mdView.innerHTML = "<p>File not found: " + path + "</p>"; return; }
const text=await res.text(); const text = await res.text();
const html=(window.marked?window.marked.parse(text):text); const html = window.marked ? window.marked.parse(text) : text.replace(/&/g, '&amp;').replace(/</g, '&lt;');
const safe=(window.DOMPurify?window.DOMPurify.sanitize(html):html); const safe = window.DOMPurify ? window.DOMPurify.sanitize(html) : html;
mdView.innerHTML=safe; mdView.innerHTML = safe
iframe.style.display="none";mdView.style.display="block";
}
function renderHTML(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");let p=el.parentElement;while(p&&p!==treeEl){if(p.classList.contains("children"))p.parentElement.classList.add("open");p=p.parentElement;}}
}
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<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);