Update app.js

This commit is contained in:
Mark Randall Havens △ The Empathic Technologist ⟁ Doctor Who 42 2025-11-08 14:17:27 -06:00 committed by GitHub
parent cd689a0471
commit 9f759c7e13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,241 +1,154 @@
/* ============================================================ /* ============================================================
The Fold Within Framework v2.6 Stable Layout Build The Fold Within app.js v2.6.2
============================================================ */ ============================================================ */
let INDEX, CURRENT_PATH = null, PATH_TO_EL = new Map(); document.addEventListener("DOMContentLoaded", () => {
const treeEl = document.getElementById("tree");
const mdView = document.getElementById("mdView");
const mdWarn = document.getElementById("mdWarn");
const htmlView = 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");
const sidebar = document.querySelector(".sidebar"); const sidebar = document.querySelector(".sidebar");
const navToggle = document.getElementById("navToggle");
const overlay = document.querySelector(".overlay"); const overlay = document.querySelector(".overlay");
const navToggle = document.getElementById("navToggle");
const content = document.querySelector(".content");
const mdView = document.getElementById("mdView");
const htmlView = document.getElementById("htmlView");
/* Sidebar toggle */ let currentPath = "";
navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); let usedFallback = false;
overlay.addEventListener("click", () => sidebar.classList.remove("open"));
/* Load index and initialize */ /* ============================================================
async function loadIndex() { Sidebar Toggle (Desktop + Mobile)
const res = await fetch("/index.json", { cache: "no-store" }); ============================================================ */
INDEX = await res.json(); navToggle.addEventListener("click", () => {
populateFilters(); const isDesktop = window.innerWidth >= 900;
rebuildTree(); if (isDesktop) {
sidebar.classList.toggle("collapsed");
window.addEventListener("popstate", () => { content.classList.toggle("full");
const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; } else {
if (hp) openPath(hp); sidebar.classList.toggle("open");
overlay.classList.toggle("active");
}
}); });
const init = location.hash.startsWith("#=") overlay.addEventListener("click", () => {
? location.hash.slice(2) sidebar.classList.remove("open");
: INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0]?.path; overlay.classList.remove("active");
openPath(init);
}
function populateFilters() {
filterSel.innerHTML = '<option value="all">All</option>';
for (const cat of INDEX.sections) {
const o = document.createElement("option");
o.value = o.textContent = cat;
filterSel.appendChild(o);
}
}
/* Tree building */
function rebuildTree() {
treeEl.innerHTML = "";
PATH_TO_EL.clear();
const filter = filterSel.value;
const sort = sortSel.value;
const query = searchBox.value.trim().toLowerCase();
const root = { type: "dir", children: INDEX.tree };
const pruned = filterTree(root, f =>
(filter==="all" || f.path.split("/")[0]===filter) &&
(!query || (f.title||f.name).toLowerCase().includes(query))
);
sortDir(pruned, sort);
for (const c of pruned.children) treeEl.appendChild(renderNode(c));
treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open"));
}
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(n) {
if (n.type==="dir") {
const d = document.createElement("div");
d.className="dir";
const lbl=document.createElement("span");
lbl.className="label"; lbl.textContent=n.name||"/";
lbl.addEventListener("click",()=>{
const idx=n.children.find(c=>c.type==="file"&&/^index\.(md|html)$/i.test(c.name));
if(idx) openPath(idx.path); else d.classList.toggle("open");
}); });
d.appendChild(lbl);
const kids=document.createElement("div"); /* ============================================================
kids.className="children"; Load External Libraries (Marked + DOMPurify)
n.children.forEach(c=>kids.appendChild(renderNode(c))); ============================================================ */
d.appendChild(kids); async function ensureLibsReady(timeoutMs = 4000) {
return d; const start = Date.now();
while (
(!window.marked || !window.DOMPurify) &&
Date.now() - start < timeoutMs
) {
await new Promise((r) => setTimeout(r, 100));
} }
const a=document.createElement("a"); if (!window.marked || !window.DOMPurify) {
a.className="file"; console.warn("Markdown libraries not loaded — fallback to plain text.");
a.innerHTML=`${n.pinned?'📌 ':''}${iconForExt(n.ext)} ${n.title} usedFallback = true;
<span class="meta">(${fmtDate(n.mtime)} · ${n.name})</span>`; } else usedFallback = false;
a.addEventListener("click",e=>{e.preventDefault();openPath(n.path);});
PATH_TO_EL.set(n.path,a);
return a;
}
function iconForExt(ext){return ext===".md"?"📝":"🧩";}
function fmtDate(ms){return new Date(ms).toISOString().slice(0,10);}
/* File open */
async function openPath(path){
if(path===CURRENT_PATH) return;
CURRENT_PATH=path;
if(location.hash!==`#=${path}`) history.pushState(null,"",`#=${path}`);
const f=INDEX.flat.find(x=>x.path===path);
if(!f){ 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);
setActive(path);
updatePager();
if(window.innerWidth<900) sidebar.classList.remove("open");
} }
/* Markdown renderer */ /* ============================================================
async function renderMarkdown(path){ File Loading + Rendering
mdWarn.style.display = "none"; ============================================================ */
mdView.innerHTML="<p class='loading-note'>Loading…</p>"; async function loadFile(path) {
htmlView.style.display="none"; if (!path) return;
mdView.style.display="block"; currentPath = path;
try { try {
const res=await fetch("/"+path,{cache:"no-store"}); const res = await fetch(path);
if (!res.ok) throw new Error(`Path not found: ${path}`);
const ext = path.split(".").pop().toLowerCase();
const text = await res.text(); const text = await res.text();
let html, usedFallback=false; if (ext === "md" || ext === "markdown") {
if(window.marked) html=window.marked.parse(text); await ensureLibsReady();
else {usedFallback=true; html=text.replace(/&/g,"&amp;").replace(/</g,"&lt;");} renderMarkdown(text);
} else if (ext === "html" || ext === "htm") {
const safe=window.DOMPurify?window.DOMPurify.sanitize(html):html; renderHTML(text);
mdView.classList.remove("fade-in"); } else {
mdView.innerHTML=safe; mdView.innerHTML = `<div class="md-warn">Unsupported file type: ${ext}</div>`;
mdView.scrollTop=0; htmlView.innerHTML = "";
mdView.offsetHeight; }
mdView.classList.add("fade-in"); } catch (err) {
if(usedFallback) mdWarn.style.display="block"; console.error(err);
mdView.innerHTML = `<div class="md-warn">${err.message}</div>`;
setTimeout(()=>{ htmlView.innerHTML = "";
const content=document.querySelector(".content");
if(content){
const vh=window.innerHeight;
content.style.minHeight=`${vh-48}px`;
} }
mdView.scrollIntoView({behavior:"instant",block:"start"});
},80);
}catch(e){ mdView.innerHTML=`<p style='color:red;'>${e.message}</p>`; }
} }
/* HTML renderer */ /* ============================================================
function renderHTML(path){ Renderers
htmlView.src="/"+path; ============================================================ */
htmlView.style.display="block"; function renderMarkdown(text) {
mdView.style.display="none"; const safe = window.DOMPurify
htmlView.classList.remove("fade-in"); ? DOMPurify.sanitize(window.marked.parse(text))
htmlView.offsetHeight; : text
htmlView.classList.add("fade-in"); .replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
mdView.innerHTML = usedFallback
setTimeout(()=>{ ? `<div class='md-warn'>Markdown fallback to plain text (libs failed). Check console.</div><pre>${safe}</pre>`
const content=document.querySelector(".content"); : safe;
if(content){ fadeIn(mdView);
const vh=window.innerHeight;
content.style.minHeight=`${vh-48}px`;
}
htmlView.scrollIntoView({behavior:"instant",block:"start"});
},120);
} }
/* Active + pager */ function renderHTML(text) {
function setActive(path){ htmlView.srcdoc = text;
document.querySelectorAll(".file.active").forEach(el=>el.classList.remove("active")); fadeIn(htmlView);
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 q=searchBox.value.trim().toLowerCase();
const list=INDEX.flat.filter(f=>
(filterSel.value==="all"||f.path.split("/")[0]===filterSel.value)&&
(!q||f.title.toLowerCase().includes(q))
);
const cmp=sortSel.value==="name"?(a,b)=>a.name.localeCompare(b.name)
:sortSel.value==="old"?(a,b)=>a.mtime-b.mtime
:(a,b)=>b.mtime-a.mtime;
list.sort(cmp);
const i=list.findIndex(x=>x.path===CURRENT_PATH);
prevBtn.disabled=i<=0;
nextBtn.disabled=i>=list.length-1||i<0;
prevBtn.onclick=()=>i>0&&openPath(list[i-1].path);
nextBtn.onclick=()=>i<list.length-1&&openPath(list[i+1].path);
} }
/* Search + filter */ /* ============================================================
let searchTimer; Tree Navigation (dynamic)
searchBox.addEventListener("input",()=>{ ============================================================ */
clearTimeout(searchTimer); document.querySelectorAll(".file").forEach((fileEl) => {
searchTimer=setTimeout(rebuildTree,300); fileEl.addEventListener("click", () => {
}); const path = fileEl.dataset.path;
sortSel.addEventListener("change",rebuildTree); loadFile(path);
filterSel.addEventListener("change",rebuildTree);
/* Internal link interception */ // highlight active
document.body.addEventListener("click",e=>{ document
const a=e.target.closest("a[href]"); .querySelectorAll(".file.active")
if(!a) return; .forEach((el) => el.classList.remove("active"));
const href=a.getAttribute("href"); fileEl.classList.add("active");
if(href.startsWith("/")&&!href.startsWith("//")&&!a.target){
e.preventDefault(); // auto-collapse mobile
openPath(href.replace(/^\//,"")); if (window.innerWidth < 900) {
sidebar.classList.remove("open");
overlay.classList.remove("active");
} }
}); });
/* Resize */
window.addEventListener("resize",()=>{
const vh=window.innerHeight;
const c=document.querySelector(".content");
if(c) c.style.minHeight=`${vh-48}px`;
}); });
window.addEventListener("DOMContentLoaded",loadIndex); /* ============================================================
Fade Animation Helper
============================================================ */
function fadeIn(el) {
if (!el) return;
el.classList.remove("fade-in");
void el.offsetWidth; // reflow
el.classList.add("fade-in");
}
/* ============================================================
Hash-Based Routing (supports #=posts/file.md)
============================================================ */
function handleHashChange() {
const hash = window.location.hash.replace(/^#=+/, "");
if (hash && hash !== currentPath) loadFile(hash);
}
window.addEventListener("hashchange", handleHashChange);
handleHashChange();
/* ============================================================
Keyboard Shortcuts (optional)
============================================================ */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
sidebar.classList.remove("open", "collapsed");
content.classList.remove("full");
overlay.classList.remove("active");
}
});
});