document.addEventListener('DOMContentLoaded', () => { // ---------- DOM Elements ---------- const shell = document.getElementById("shell"); const topbar = document.getElementById("topbar"); const filterWrap = document.getElementById("filterWrap"); const filterBtn = document.getElementById("filterBtn"); const filterDrawer = document.getElementById("filterDrawer"); const drawerBackdrop = document.getElementById("drawerBackdrop"); const filterJson = document.getElementById("filterJson"); const jsonError = document.getElementById("jsonError"); const filterLabel = document.getElementById("filterLabel"); const qInput = document.getElementById("q"); const masonry = document.getElementById("masonry"); const measure = document.getElementById("measure"); const count = document.getElementById("count"); const layoutMeta = document.getElementById("layoutMeta"); const sortSelect = document.getElementById("sortSelect"); // ---------- State ---------- let filters = { sort: "relevance", status: ["active", "archived"], tags: ["ui", "mock"], maxResults: 20 }; let activeSortKey = "relevance"; let results = []; // Fills via Ajax // ---------- Sort Definitions ---------- LEFTOVER FROM TESTING REMOVE IN PROD const SORTS = { relevance: { label: "Relevance", compare: (a, b) => (b.relevance - a.relevance) || (b.score - a.score) }, score_desc: { label: "Score (high → low)", compare: (a, b) => (b.score - a.score) }, date_desc: { label: "Date (new → old)", compare: (a, b) => new Date(b.updated) - new Date(a.updated) }, title_asc: { label: "Title (A → Z)", compare: (a, b) => (a.title || "").localeCompare(b.title || "") } }; // ---------- Helpers ---------- function escapeHtml(str) { return String(str).replace(/[&<>"']/g, s => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[s])); } function cardEl(r) { const div = document.createElement("div"); div.className = "pexpo-core-card"; div.innerHTML = `
${escapeHtml(r.title)}
${escapeHtml(r.title)}
${escapeHtml(r.excerpt)}
${ r.tag != null ? r.tag.map(element => `${escapeHtml(element)}`).join('') : '' }
${escapeHtml(r.date)}
`; return div; } // ---------- Ajax Function ---------- async function fetchData(query = "") { // 1. Show loading state if needed masonry.style.opacity = "0.5"; try { const response = await fetch(`/wp-json/pexpo/v1/query?q=${encodeURIComponent(query)}&limit=${filters.maxResults}`); const data = await response.json(); results = data; requestLayout(); } catch (error) { console.error("Search failed:", error); masonry.innerHTML = `
Hiba történt a keresés során.
`; } finally { masonry.style.opacity = "1"; } } // FOR TESTING ONLY REMOVE IN PROD // function computeLocalRelevance(query) { // if (!query) { // results.forEach(r => r.relevance = r.score); // return; // } // const q = query.toLowerCase(); // results.forEach(r => { // let hits = 0; // if (r.title.toLowerCase().includes(q)) hits += 50; // if (r.desc.toLowerCase().includes(q)) hits += 20; // r.relevance = hits + r.score; // }); // } // ---------- Layout & Masonry Logic ---------- const minCard = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--cardMin")) || 240; const gap = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--gap")) || 12; let layoutQueued = false; function requestLayout() { if (layoutQueued) return; layoutQueued = true; requestAnimationFrame(() => { layoutQueued = false; applyMasonry(); }); } function getColumnCount() { const w = masonry.clientWidth || masonry.getBoundingClientRect().width || 1; const mc = minCard(); const g = gap(); return Math.max(1, Math.floor((w + g) / (mc + g))); } function measureCardHeights(cols, list) { const w = masonry.clientWidth || 1; const g = gap(); const colW = Math.floor((w - (cols - 1) * g) / cols); measure.style.width = colW + "px"; measure.innerHTML = ""; const measured = list.map((r, idx) => { const el = cardEl(r); measure.appendChild(el); const h = el.offsetHeight; measure.removeChild(el); return { r, idx, h }; }); return { measured, colW }; } function packRankAware(measured, cols, K = 6) { const colHeights = new Array(cols).fill(0); const placements = []; const queue = measured.slice(); while (queue.length) { // Find shortest column let bestCol = 0; for (let c = 1; c < cols; c++) { if (colHeights[c] < colHeights[bestCol]) bestCol = c; } // Look at top K items, pick the one that fits best (tallest) // or just the next one if you prefer strict ordering. // Here we pick the tallest of the next K to fill gaps. const lookN = Math.min(K, queue.length); let pick = 0; for (let i = 1; i < lookN; i++) { if (queue[i].h > queue[pick].h) pick = i; } const picked = queue.splice(pick, 1)[0]; placements.push({ item: picked, col: bestCol }); colHeights[bestCol] += picked.h + gap(); } return placements; } function applyMasonry() { const n = results.length || 0; count.textContent = n; masonry.innerHTML = ""; const cols = getColumnCount(); // Create Columns with pexpo-core class const colEls = []; for (let c = 0; c < cols; c++) { const col = document.createElement("div"); col.className = "pexpo-core-mCol"; masonry.appendChild(col); colEls.push(col); } const sliced = results.slice(); // Measure & Pack const { measured } = measureCardHeights(cols, sliced); const placements = packRankAware(measured, cols, 6); // Render for (const p of placements) { colEls[p.col].appendChild(cardEl(p.item.r)); } layoutMeta.textContent = `${cols} cols • ${SORTS[activeSortKey]?.label || "Relevance"}`; } // ---------- UI Events ---------- // Sort Dropdown if (sortSelect) { sortSelect.innerHTML = Object.entries(SORTS) .map(([k, def]) => ``) .join(""); sortSelect.value = activeSortKey; sortSelect.addEventListener("change", () => { activeSortKey = sortSelect.value; requestLayout(); }); } // Search qInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { fetchData(qInput.value.trim()); } }); // Filter Drawer Logic function syncTopbarHeight() { const h = topbar.offsetHeight; document.documentElement.style.setProperty("--topbarH", h + "px"); } function setOpen(open) { filterWrap.classList.toggle("pexpo-core-open", open); // Updated class filterBtn.setAttribute("aria-expanded", String(open)); filterDrawer.classList.toggle("pexpo-core-open", open); // Updated class filterDrawer.setAttribute("aria-hidden", String(!open)); drawerBackdrop.classList.toggle("pexpo-core-show", open); // Updated class shell.classList.toggle("pexpo-core-drawerOpen", open); syncTopbarHeight(); if (open) filterJson.focus(); } filterBtn.addEventListener("click", () => setOpen(!filterDrawer.classList.contains("pexpo-core-open"))); drawerBackdrop.addEventListener("click", () => setOpen(false)); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && filterDrawer.classList.contains("pexpo-core-open")) setOpen(false); }); // Apply JSON Filter function applyJson() { try { const parsed = JSON.parse(filterJson.value); jsonError.classList.remove("pexpo-core-show"); filters = parsed; filterLabel.textContent = (filters.category || "Custom"); // Trigger new search with new filters fetchData(qInput.value.trim()); // Close drawer on success (optional) // setOpen(false); } catch (err) { jsonError.classList.add("pexpo-core-show"); } } filterJson.addEventListener("keydown", (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); applyJson(); } }); // Resize Observer const ro = new ResizeObserver(() => { syncTopbarHeight(); requestLayout(); }); ro.observe(shell); ro.observe(masonry); // Initial Load syncTopbarHeight(); fetchData(""); // --- Temporary Mock Response Generator (DELETE ME IN PRODUCTION) --- function simulateBackendResponse(q) { const count = filters.maxResults || 12; const arr = []; for(let i=0; i