/** * cookbook.js — fixed for the new tab-based layout * * Changes vs original: * 1. initCookbook() is exported so script.js can call it after tab content loads * 2. cellValue() scans ALL tr elements (not just tbody) — org-mode hides thead via CSS * but the rows are still in the DOM under thead * 3. watchForPanes() now watches #content-area (the new tab container) instead of .stack-track * 4. Toolbar falls back to inserting before the first .outline-3 if .outline-2 is absent */ (function () { "use strict"; // ── Core init — scoped to a content root ────────────────────────────────── function initCookbook(root) { if (root.dataset.cbInit) return; root.dataset.cbInit = "1"; const recipes = Array.from(root.querySelectorAll(".outline-3")) .filter(el => el.querySelector("table.recipe-meta")); if (!recipes.length) return; // ── Helpers ───────────────────────────────────────────────────────────── function cellValue(table, label) { // Scan ALL tr elements — org-mode thead is hidden via CSS but rows are // still in the DOM there; relying only on tbody misses them. for (const row of table.querySelectorAll("tr")) { const cells = row.querySelectorAll("td"); if (cells.length >= 2 && cells[0].textContent.trim() === label) return cells[1].textContent.trim(); } return ""; } function isCooked(table) { const v = cellValue(table, "Cooked"); return Boolean(v && v !== "not yet made"); } // ── Collect keywords ───────────────────────────────────────────────────── const allKeywords = new Set(); recipes.forEach(block => { const kw = cellValue(block.querySelector("table.recipe-meta"), "Keywords"); if (kw) kw.split(",").forEach(k => allKeywords.add(k.trim())); }); // ── Build toolbar ──────────────────────────────────────────────────────── const toolbar = document.createElement("div"); toolbar.className = "cb-toolbar"; toolbar.setAttribute("role", "search"); toolbar.innerHTML = `
`; // Insert before first .outline-2; fall back to first .outline-3 const insertTarget = root.querySelector(".outline-2") || root.querySelector(".outline-3"); if (insertTarget) { insertTarget.parentNode.insertBefore(toolbar, insertTarget); } else { // Last resort: prepend into root root.prepend(toolbar); } const searchInput = toolbar.querySelector(".cb-search"); const filterBtns = toolbar.querySelectorAll(".cb-filter-btn"); const tagsWrap = toolbar.querySelector(".cb-tags"); const countEl = toolbar.querySelector(".cb-count"); // Keyword pills Array.from(allKeywords).sort().forEach(kw => { const btn = document.createElement("button"); btn.className = "cb-tag"; btn.textContent = kw; btn.dataset.tag = kw; btn.setAttribute("aria-pressed", "false"); tagsWrap.appendChild(btn); }); // ── State ──────────────────────────────────────────────────────────────── let activeFilter = "all"; let activeTags = new Set(); let query = ""; // ── Filter engine ──────────────────────────────────────────────────────── function applyFilters() { let shown = 0; recipes.forEach(block => { const table = block.querySelector("table.recipe-meta"); const cooked = isCooked(table); const title = (block.querySelector("h3")?.textContent ?? "").toLowerCase(); const kwRaw = cellValue(table, "Keywords"); const kwLow = kwLow = kwRaw.toLowerCase(); if (activeFilter === "cooked" && !cooked) { hide(block); return; } if (activeFilter === "uncooked" && cooked) { hide(block); return; } if (activeTags.size) { const recipeKws = kwRaw.split(",").map(k => k.trim()); if ([...activeTags].some(t => !recipeKws.includes(t))) { hide(block); return; } } if (query && !title.includes(query) && !kwLow.includes(query)) { hide(block); return; } show(block); shown++; }); root.querySelectorAll(".outline-2").forEach(section => { const hasVisible = Array.from(section.querySelectorAll(".outline-3")) .some(b => b.style.display !== "none"); section.style.display = hasVisible ? "" : "none"; }); countEl.textContent = shown === recipes.length ? `${shown} recipe${shown !== 1 ? "s" : ""}` : `${shown} / ${recipes.length}`; } function hide(el) { el.style.display = "none"; } function show(el) { el.style.display = ""; } // ── Events ─────────────────────────────────────────────────────────────── searchInput.addEventListener("input", () => { query = searchInput.value.trim().toLowerCase(); applyFilters(); }); filterBtns.forEach(btn => { btn.addEventListener("click", () => { filterBtns.forEach(b => b.classList.remove("active")); btn.classList.add("active"); activeFilter = btn.dataset.filter; applyFilters(); }); }); tagsWrap.addEventListener("click", e => { const btn = e.target.closest(".cb-tag"); if (!btn) return; const tag = btn.dataset.tag; if (activeTags.has(tag)) { activeTags.delete(tag); btn.classList.remove("active"); btn.setAttribute("aria-pressed", "false"); } else { activeTags.add(tag); btn.classList.add("active"); btn.setAttribute("aria-pressed", "true"); } applyFilters(); }); // ── Badges ─────────────────────────────────────────────────────────────── recipes.forEach(block => { const table = block.querySelector("table.recipe-meta"); const h3 = block.querySelector("h3"); const cooked = isCooked(table); block.classList.add(cooked ? "cb-cooked" : "cb-uncooked"); const badge = document.createElement("span"); badge.className = "cb-badge " + (cooked ? "cb-badge-cooked" : "cb-badge-uncooked"); badge.textContent = cooked ? "made" : "not yet"; const tagSpan = h3.querySelector(".tag"); if (tagSpan) h3.insertBefore(badge, tagSpan); else h3.appendChild(badge); }); applyFilters(); } // ── Entry points ────────────────────────────────────────────────────────── function tryInitOnContent() { const content = document.getElementById("content"); if (content && content.querySelector("table.recipe-meta")) { initCookbook(content); } } function watchForPanes() { // Watch #content-area — this is where script.js appends new .tab-content divs. // Falls back to watching document.body if #content-area isn't ready yet. const getTrack = () => document.getElementById("content-area") || document.body; const observe = (track) => { const observer = new MutationObserver(mutations => { mutations.forEach(m => { m.addedNodes.forEach(node => { if (node.nodeType !== 1) return; // script.js appends .tab-content divs containing a div#content const content = (node.id === "content" ? node : null) || node.querySelector("#content, .content"); if (content && content.querySelector("table.recipe-meta")) { initCookbook(content); } }); }); }); observer.observe(track, { childList: true, subtree: true }); }; const track = document.getElementById("content-area"); if (track) { observe(track); } else { // content-area is built by script.js after DOMContentLoaded; // wait for it to appear then start observing. const bodyObserver = new MutationObserver(() => { const area = document.getElementById("content-area"); if (area) { bodyObserver.disconnect(); observe(area); } }); bodyObserver.observe(document.body, { childList: true, subtree: false }); } } // Expose for direct calls from script.js if needed: // window.initCookbook(contentEl); window.initCookbook = initCookbook; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { tryInitOnContent(); watchForPanes(); }); } else { tryInitOnContent(); watchForPanes(); } })();