diff --git a/public/app.js b/public/app.js index da9211e..a549834 100755 --- a/public/app.js +++ b/public/app.js @@ -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 sidebar = document.querySelector(".sidebar"); + 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"); -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 navToggle = document.getElementById("navToggle"); -const overlay = document.querySelector(".overlay"); + let currentPath = ""; + let usedFallback = false; -/* Sidebar toggle */ -navToggle.addEventListener("click", () => sidebar.classList.toggle("open")); -overlay.addEventListener("click", () => sidebar.classList.remove("open")); - -/* Load index and initialize */ -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.startsWith("#=") ? location.hash.slice(2) : null; - if (hp) openPath(hp); + /* ============================================================ + Sidebar Toggle (Desktop + Mobile) + ============================================================ */ + navToggle.addEventListener("click", () => { + const isDesktop = window.innerWidth >= 900; + if (isDesktop) { + sidebar.classList.toggle("collapsed"); + content.classList.toggle("full"); + } else { + sidebar.classList.toggle("open"); + overlay.classList.toggle("active"); + } }); - const init = location.hash.startsWith("#=") - ? location.hash.slice(2) - : INDEX.flat.sort((a,b)=>b.mtime-a.mtime)[0]?.path; + overlay.addEventListener("click", () => { + sidebar.classList.remove("open"); + overlay.classList.remove("active"); + }); - openPath(init); -} - -function populateFilters() { - filterSel.innerHTML = ''; - for (const cat of INDEX.sections) { - const o = document.createElement("option"); - o.value = o.textContent = cat; - filterSel.appendChild(o); + /* ============================================================ + Load External Libraries (Marked + DOMPurify) + ============================================================ */ + async function ensureLibsReady(timeoutMs = 4000) { + const start = Date.now(); + while ( + (!window.marked || !window.DOMPurify) && + Date.now() - start < timeoutMs + ) { + await new Promise((r) => setTimeout(r, 100)); + } + if (!window.marked || !window.DOMPurify) { + console.warn("Markdown libraries not loaded β fallback to plain text."); + usedFallback = true; + } else usedFallback = false; } -} -/* 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")); -} + /* ============================================================ + File Loading + Rendering + ============================================================ */ + async function loadFile(path) { + if (!path) return; + currentPath = path; -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"; - n.children.forEach(c=>kids.appendChild(renderNode(c))); - d.appendChild(kids); - return d; - } - const a=document.createElement("a"); - a.className="file"; - a.innerHTML=`${n.pinned?'π ':''}${iconForExt(n.ext)} ${n.title} - `; - 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);} + try { + const res = await fetch(path); + if (!res.ok) throw new Error(`Path not found: ${path}`); -/* File open */ -async function openPath(path){ - if(path===CURRENT_PATH) return; - CURRENT_PATH=path; - if(location.hash!==`#=${path}`) history.pushState(null,"",`#=${path}`); + const ext = path.split(".").pop().toLowerCase(); + const text = await res.text(); - 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){ - mdWarn.style.display = "none"; - mdView.innerHTML="
Loadingβ¦
"; - htmlView.style.display="none"; - mdView.style.display="block"; - - try{ - const res=await fetch("/"+path,{cache:"no-store"}); - const text=await res.text(); - - let html, usedFallback=false; - if(window.marked) html=window.marked.parse(text); - else {usedFallback=true; html=text.replace(/&/g,"&").replace(/{ - const content=document.querySelector(".content"); - if(content){ - const vh=window.innerHeight; - content.style.minHeight=`${vh-48}px`; + if (ext === "md" || ext === "markdown") { + await ensureLibsReady(); + renderMarkdown(text); + } else if (ext === "html" || ext === "htm") { + renderHTML(text); + } else { + mdView.innerHTML = `${e.message}
`; } -} - -/* HTML renderer */ -function renderHTML(path){ - htmlView.src="/"+path; - htmlView.style.display="block"; - mdView.style.display="none"; - htmlView.classList.remove("fade-in"); - htmlView.offsetHeight; - htmlView.classList.add("fade-in"); - - setTimeout(()=>{ - const content=document.querySelector(".content"); - if(content){ - const vh=window.innerHeight; - content.style.minHeight=`${vh-48}px`; - } - htmlView.scrollIntoView({behavior:"instant",block:"start"}); - },120); -} - -/* Active + pager */ -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; + } catch (err) { + console.error(err); + mdView.innerHTML = `${safe}`
+ : safe;
+ fadeIn(mdView);
}
-});
-/* Resize */
-window.addEventListener("resize",()=>{
- const vh=window.innerHeight;
- const c=document.querySelector(".content");
- if(c) c.style.minHeight=`${vh-48}px`;
-});
+ function renderHTML(text) {
+ htmlView.srcdoc = text;
+ fadeIn(htmlView);
+ }
-window.addEventListener("DOMContentLoaded",loadIndex);
\ No newline at end of file
+ /* ============================================================
+ Tree Navigation (dynamic)
+ ============================================================ */
+ document.querySelectorAll(".file").forEach((fileEl) => {
+ fileEl.addEventListener("click", () => {
+ const path = fileEl.dataset.path;
+ loadFile(path);
+
+ // highlight active
+ document
+ .querySelectorAll(".file.active")
+ .forEach((el) => el.classList.remove("active"));
+ fileEl.classList.add("active");
+
+ // auto-collapse mobile
+ if (window.innerWidth < 900) {
+ sidebar.classList.remove("open");
+ overlay.classList.remove("active");
+ }
+ });
+ });
+
+ /* ============================================================
+ 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");
+ }
+ });
+});
\ No newline at end of file