340 lines
10 KiB
JavaScript
340 lines
10 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// ---------- DOM Elements ----------
|
|
const shell = document.getElementById("pexpo-core-shell");
|
|
const topbar = document.getElementById("pexpo-core-topbar");
|
|
|
|
const filterWrap = document.getElementById("pexpo-core-filterWrap");
|
|
const filterBtn = document.getElementById("pexpo-core-filterBtn");
|
|
const filterDrawer = document.getElementById("pexpo-core-filterDrawer");
|
|
const drawerBackdrop = document.getElementById("pexpo-core-drawerBackdrop");
|
|
|
|
// const filterBody = document.getElementById("pexpo-core-filterBody");
|
|
// const filterLabel = document.getElementById("pexpo-core-filterLabel");
|
|
const filterFTag = document.getElementById("pexpo-core-tag-toggle");
|
|
const filterTags = document.getElementById("pexpo-core-tags");
|
|
|
|
const qInput = document.getElementById("pexpo-core-q");
|
|
const masonry = document.getElementById("pexpo-core-masonry");
|
|
const measure = document.getElementById("pexpo-core-measure");
|
|
|
|
const count = document.getElementById("pexpo-core-count");
|
|
const layoutMeta = document.getElementById("pexpo-core-layoutMeta");
|
|
const sortSelect = document.getElementById("pexpo-core-sortSelect");
|
|
|
|
// ---------- State ----------
|
|
let filters = {
|
|
q: "",
|
|
sort: "relevance",
|
|
force_tags: false,
|
|
tags: [],
|
|
resultsPerPage: 20,
|
|
page: 1,
|
|
};
|
|
|
|
let activeSortKey = "relevance";
|
|
let results = [];
|
|
|
|
// ---------- Initialize MultiSelect Library ----------
|
|
if (typeof MultiSelect !== 'undefined') {
|
|
new MultiSelect(filterTags, {
|
|
// max: 20,
|
|
search: true,
|
|
selectAll: false,
|
|
onSelect: function(value) {
|
|
if (!filters.tags.includes(value)) {
|
|
filters.tags.push(value);
|
|
}
|
|
console.log('Updated filters.tags:', filters.tags);
|
|
},
|
|
onUnselect: function(value) {
|
|
filters.tags = filters.tags.filter(tag => tag !== value);
|
|
console.log('Updated filters.tags:', filters.tags);
|
|
}
|
|
});
|
|
} else {
|
|
console.warn("MultiSelect library not found. Filters will not work without it.");
|
|
}
|
|
|
|
// ---------- Sort Definitions ----------
|
|
const SORTS = {
|
|
relevance: "Relevánsság (legjobb → legrosszabb)",
|
|
date_desc: "Dátum (új → régi)",
|
|
date_asc: "Dátum (régi → új)",
|
|
title_asc: "Cím (A → Z)",
|
|
title_desc: "Cím (Z → A)",
|
|
};
|
|
|
|
// ---------- Helpers ----------
|
|
function escapeHtml(str) {
|
|
return String(str).replace(/[&<>"']/g, s => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
}[s]));
|
|
}
|
|
|
|
function encodeDataToURL(data) {
|
|
return Object
|
|
.keys(data)
|
|
.map(value => `${value}=${encodeURIComponent(data[value])}`)
|
|
.join('&');
|
|
}
|
|
|
|
function cardEl(r) {
|
|
const div = document.createElement("div");
|
|
div.className = "pexpo-core-card";
|
|
div.innerHTML = `
|
|
<div class="pexpo-core-imageWrap">
|
|
<img src="${escapeHtml(r.image || '')}"
|
|
alt="${escapeHtml(r.title)}"
|
|
style="width:100%; border-radius:10px; object-fit:cover;" />
|
|
</div>
|
|
<div class="pexpo-core-cardTop">
|
|
<a href="${escapeHtml(r.url)}" class="pexpo-core-title">${escapeHtml(r.title)}</a>
|
|
</div>
|
|
<div class="pexpo-core-desc">${escapeHtml(r.excerpt)}</div>
|
|
<div class="pexpo-core-foot">
|
|
<div class="pexpo-core-tags">
|
|
${
|
|
r.tag != null
|
|
? r.tag.map(element => `<span class="pexpo-core-tag">${escapeHtml(element)}</span>`).join('')
|
|
: ''
|
|
}
|
|
</div>
|
|
<div>${escapeHtml(r.date)}</div>
|
|
</div>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
// ---------- Ajax Function ----------
|
|
async function fetchData() {
|
|
// 1. Show loading state if needed
|
|
masonry.style.opacity = "0.5";
|
|
|
|
try {
|
|
const response = await fetch(`/wp-json/pexpo/v1/query?${encodeDataToURL(filters)}`);
|
|
const data = await response.json();
|
|
|
|
results = data;
|
|
|
|
requestLayout();
|
|
|
|
} catch (error) {
|
|
console.error("Search failed:", error);
|
|
masonry.innerHTML = `<div style="padding:20px; color:red;">Hiba történt a keresés során.</div>`;
|
|
} finally {
|
|
masonry.style.opacity = "1";
|
|
}
|
|
}
|
|
|
|
// ---------- 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} oszlop • ${SORTS[activeSortKey]?.label || "Relevance"}`;
|
|
}
|
|
|
|
// ---------- UI Events ----------
|
|
|
|
// Sort Dropdown
|
|
if (sortSelect) {
|
|
sortSelect.innerHTML = Object.entries(SORTS)
|
|
.map(([k, label]) => `<option value="${escapeHtml(k)}">${escapeHtml(label)}</option>`)
|
|
.join("");
|
|
sortSelect.value = activeSortKey;
|
|
sortSelect.addEventListener("change", () => {
|
|
activeSortKey = sortSelect.value;
|
|
filters.sort = activeSortKey;
|
|
fetchData();
|
|
});
|
|
}
|
|
|
|
// Search
|
|
qInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") {
|
|
filters.q = qInput.value.trim();
|
|
fetchData();
|
|
}
|
|
});
|
|
|
|
// Tag Toggle
|
|
filterFTag.addEventListener("change", () => {
|
|
filters.force_tags = filterFTag.checked;
|
|
});
|
|
|
|
// Tags multiselect
|
|
filterTags.addEventListener("change", (value, text, element) => {
|
|
console.log('Change:', value, text, element);
|
|
});
|
|
|
|
|
|
// 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();
|
|
}
|
|
|
|
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
|
|
// filters.q = qInput.value.trim();
|
|
// fetchData();
|
|
|
|
// // 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
|
|
initFilters();
|
|
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<count; i++) {
|
|
arr.push({
|
|
id: i,
|
|
title: `Item ${i} - ${q || 'Random'}`,
|
|
desc: "This is a placeholder description returned from the fake backend.",
|
|
score: Math.floor(Math.random()*100),
|
|
updated: new Date().toISOString().slice(0,10),
|
|
relevance: 0
|
|
});
|
|
}
|
|
return arr;
|
|
}
|
|
}); |