/* ========================================================= ZETTELKASTEN GARDEN script.js ========================================================= */ document.addEventListener('DOMContentLoaded', () => { rearrangeDOM(); buildSidebar(); initNoteSystem(); initSidebarRail(); initBacklinkPanel(); initLinkPreviews(); initCopyButtons(); initFootnoteSidenotes(); initThemeToggle(); initCountdowns(); initTOCHighlighting(); initMediaFixes(document); initReadingProgress(); hookSearchResults(); }); /* ========================================================= DOM REARRANGEMENT ========================================================= */ function rearrangeDOM() { const body = document.body; const preamble = document.getElementById('preamble'); const stackRoot = document.getElementById('stack-root'); const postamble = document.getElementById('postamble'); if (!stackRoot) return; const content = stackRoot.querySelector('#content'); // Inject breadcrumb trail into header const bannerHeader = preamble?.querySelector('.banner-header'); if (bannerHeader) { const bannerLeft = bannerHeader.querySelector('.banner-left'); const bannerSearch = bannerHeader.querySelector('.banner-search'); const crumbTrail = el('div', { id: 'breadcrumb-trail', 'aria-label': 'Navigation history' }); if (bannerLeft && bannerSearch) { bannerHeader.insertBefore(crumbTrail, bannerSearch); } else { bannerHeader.appendChild(crumbTrail); } } const layoutBody = el('div', { id: 'layout-body' }); const sidebar = el('nav', { id: 'sidebar', 'aria-label': 'Navigation' }); const main = el('div', { id: 'main' }); const noteToolbar = el('div', { id: 'note-toolbar' }); const mainInner = el('div', { id: 'main-inner' }); const contentArea = el('div', { id: 'content-area' }); const backlinkPnl = el('div', { id: 'backlink-panel', role: 'complementary', 'aria-label': 'Backlinks' }); buildNoteToolbar(noteToolbar); backlinkPnl.innerHTML = `
backlinks

No backlinks to this note.

`; if (content) { const rootPane = el('div', { class: 'tab-content active', 'data-tab-id': 'tab-root' }); rootPane.appendChild(content); contentArea.appendChild(rootPane); } mainInner.appendChild(contentArea); mainInner.appendChild(backlinkPnl); main.appendChild(noteToolbar); main.appendChild(mainInner); layoutBody.appendChild(sidebar); layoutBody.appendChild(main); stackRoot.remove(); const progress = el('div', { id: 'reading-progress' }); body.appendChild(progress); if (preamble?.nextSibling) { body.insertBefore(layoutBody, preamble.nextSibling); } else { body.appendChild(layoutBody); } if (postamble) body.appendChild(postamble); } function buildNoteToolbar(toolbar) { toolbar.innerHTML = `
`; } /* ========================================================= SIDEBAR RAIL ========================================================= */ function initSidebarRail() { const sidebar = document.getElementById('sidebar'); if (!sidebar) return; const railBtns = [ { label: '\u2630', title: 'Catalogue', id: 'rail-tree', panel: 'tree' }, { label: '\u2315', title: 'Search', id: 'rail-search', action: 'search' }, ]; railBtns.forEach(({ label, title, id, panel, action }) => { const btn = el('button', { class: 'rail-btn', id, title, 'aria-label': title }); btn.textContent = label; sidebar.appendChild(btn); btn.addEventListener('click', () => { if (action === 'search') { if (typeof window.openSearchModal === 'function') { window.openSearchModal(); } else { document.getElementById('search-box')?.focus(); } return; } sidebar.classList.add('expanded'); sidebar.dataset.activePanel = panel; sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); if (window.matchMedia('(max-width: 900px)').matches) { sidebar.classList.add('mobile-open'); document.getElementById('sidebar-backdrop')?.classList.add('visible'); } }); }); // Mobile backdrop let backdrop = document.getElementById('sidebar-backdrop'); if (!backdrop) { backdrop = el('div', { id: 'sidebar-backdrop' }); document.body.appendChild(backdrop); } backdrop.addEventListener('click', () => { sidebar.classList.remove('expanded', 'mobile-open'); backdrop.classList.remove('visible'); sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active')); }); initSidebarToggle(); } function showSidebarPanel(panel) { const sidebar = document.getElementById('sidebar'); sidebar.querySelectorAll('.sidebar-panel').forEach(p => p.remove()); if (panel === 'tree') { const panelEl = el('div', { class: 'sidebar-panel' }); panelEl.innerHTML = `
`; sidebar.appendChild(panelEl); panelEl.querySelector('.sidebar-close-btn').addEventListener('click', () => { sidebar.classList.remove('expanded'); sidebar.dataset.activePanel = ''; sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active')); }); buildFileTree(panelEl.querySelector('#file-tree')); } } async function buildFileTree(container) { const currentPath = location.pathname; let treeData = null; try { const res = await fetch('/assets/sidebar-tree.json'); if (res.ok) { const json = await res.json(); if (Array.isArray(json.tree)) treeData = json.tree; } } catch (_) {} if (treeData) { renderTree(treeData, container, currentPath); return; } const groups = await buildGroupsFromSearchIndex(); if (!groups?.length) { const empty = el('div', { class: 'tree-file' }); empty.style.cssText = 'opacity:.4;font-size:.7rem;padding:8px 14px;'; empty.textContent = 'no notes found'; container.appendChild(empty); return; } const fallbackTree = groups.map(g => ({ label: g.label, children: [], files: g.files })); renderTree(fallbackTree, container, currentPath); } function renderTree(nodes, container, currentPath) { nodes.forEach(node => { const folderEl = el('div', { class: 'tree-folder', 'data-folder': node.label }); const icon = el('span', { class: 'tree-icon' }); icon.textContent = '\u25BE '; const labelSpan = el('span', { class: 'tree-folder-label' }); labelSpan.textContent = node.label; folderEl.append(icon, labelSpan); const childrenWrap = el('div', { class: 'tree-children' }); (node.files || []).forEach(f => { const pathname = normalisePathname(f.url); childrenWrap.appendChild(makeFileItem({ title: f.title, pathname }, currentPath)); }); if (node.children?.length) renderTree(node.children, childrenWrap, currentPath); folderEl.addEventListener('click', () => { const isOpen = childrenWrap.style.display !== 'none'; childrenWrap.style.display = isOpen ? 'none' : ''; icon.textContent = isOpen ? '\u25B8 ' : '\u25BE '; }); container.appendChild(folderEl); container.appendChild(childrenWrap); }); } async function buildGroupsFromSearchIndex() { try { const res = await fetch('/search-index.json'); if (!res.ok) return null; const raw = await res.json(); const all = Array.isArray(raw) ? raw : raw.entries || raw.notes || []; const files = all .filter(e => e && (e.url || e.path || e.pathname)) .map(e => ({ title: e.title || filenameToTitle(e.url || e.path || e.pathname), url: e.url || e.path || e.pathname, })) .sort((a, b) => a.title.localeCompare(b.title)); return files.length ? [{ label: 'all notes', files }] : null; } catch (_) { return null; } } function makeFileItem({ title, pathname }, currentPath) { const fileEl = el('div', { class: 'tree-file' + (pathname === currentPath ? ' active' : ''), 'data-pathname': pathname, }); const iconSpan = el('span', { class: 'tree-icon' }); iconSpan.textContent = '\u00B7 '; fileEl.appendChild(iconSpan); fileEl.appendChild(document.createTextNode(title)); fileEl.title = title; fileEl.addEventListener('click', () => { document.getElementById('sidebar')?.classList.remove('expanded'); openNote(pathname, title); }); return fileEl; } /* ========================================================= NOTE SYSTEM ========================================================= */ const noteHistory = []; let historyIndex = -1; let activeNoteId = null; function makeHistoryState(entry, hash = '') { return { notePathname: entry.pathname, noteLabel: entry.label, noteHash: hash || '', isRoot: !!entry.isRoot, }; } function historyUrlFor(entry, hash = '') { if (entry.isRoot) { return location.pathname + location.search; } return entry.pathname + (hash || ''); } function readHashState() { const hash = location.hash.slice(1); const state = { active: null }; if (!hash) return state; hash.split('&').forEach(part => { const [key, val] = part.split('='); if (!val) return; if (key === 'active') state.active = decodeURIComponent(val); }); return state; } function writeHashState() { const current = noteHistory[historyIndex]; if (!current || current.isRoot) { history.replaceState(null, '', location.pathname + location.search); return; } const newHash = '#active=' + encodeURIComponent(current.pathname); history.replaceState(null, '', location.pathname + location.search + newHash); } function initNoteSystem() { const rootPane = document.querySelector(".tab-content[data-tab-id='tab-root']"); if (!rootPane) return; const titleEl = rootPane.querySelector('.title-section .title, h1.title, h1'); const label = titleEl?.textContent.trim() || document.title || 'Home'; const rootEntry = { id: 'tab-root', label, pathname: location.pathname, paneEl: rootPane, isRoot: true }; noteHistory.push(rootEntry); historyIndex = 0; activeNoteId = 'tab-root'; updateBreadcrumb(); updateToolbar(); initMediaFixes(rootPane); if (window._gallerySetupVideo) window._gallerySetupVideo(rootPane); if (window._gallerySetupAudio) window._gallerySetupAudio(rootPane); // Register the initial entry with browser history history.replaceState( makeHistoryState(rootEntry), '', historyUrlFor(rootEntry) ); // Intercept internal .html link clicks only document.addEventListener('click', e => { const link = e.target.closest('a'); if (!link) return; const href = link.getAttribute('href'); if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return; let url; try { url = new URL(href, location.href); } catch { return; } if (url.origin !== location.origin) return; // Only intercept .html pages — let assets, PDFs, etc. through if (!url.pathname.endsWith('.html')) return; e.preventDefault(); const pathname = normalisePathname(url.pathname); const hash = url.hash || ''; const label = link.textContent.trim() || filenameToTitle(pathname); openNote(pathname, label, hash); }); // Toolbar buttons document.getElementById('tb-catalogue')?.addEventListener('click', toggleCatalogue); document.getElementById('tb-back')?.addEventListener('click', goBack); document.getElementById('tb-forward')?.addEventListener('click', goForward); document.getElementById('tb-backlinks')?.addEventListener('click', toggleBacklinkPanel); document.getElementById('tb-toc')?.addEventListener('click', toggleTOC); // Backlink panel close document.getElementById('bp-close')?.addEventListener('click', () => { document.getElementById('backlink-panel')?.classList.remove('open'); document.getElementById('tb-backlinks')?.classList.remove('active'); }); window.addEventListener('popstate', async (e) => { const state = e.state; // No state: fall back to root if (!state || !state.notePathname) { if (noteHistory[0]) activateNote(noteHistory[0].id, true); return; } const pathname = normalisePathname(state.notePathname); const label = state.noteLabel || filenameToTitle(pathname); const hash = state.noteHash || ''; const existing = noteHistory.find(n => n.pathname === pathname); if (existing) { activateNote(existing.id, true); if (hash) scrollToHash(existing.paneEl, hash); return; } await openNote(pathname, label, hash, true, false); }); } async function openNote(pathname, label, hash = '', silent = false, pushBrowserState = true) { console.log('Opening note:', pathname, { label, hash, silent }); // Normalise pathname = normalisePathname(pathname); // Already at this note? const current = noteHistory[historyIndex]; if (current?.pathname === pathname) { if (hash) scrollToHash(current.paneEl, hash); return; } // Already exists in history? const existing = noteHistory.find(n => n.pathname === pathname); if (existing) { historyIndex = noteHistory.indexOf(existing); activateNote(existing.id, true); if (hash) scrollToHash(existing.paneEl, hash); if (pushBrowserState) { history.pushState( makeHistoryState(existing, hash), '', historyUrlFor(existing, hash) ); } return; } // Show loading indicator const contentArea = document.getElementById('content-area'); const loadingPane = el('div', { class: 'tab-content active', 'data-loading': '1' }); loadingPane.innerHTML = '
Loading\u2026
'; contentArea?.appendChild(loadingPane); // Hide all other panes while loading noteHistory.forEach(n => n.paneEl.classList.remove('active')); let contentEl; let resolvedLabel = label || filenameToTitle(pathname); try { const res = await fetch(pathname); if (!res.ok) throw new Error(`HTTP ${res.status}`); const doc = new DOMParser().parseFromString(await res.text(), 'text/html'); const titleEl = doc.querySelector('.title-section .title, h1.title, h1'); if (titleEl) resolvedLabel = titleEl.textContent.trim(); // Prefer #content, else fall back gracefully contentEl = doc.getElementById('content') || doc.querySelector('.stack-pane') || (() => { const d = document.createElement('div'); d.id = 'content'; d.innerHTML = '

Note loaded but content element not found.

'; return d; })(); // Decode mermaid entities contentEl.querySelectorAll('.mermaid').forEach(m => { m.innerHTML = m.innerHTML .replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&'); }); fixRelativeSrcs(contentEl, pathname); } catch (err) { console.error('Note fetch error:', err); contentEl = document.createElement('div'); contentEl.id = 'content'; contentEl.innerHTML = `

` + `Could not load ${escHtml(pathname)}: ${escHtml(err.message)}

`; } // Remove the temporary loading pane loadingPane.remove(); const id = 'tab-' + Date.now(); const paneEl = el('div', { class: 'tab-content', 'data-tab-id': id }); paneEl.appendChild(contentEl); contentArea?.appendChild(paneEl); const entry = { id, label: resolvedLabel, pathname, paneEl, isRoot: false }; // Truncate forward history when branching if (historyIndex < noteHistory.length - 1) { noteHistory.slice(historyIndex + 1).forEach(n => { if (!n.isRoot) n.paneEl.remove(); }); noteHistory.splice(historyIndex + 1); } noteHistory.push(entry); historyIndex = noteHistory.length - 1; activateNote(id, true); if (hash) scrollToHash(paneEl, hash); if (pushBrowserState) { history.pushState( makeHistoryState(entry, hash), '', historyUrlFor(entry, hash) ); } // Run mermaid on newly loaded diagrams if (typeof mermaid !== 'undefined') { const diagrams = paneEl.querySelectorAll('.mermaid:not([data-processed])'); if (diagrams.length) mermaid.run({ nodes: diagrams }); } initCopyButtonsIn(paneEl); initFootnoteSidenotesIn(paneEl); initMediaFixes(paneEl); initLinkPreviewsIn(paneEl); const trySetup = () => { if (window._gallerySetupVideo) window._gallerySetupVideo(paneEl); if (window._gallerySetupAudio) window._gallerySetupAudio(paneEl); }; if (window._gallerySetupVideo) { trySetup(); } else { setTimeout(trySetup, 200); } updateSidebarActive(pathname); } function activateNote(id, skipBrowserWrite = true) { activeNoteId = id; const idx = noteHistory.findIndex(n => n.id === id); if (idx !== -1) historyIndex = idx; noteHistory.forEach(n => n.paneEl.classList.toggle('active', n.id === id)); const area = document.getElementById('content-area'); if (area) area.scrollTop = 0; updateBreadcrumb(); updateToolbar(); // Refresh backlink panel if open const panel = document.getElementById('backlink-panel'); if (panel?.classList.contains('open')) refreshBacklinkPanel(); updateSidebarActive(noteHistory[historyIndex]?.pathname); } function goBack() { window.history.back(); } function goForward() { window.history.forward(); } function updateBreadcrumb() { const trail = document.getElementById('breadcrumb-trail'); if (!trail) return; trail.innerHTML = ''; const start = Math.max(0, historyIndex - 2); const slice = noteHistory.slice(start, historyIndex + 1); slice.forEach((entry, i) => { if (i > 0) { const sep = el('span', { class: 'crumb-sep', 'aria-hidden': 'true' }); sep.textContent = '/'; trail.appendChild(sep); } const isActive = i === slice.length - 1; const crumb = el('span', { class: 'crumb' + (isActive ? ' active' : '') }); if (isActive) crumb.setAttribute('aria-current', 'page'); crumb.textContent = entry.label; crumb.title = entry.pathname; if (!isActive) { const targetIdx = start + i; crumb.addEventListener('click', () => { historyIndex = targetIdx; activateNote(entry.id); }); } trail.appendChild(crumb); }); } function updateToolbar() { const backBtn = document.getElementById('tb-back'); if (backBtn) { const canGoBack = historyIndex > 0; backBtn.disabled = !canGoBack; backBtn.style.opacity = canGoBack ? '1' : '0.35'; } const fwdBtn = document.getElementById('tb-forward'); if (fwdBtn) { const canGoFwd = historyIndex < noteHistory.length - 1; fwdBtn.disabled = !canGoFwd; fwdBtn.style.opacity = canGoFwd ? '1' : '0.35'; } const pos = document.getElementById('note-graph-pos'); if (pos) { const current = noteHistory[historyIndex]; if (current) { const name = current.pathname.split('/').pop().replace('.html', ''); // Strip the timestamp prefix from Org-Roam filenames const clean = name.replace(/^\d{14}-/, ''); pos.textContent = clean.length > 24 ? clean.slice(0, 22) + '\u2026' : clean; } } } function toggleTOC() { const active = noteHistory[historyIndex]; if (!active) return; const toc = active.paneEl.querySelector('#table-of-contents'); if (!toc) return; const hidden = toc.style.display === 'none'; toc.style.display = hidden ? '' : 'none'; document.getElementById('tb-toc')?.classList.toggle('active', hidden); } function toggleCatalogue() { const sidebar = document.getElementById('sidebar'); if (!sidebar) return; const mobile = window.matchMedia('(max-width: 900px)').matches; if (mobile) { const isOpen = sidebar.classList.toggle('mobile-open'); sidebar.classList.toggle('expanded', isOpen); document.getElementById('sidebar-backdrop')?.classList.toggle('visible', isOpen); return; } document.getElementById('file-tree')?.focus?.(); } /* ========================================================= BACKLINK PANEL ========================================================= */ function initBacklinkPanel() { // Panel built in rearrangeDOM; nothing else needed here } function toggleBacklinkPanel() { const panel = document.getElementById('backlink-panel'); const btn = document.getElementById('tb-backlinks'); if (!panel) return; const isOpen = panel.classList.toggle('open'); btn?.classList.toggle('active', isOpen); if (isOpen) refreshBacklinkPanel(); } async function refreshBacklinkPanel() { const panel = document.getElementById('backlink-panel'); if (!panel?.classList.contains('open')) return; const list = document.getElementById('bp-list'); const countEl = document.getElementById('bp-count'); const current = noteHistory[historyIndex]; if (!list || !current) return; list.innerHTML = '

Loading\u2026

'; if (countEl) countEl.textContent = '\u2026'; try { const res = await fetch('/search-index.json'); if (!res.ok) throw new Error('no index'); const raw = await res.json(); const all = Array.isArray(raw) ? raw : raw.entries || raw.notes || []; // Match by both the bare filename and the full pathname const currentFile = current.pathname.split('/').pop().replace(/\.html$/, ''); const currentPath = normalisePathname(decodeURIComponent(current.pathname)); const backlinks = all.filter(e => { if (!e) return false; const entryUrl = normalisePathname(decodeURIComponent(e.url || '')); if (entryUrl === currentPath) return false; const links = (e.links || []).map(l => normalisePathname(decodeURIComponent(l)) ); return links.some(l => l === currentPath); }); if (countEl) countEl.textContent = String(backlinks.length); if (!backlinks.length) { list.innerHTML = '

No backlinks found for this note.

'; return; } console.log("Have backlinks for",{ currentPath, currentFile, sampleEntry: all[0] }); list.innerHTML = ''; backlinks.forEach(bl => { const pathname = normalisePathname(bl.url || bl.path || bl.pathname || ''); const title = bl.title || filenameToTitle(pathname); const excerpt = (bl.content || bl.body || bl.text || '').slice(0, 140).trim(); const item = el('div', { class: 'bp-item' }); const titleDiv = el('div', { class: 'bp-item-title' }); titleDiv.textContent = title; item.appendChild(titleDiv); if (excerpt) { const excDiv = el('div', { class: 'bp-item-excerpt' }); excDiv.textContent = excerpt + '\u2026'; item.appendChild(excDiv); } item.addEventListener('click', () => openNote(pathname, title)); list.appendChild(item); }); } catch (_) { list.innerHTML = '

Could not load backlinks.

'; if (countEl) countEl.textContent = '!'; } } /* ========================================================= LINK HOVER PREVIEWS ========================================================= */ let previewTimer = null; const previewCache = {}; function initLinkPreviews() { if (!document.getElementById('link-preview')) { const card = el('div', { id: 'link-preview', role: 'tooltip', 'aria-hidden': 'true' }); card.innerHTML = `
Note title
Loading\u2026
`; document.body.appendChild(card); } initLinkPreviewsIn(document); } function initLinkPreviewsIn(root) { // Only attach previews to internal .html links, not external or asset links root.querySelectorAll('a[href*=".html"]').forEach(link => { if (link.dataset.previewInited) return; // Skip links to external origins try { const url = new URL(link.getAttribute('href'), location.href); if (url.origin !== location.origin) return; } catch { return; } link.dataset.previewInited = '1'; link.addEventListener('mouseenter', e => { clearTimeout(previewTimer); previewTimer = setTimeout(() => showPreview(link, e), 300); }); link.addEventListener('mouseleave', () => { clearTimeout(previewTimer); hidePreview(); }); link.addEventListener('mousemove', e => { const card = document.getElementById('link-preview'); if (card?.classList.contains('visible')) positionPreview(card, e); }); }); } async function showPreview(link, evt) { const card = document.getElementById('link-preview'); if (!card) return; const href = link.getAttribute('href'); let url; try { url = new URL(href, location.href); } catch { return; } if (url.origin !== location.origin) return; const pathname = normalisePathname(url.pathname); const titleEl = document.getElementById('preview-title'); const bodyEl = document.getElementById('preview-body'); titleEl.textContent = link.textContent.trim() || filenameToTitle(pathname); bodyEl.textContent = 'Loading\u2026'; positionPreview(card, evt); card.classList.add('visible'); card.setAttribute('aria-hidden', 'false'); if (previewCache[pathname]) { const { title, body } = previewCache[pathname]; titleEl.textContent = title; bodyEl.textContent = body || 'No preview available.'; return; } try { const res = await fetch(pathname); if (!res.ok) throw new Error('fetch failed'); const doc = new DOMParser().parseFromString(await res.text(), 'text/html'); const h1 = doc.querySelector('.title-section .title, h1.title, h1'); const paras = [...doc.querySelectorAll('#content p')]; const title = h1?.textContent.trim() || filenameToTitle(pathname); const body = paras.slice(0, 3).map(p => p.textContent.trim()).join(' ').slice(0, 280); previewCache[pathname] = { title, body }; if (card.classList.contains('visible')) { titleEl.textContent = title; bodyEl.textContent = body || 'No preview available.'; } } catch (_) { if (card.classList.contains('visible')) bodyEl.textContent = 'Preview unavailable.'; } } function positionPreview(card, evt) { const margin = 14; const cw = card.offsetWidth || 300; const ch = card.offsetHeight || 200; let x = evt.clientX + margin; let y = evt.clientY + margin; if (x + cw > window.innerWidth - margin) x = evt.clientX - cw - margin; if (y + ch > window.innerHeight - margin) y = evt.clientY - ch - margin; card.style.left = x + 'px'; card.style.top = y + 'px'; } function hidePreview() { const card = document.getElementById('link-preview'); card?.classList.remove('visible'); card?.setAttribute('aria-hidden', 'true'); } /* ========================================================= READING PROGRESS BAR ========================================================= */ function initReadingProgress() { const bar = document.getElementById('reading-progress'); const area = document.getElementById('content-area'); if (!bar || !area) return; area.addEventListener('scroll', () => { const { scrollTop, scrollHeight, clientHeight } = area; const pct = scrollHeight <= clientHeight ? 0 : (scrollTop / (scrollHeight - clientHeight)) * 100; bar.style.width = pct + '%'; }, { passive: true }); } /* ========================================================= SEARCH RESULTS HOOK ========================================================= */ function hookSearchResults() { const resultsBox = document.getElementById('search-results'); const searchBox = document.getElementById('search-box'); if (!resultsBox) return; resultsBox.addEventListener('mousedown', e => { let item = e.target; while (item && item !== resultsBox) { const url = item.dataset.url || item.dataset.href || item.dataset.path || item.querySelector?.('a')?.getAttribute('href') || (item.tagName === 'A' ? item.getAttribute('href') : null); if (url) { e.preventDefault(); e.stopPropagation(); resultsBox.innerHTML = ''; if (searchBox) searchBox.value = ''; window.closeSearchModal?.(); let resolved; try { resolved = new URL(url, location.href); } catch { return; } if (resolved.origin !== location.origin) { window.open(url, '_blank'); return; } if (!resolved.pathname.endsWith('.html')) return; const label = item.textContent.trim() || filenameToTitle(resolved.pathname); openNote(resolved.pathname, label, resolved.hash); return; } item = item.parentElement; } }); if (searchBox) { searchBox.addEventListener('keydown', e => { if (e.key !== 'Enter') return; const active = resultsBox.querySelector('.active, li:first-child, div:first-child'); if (!active) return; const url = active.dataset.url || active.dataset.href || active.querySelector?.('a')?.getAttribute('href'); if (!url) return; resultsBox.innerHTML = ''; searchBox.value = ''; window.closeSearchModal?.(); let resolved; try { resolved = new URL(url, location.href); } catch { return; } openNote(resolved.pathname, active.textContent.trim(), resolved.hash); }); } } /* ========================================================= SIDEBAR TOGGLE ========================================================= */ function initSidebarToggle() { const sidebar = document.getElementById('sidebar'); if (!sidebar) return; // Keyboard shortcut: Ctrl+\ toggles sidebar document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === '\\') { e.preventDefault(); const nowExpanded = sidebar.classList.toggle('expanded'); if (nowExpanded && !sidebar.dataset.activePanel) { sidebar.dataset.activePanel = 'tree'; showSidebarPanel('tree'); sidebar.querySelector('#rail-tree')?.classList.add('active'); } if (!nowExpanded) { sidebar.dataset.activePanel = ''; sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active')); } } }); // Touch swipe-to-open let touchStartX = null; document.addEventListener('touchstart', e => { if (e.touches[0].clientX < 20) touchStartX = e.touches[0].clientX; }, { passive: true }); document.addEventListener('touchend', e => { if (touchStartX === null) return; const dx = e.changedTouches[0].clientX - touchStartX; touchStartX = null; if (dx > 40) { sidebar.classList.add('expanded', 'mobile-open'); document.getElementById('sidebar-backdrop')?.classList.add('visible'); } }, { passive: true }); sidebar.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true }); sidebar.addEventListener('touchend', e => { if (touchStartX === null) return; const dx = e.changedTouches[0].clientX - touchStartX; touchStartX = null; if (dx < -40) { sidebar.classList.remove('expanded', 'mobile-open'); document.getElementById('sidebar-backdrop')?.classList.remove('visible'); } }, { passive: true }); } /* ========================================================= SIDEBAR ACTIVE STATE ========================================================= */ function updateSidebarActive(pathname) { document.querySelectorAll('.tree-file').forEach(f => { f.classList.toggle('active', f.dataset.pathname === pathname); }); } /* ========================================================= MEDIA FIXES ========================================================= */ function initMediaFixes(root) { root.querySelectorAll('audio').forEach(audio => { audio.controls = true; if (!audio.closest('.audio-player')) { const wrapper = document.createElement('div'); wrapper.className = 'audio-player'; const src = audio.src || audio.querySelector?.('source')?.src || ''; const labelText = audio.dataset.label || audio.title || src.split('/').pop().replace(/\.[^.]+$/, '') || 'audio'; const labelEl = document.createElement('div'); labelEl.className = 'audio-label'; labelEl.textContent = labelText; audio.parentNode.insertBefore(wrapper, audio); wrapper.append(labelEl, audio); } }); root.querySelectorAll('video').forEach(video => { video.controls = true; video.style.cssText = 'max-width:100%;width:100%;height:auto;display:block;border-radius:6px;'; if (!video.closest('.video-container')) { const wrapper = document.createElement('div'); wrapper.className = 'video-container'; video.parentNode.insertBefore(wrapper, video); wrapper.appendChild(video); } if (!video.currentSrc) video.load(); }); } function fixRelativeSrcs(root, fromPathname) { const base = new URL(fromPathname, location.href).href.split('/').slice(0, -1).join('/') + '/'; const fix = (elmt, attr) => { const val = elmt.getAttribute(attr); if (!val || val.startsWith('http') || val.startsWith('//') || val.startsWith('data:') || val.startsWith('#') || val.startsWith('/')) return; try { elmt.setAttribute(attr, new URL(val, base).href); } catch {} }; root.querySelectorAll('img').forEach(e => fix(e, 'src')); root.querySelectorAll('video').forEach(e => fix(e, 'src')); root.querySelectorAll('audio').forEach(e => fix(e, 'src')); root.querySelectorAll('source').forEach(e => fix(e, 'src')); root.querySelectorAll('a').forEach(e => fix(e, 'href')); } /* ========================================================= COPY BUTTONS ========================================================= */ function initCopyButtons() { initCopyButtonsIn(document); } function initCopyButtonsIn(root) { root.querySelectorAll('pre.src').forEach(block => { if (block.querySelector('.copy-btn')) return; block.style.position = 'relative'; const btn = document.createElement('button'); btn.className = 'copy-btn'; btn.textContent = 'copy'; block.appendChild(btn); btn.addEventListener('click', async () => { const text = block.innerText.replace(btn.innerText, '').trim(); try { await navigator.clipboard.writeText(text); btn.textContent = 'copied'; } catch { btn.textContent = 'failed'; } setTimeout(() => { btn.textContent = 'copy'; }, 1600); }); }); } /* ========================================================= FOOTNOTE SIDENOTES ========================================================= */ function initFootnoteSidenotes() { initFootnoteSidenotesIn(document); } function initFootnoteSidenotesIn(root) { root.querySelectorAll('a.footref[href^="#fn"]').forEach(ref => { const sup = ref.closest('sup') || ref; if (sup.nextElementSibling?.classList.contains('footnote-sidenote')) return; const targetId = ref.getAttribute('href').slice(1); const anchor = document.getElementById(targetId) || root.querySelector?.(`#${CSS.escape(targetId)}`); if (!anchor) return; const footdef = anchor.closest('.footdef') || anchor.parentElement; if (!footdef) return; let paras = footdef.querySelectorAll('p.footpara'); if (!paras.length) paras = footdef.querySelectorAll('.footpara'); let parts = []; if (paras.length) { const seen = new Set(); parts = [...paras].map(p => { const txt = p.textContent.trim().replace(/\s+/g, ' '); if (seen.has(txt)) return ''; seen.add(txt); return p.innerHTML.trim(); }).filter(Boolean); } if (!parts.length) { const clone = footdef.cloneNode(true); clone.querySelectorAll('sup.footnum, a[role="doc-backlink"]').forEach(n => n.remove()); parts = [clone.innerHTML.trim()]; } const sidenote = document.createElement('span'); sidenote.className = 'sidenote footnote-sidenote'; sidenote.dataset.fn = ref.textContent.trim(); sidenote.innerHTML = parts.join(' '); sup.insertAdjacentElement('afterend', sidenote); }); } /* ========================================================= THEME TOGGLE ========================================================= */ function initThemeToggle() { const root = document.documentElement; const key = 'theme'; const saved = localStorage.getItem(key); const initial = (saved === 'dark' || saved === 'light') ? saved : 'dark'; root.setAttribute('data-theme', initial); const btn = document.getElementById('theme-toggle'); if (!btn) return; const updateIcon = theme => { // Moon for dark mode, sun for light mode btn.textContent = theme === 'dark' ? '\u263D' : '\u2600'; btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); }; updateIcon(initial); btn.addEventListener('click', () => { const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; root.setAttribute('data-theme', next); localStorage.setItem(key, next); updateIcon(next); }); } /* ========================================================= COUNTDOWNS ========================================================= */ function initCountdowns() { const els = document.querySelectorAll('time.countdown'); if (!els.length) return; const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`; const render = el => { const target = new Date(el.getAttribute('datetime')); const label = el.dataset.label || ''; if (isNaN(target)) { el.textContent = ''; return; } let diff = target - new Date(); if (diff <= 0) { el.textContent = `${label ? label + ' ' : ''}today`; el.classList.add('expired'); return; } const d = Math.floor(diff / 86400000); diff %= 86400000; const h = Math.floor(diff / 3600000); diff %= 3600000; const m = Math.floor(diff / 60000); diff %= 60000; const s = Math.floor(diff / 1000); const parts = []; if (d) parts.push(plural(d, 'day')); parts.push(`${h}h ${m}m ${s}s`); el.textContent = `${label ? label + ' in: ' : ''}${parts.join(' ')}`; }; const tick = () => els.forEach(render); tick(); setInterval(tick, 1000); } /* ========================================================= TOC HIGHLIGHTING ========================================================= */ function initTOCHighlighting() { // Scoped to the currently active pane const getActiveTOC = () => { const active = noteHistory[historyIndex]; if (!active) return null; return active.paneEl.querySelector('#text-table-of-contents'); }; const toc = getActiveTOC() || document.querySelector('#text-table-of-contents'); if (!toc) return; const links = [...toc.querySelectorAll('a[href^="#"]')]; if (!links.length) return; const linkById = new Map(); links.forEach(a => { const id = decodeURIComponent(a.hash.slice(1)); const elmt = document.getElementById(id); if (elmt) linkById.set(id, a); }); const headings = [...document.querySelectorAll('h2[id], h3[id], h4[id]')] .filter(h => linkById.has(h.id)); const setActive = id => { links.forEach(a => { const active = a.hash === `#${id}`; a.classList.toggle('is-active', active); if (active) a.setAttribute('aria-current', 'true'); else a.removeAttribute('aria-current'); }); }; const headerOffset = 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize); const visible = new Map(); const observer = new IntersectionObserver(entries => { entries.forEach(entry => { const id = entry.target.id; if (entry.isIntersecting) { visible.set(id, entry.target.getBoundingClientRect().top - headerOffset); } else { visible.delete(id); } }); if (visible.size) { const [id] = [...visible.entries()].sort((a, b) => Math.abs(a[1]) - Math.abs(b[1]))[0]; setActive(id); } }, { rootMargin: `-${headerOffset}px 0px -70% 0px`, threshold: [0, 0.01, 0.1] }); headings.forEach(h => observer.observe(h)); toc.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (!a) return; const id = decodeURIComponent(a.hash.slice(1)); const elmt = document.getElementById(id); if (!elmt) return; e.preventDefault(); elmt.scrollIntoView({ behavior: 'smooth', block: 'start' }); elmt.setAttribute('tabindex', '-1'); elmt.focus({ preventScroll: true }); history.pushState(null, '', `#${id}`); }); } /* ========================================================= HELPERS ========================================================= */ function scrollToHash(paneEl, hash) { if (!hash) return; requestAnimationFrame(() => { const id = hash.replace(/^#/, ''); const target = paneEl.querySelector(`#${CSS.escape(id)}`); const area = document.getElementById('content-area'); if (target && area) area.scrollTop = target.offsetTop; }); } function el(tag, attrs = {}) { const e = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => { if (v !== undefined) e.setAttribute(k, v); }); return e; } function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function filenameToTitle(pathname) { const base = (pathname || '').split('/').pop().replace(/\.html$/, ''); // Strip Org-Roam timestamp prefix (14 digits) const clean = base.replace(/^\d{14}-/, ''); return clean.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } function normalisePathname(p) { if (!p) return ''; if (p.startsWith('/')) return p; try { return new URL(p, location.href).pathname; } catch { return '/' + p; } } // Lazy sidebar builder stub (built on demand) async function buildSidebar() {} // Pane-out animation keyframe const _s = document.createElement('style'); _s.textContent = ` @keyframes pane-out { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(16px); } } .note-loading { display: flex; align-items: center; justify-content: center; min-height: 40vh; color: var(--fg-faint); font-family: var(--font-mono); font-size: 0.85rem; letter-spacing: 0.06em; opacity: 0.6; } `; document.head.appendChild(_s);