diff --git a/public/app.js b/public/app.js index a549834..cc2a85c 100755 --- a/public/app.js +++ b/public/app.js @@ -1,154 +1,252 @@ -/* ============================================================ - The Fold Within — app.js v2.6.2 - ============================================================ */ +/* global marked, DOMPurify */ +const $ = (sel, root = document) => root.querySelector(sel); +const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; -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 state = { + index: null, + sidebarOpen: false, // mobile overlay (<=1024) + desktopCollapsed: false, // desktop collapse + sort: "newest", + section: "all", + q: "" +}; - let currentPath = ""; - let usedFallback = false; +const els = { + body: document.body, + sidebar: $("#sidebar"), + content: $("#content"), + viewer: $("#viewer"), + tree: $("#tree"), + routeHint: $("#routeHint"), + navToggle: $("#navToggle"), + filterSection: $("#filterSection"), + sortOrder: $("#sortOrder"), + searchBox: $("#searchBox"), +}; - /* ============================================================ - 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"); - } - }); +init(); - overlay.addEventListener("click", () => { - sidebar.classList.remove("open"); - overlay.classList.remove("active"); - }); +async function init(){ + // restore preferences + try { + state.desktopCollapsed = localStorage.getItem("desktopCollapsed") === "1"; + } catch {} - /* ============================================================ - 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; + // set collapse classes for initial paint + if (window.matchMedia("(min-width:1025px)").matches) { + els.body.classList.toggle("sidebar-collapsed", state.desktopCollapsed); + els.body.classList.add("sidebar-open"); // open on desktop unless collapsed } - /* ============================================================ - File Loading + Rendering - ============================================================ */ - async function loadFile(path) { - if (!path) return; - currentPath = path; + els.navToggle.addEventListener("click", toggleSidebar); + window.addEventListener("resize", onResizeMode); + window.addEventListener("hashchange", onRoute); + onResizeMode(); // set initial open/closed class for mobile/desktop + + // Fetch index + try { + const res = await fetch("/index.json", { cache: "no-cache" }); + if (!res.ok) throw new Error(res.status + " " + res.statusText); + state.index = await res.json(); + } catch (e) { + console.error("Failed to load index.json", e); + els.viewer.innerHTML = `
Could not load index.json
`; + return; + } + + // Build filters + buildFilters(); + + // Render tree + renderTree(); + + // Route initial + onRoute(); +} + +/* ---------- UI wiring ---------- */ + +function onResizeMode(){ + const desktop = window.matchMedia("(min-width:1025px)").matches; + + if (desktop) { + // Desktop: overlay classes off; use collapsed flag to shift layout + els.body.classList.remove("sidebar-open"); + els.body.classList.toggle("sidebar-collapsed", state.desktopCollapsed); + els.navToggle.setAttribute("aria-expanded", (!state.desktopCollapsed).toString()); + } else { + // Mobile: collapsed flag irrelevant; use overlay class instead + els.body.classList.remove("sidebar-collapsed"); + if (!state.sidebarOpen) els.body.classList.remove("sidebar-open"); + els.navToggle.setAttribute("aria-expanded", state.sidebarOpen.toString()); + } +} + +function toggleSidebar(){ + const desktop = window.matchMedia("(min-width:1025px)").matches; + if (desktop){ + state.desktopCollapsed = !els.body.classList.contains("sidebar-collapsed"); + els.body.classList.toggle("sidebar-collapsed"); + els.navToggle.setAttribute("aria-expanded", (!state.desktopCollapsed).toString()); + try { localStorage.setItem("desktopCollapsed", state.desktopCollapsed ? "1" : "0"); } catch {} + } else { + state.sidebarOpen = !els.body.classList.contains("sidebar-open"); + els.body.classList.toggle("sidebar-open"); + els.navToggle.setAttribute("aria-expanded", state.sidebarOpen.toString()); + } +} + +function buildFilters(){ + const sections = ["all", ...state.index.sections]; + els.filterSection.innerHTML = sections.map(s => + `` + ).join(""); + els.filterSection.value = state.section; + + els.filterSection.addEventListener("change", e => { + state.section = e.target.value; + renderTree(); + }); + + els.sortOrder.value = state.sort; + els.sortOrder.addEventListener("change", e => { + state.sort = e.target.value; + renderTree(); + }); + + els.searchBox.addEventListener("input", e => { + state.q = e.target.value.trim().toLowerCase(); + renderTree(); + }); +} + +function renderTree(){ + if (!state.index) return; + const items = state.index.flat.slice(); + + // filter + const filtered = items.filter(f => { + const inSection = state.section === "all" || f.path.startsWith(state.section + "/"); + const inQuery = !state.q || f.title.toLowerCase().includes(state.q) || f.name.toLowerCase().includes(state.q); + return inSection && inQuery; + }); + + // sort + filtered.sort((a,b) => { + if (state.sort === "title") return a.title.localeCompare(b.title, undefined, {sensitivity:"base"}); + if (state.sort === "oldest") return (a.mtime ?? 0) - (b.mtime ?? 0); + return (b.mtime ?? 0) - (a.mtime ?? 0); // newest + }); + + // render + els.tree.innerHTML = filtered.map(f => { + const d = new Date(f.mtime || Date.now()); + const meta = `${d.toISOString().slice(0,10)} • ${f.name}`; + return ` + +Select a note on the left.
+${safe}`
- : safe;
- fadeIn(mdView);
- }
-
- function renderHTML(text) {
- htmlView.srcdoc = text;
- fadeIn(htmlView);
- }
-
- /* ============================================================
- 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");
- }
- });
+ iframe.addEventListener("load", () => {
+ sizeIframe();
+ // resize observer for dynamic html (images etc.)
+ try {
+ const ro = new ResizeObserver(sizeIframe);
+ ro.observe(iframe.contentDocument.documentElement);
+ } catch { /* not critical */ }
+ // also a delayed pass for images/fonts
+ setTimeout(sizeIframe, 250);
+ setTimeout(sizeIframe, 800);
});
+}
- /* ============================================================
- 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
+/* ---------- Helpers ---------- */
+function capitalize(s){ return s.charAt(0).toUpperCase() + s.slice(1) }
+function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])) }
\ No newline at end of file