oh my, searching actually works

This commit is contained in:
2026-02-05 22:21:31 +01:00
parent 923c1d6f9a
commit 3accc85817
7 changed files with 532 additions and 317 deletions

View File

@@ -1,340 +1,366 @@
document.addEventListener('DOMContentLoaded', () => {
// ---------- DOM Elements ----------
const shell = document.getElementById("pexpo-core-shell");
const topbar = document.getElementById("pexpo-core-topbar");
// We instantiate the class when the DOM is ready
new PartnerExpoSearch();
});
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");
class PartnerExpoSearch {
constructor() {
this.els = {
shell: document.getElementById("pexpo-core-shell"),
topbar: document.getElementById("pexpo-core-topbar"),
filterWrap: document.getElementById("pexpo-core-filterWrap"),
filterBtn: document.getElementById("pexpo-core-filterBtn"),
filterDrawer: document.getElementById("pexpo-core-filterDrawer"),
drawerBackdrop: document.getElementById("pexpo-core-drawerBackdrop"),
filterFTag: document.getElementById("pexpo-core-tag-toggle"),
filterTags: document.getElementById("pexpo-core-tags"),
qInput: document.getElementById("pexpo-core-q"),
masonry: document.getElementById("pexpo-core-masonry"),
measure: document.getElementById("pexpo-core-measure"),
emptyResult: document.getElementById("pexpo-core-emptyResult"),
count: document.getElementById("pexpo-core-count"),
layoutMeta: document.getElementById("pexpo-core-layoutMeta"),
sortSelect: document.getElementById("pexpo-core-sortSelect"),
filterApply: document.getElementById("pexpo-core-filterApply"),
};
// 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");
this.state = {
filters: {
q: "",
sort: "relevance",
force_tags: false,
tags: [],
resultsPerPage: 20,
page: 1,
},
activeSortKey: "relevance",
results: [],
layoutQueued: false
};
const qInput = document.getElementById("pexpo-core-q");
const masonry = document.getElementById("pexpo-core-masonry");
const measure = document.getElementById("pexpo-core-measure");
this.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)",
};
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 => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
}[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";
if (this.els.shell) {
this.init();
}
}
// ---------- Layout & Masonry Logic ----------
const minCard = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--cardMin")) || 240;
const gap = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--gap")) || 12;
init() {
this.initMultiSelect();
this.initSortDropdown();
this.bindEvents();
this.syncTopbarHeight();
this.initResizeObserver();
// Initial Fetch
this.fetchData();
}
let layoutQueued = false;
function requestLayout() {
if (layoutQueued) return;
layoutQueued = true;
requestAnimationFrame(() => {
layoutQueued = false;
applyMasonry();
// ---------- Initialization Helpers ----------
initMultiSelect() {
if (typeof MultiSelect !== 'undefined') {
new MultiSelect(this.els.filterTags, {
placeholder: 'Címkék kiválasztása',
search: true,
selectAll: false,
onSelect: (value) => {
if (!this.state.filters.tags.includes(value)) {
this.state.filters.tags.push(value);
}
},
onUnselect: (value) => {
this.state.filters.tags = this.state.filters.tags.filter(tag => tag !== value);
}
});
} else {
console.warn("MultiSelect library not found.");
}
}
initSortDropdown() {
if (!this.els.sortSelect) return;
this.els.sortSelect.innerHTML = Object.entries(this.SORTS)
.map(([k, label]) => `<option value="${this.escapeHtml(k)}">${this.escapeHtml(label)}</option>`)
.join("");
this.els.sortSelect.value = this.state.activeSortKey;
}
initResizeObserver() {
const ro = new ResizeObserver(() => {
this.syncTopbarHeight();
this.requestLayout();
});
ro.observe(this.els.shell);
ro.observe(this.els.masonry);
}
// ---------- Event Binding ----------
bindEvents() {
// Sort
if (this.els.sortSelect) {
this.els.sortSelect.addEventListener("change", () => {
this.state.activeSortKey = this.els.sortSelect.value;
this.state.filters.sort = this.state.activeSortKey;
this.fetchData();
});
}
// Search
this.els.qInput.addEventListener("keyup", (e) => {
if (e.key === "Enter") {
this.state.filters.q = this.els.qInput.value.trim();
this.fetchData();
}
});
// Tag Toggle
this.els.filterFTag.addEventListener("change", () => {
this.state.filters.force_tags = this.els.filterFTag.checked;
});
// Drawer Toggle
this.els.filterBtn.addEventListener("click", () => {
const isOpen = this.els.filterDrawer.classList.contains("pexpo-core-open");
this.setDrawerOpen(!isOpen);
});
this.els.filterApply.addEventListener("click", () => {
this.fetchData();
this.setDrawerOpen(false);
});
this.els.drawerBackdrop.addEventListener("click", () => this.setDrawerOpen(false));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.els.filterDrawer.classList.contains("pexpo-core-open")) {
this.setDrawerOpen(false);
}
});
}
function getColumnCount() {
const w = masonry.clientWidth || masonry.getBoundingClientRect().width || 1;
const mc = minCard();
const g = gap();
// ---------- Fetch ----------
async fetchData() {
this.els.masonry.style.opacity = "0.5";
try {
const queryString = this.encodeDataToURL(this.state.filters);
const response = await fetch(`/wp-json/pexpo/v1/query?${queryString}`);
const data = await response.json();
this.state.results = data;
this.requestLayout();
} catch (error) {
console.error("Search failed:", error);
this.els.masonry.innerHTML = `<div style="padding:20px; color:red;">Hiba történt a keresés során.</div>`;
} finally {
this.els.masonry.style.opacity = "1";
}
}
// ---------- UI & Layout Methods ----------
setDrawerOpen(open) {
this.els.filterWrap.classList.toggle("pexpo-core-open", open);
this.els.filterBtn.setAttribute("aria-expanded", String(open));
this.els.filterDrawer.classList.toggle("pexpo-core-open", open);
this.els.filterDrawer.setAttribute("aria-hidden", String(!open));
this.els.drawerBackdrop.classList.toggle("pexpo-core-show", open);
this.els.shell.classList.toggle("pexpo-core-drawerOpen", open);
this.syncTopbarHeight();
}
syncTopbarHeight() {
const h = this.els.topbar.offsetHeight;
document.documentElement.style.setProperty("--topbarH", h + "px");
}
// ---------- Masonry Engine ----------
minCard() {
return Number(getComputedStyle(document.documentElement).getPropertyValue("--cardMin")) || 240;
}
gap() {
return Number(getComputedStyle(document.documentElement).getPropertyValue("--gap")) || 12;
}
requestLayout() {
if (this.state.layoutQueued) return;
this.state.layoutQueued = true;
requestAnimationFrame(() => {
this.state.layoutQueued = false;
this.applyMasonry();
});
}
getColumnCount() {
const w = this.els.masonry.clientWidth || this.els.masonry.getBoundingClientRect().width || 1;
const mc = this.minCard();
const g = this.gap();
return Math.max(1, Math.floor((w + g) / (mc + g)));
}
function measureCardHeights(cols, list) {
const w = masonry.clientWidth || 1;
const g = gap();
measureCardHeights(cols, list) {
const w = this.els.masonry.clientWidth || 1;
const g = this.gap();
const colW = Math.floor((w - (cols - 1) * g) / cols);
measure.style.width = colW + "px";
measure.innerHTML = "";
this.els.measure.style.width = colW + "px";
this.els.measure.innerHTML = "";
const measured = list.map((r, idx) => {
const el = cardEl(r);
measure.appendChild(el);
const el = this.createCardElement(r);
this.els.measure.appendChild(el);
const h = el.offsetHeight;
measure.removeChild(el);
this.els.measure.removeChild(el);
return { r, idx, h };
});
return { measured, colW };
}
function packRankAware(measured, cols, K = 6) {
packStandard(measured, cols) {
// 1. Create an array to track the current height of each column
const colHeights = new Array(cols).fill(0);
// 2. Prepare the array to store where each item goes
const placements = [];
const queue = measured.slice();
while (queue.length) {
// Find shortest column
// 3. Loop through every item strictly in order
for (const item of measured) {
// 4. Find the column that is currently the shortest
let bestCol = 0;
for (let c = 1; c < cols; c++) {
if (colHeights[c] < colHeights[bestCol]) bestCol = 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();
// 5. Place the item there
placements.push({ item: item, col: bestCol });
// 6. Update that column's height
colHeights[bestCol] += item.h + this.gap();
}
return placements;
}
function applyMasonry() {
const n = results.length || 0;
count.textContent = n;
masonry.innerHTML = "";
const cols = getColumnCount();
// packRankAware(measured, cols, K = 6) {
// const colHeights = new Array(cols).fill(0);
// const placements = [];
// const queue = measured.slice();
// Create Columns with pexpo-core class
// while (queue.length) {
// // Find shortest column
// let bestCol = 0;
// for (let c = 1; c < cols; c++) {
// if (colHeights[c] < colHeights[bestCol]) bestCol = c;
// }
// 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 + this.gap();
// }
// return placements;
// }
applyMasonry() {
const n = this.state.results.length || 0;
this.els.count.textContent = n;
this.els.masonry.innerHTML = "";
if (n === 0) {
if(this.els.emptyResult) {
let emptyClone = this.els.emptyResult.cloneNode(true);
emptyClone.style.display = "block";
this.els.masonry.appendChild(emptyClone);
} else {
this.els.masonry.innerHTML = `<div style="padding:20px; color:#666;">Nincs találat.</div>`;
}
return;
}
const cols = this.getColumnCount();
const colEls = [];
// Create Columns
for (let c = 0; c < cols; c++) {
const col = document.createElement("div");
col.className = "pexpo-core-mCol";
masonry.appendChild(col);
this.els.masonry.appendChild(col);
colEls.push(col);
}
const sliced = results.slice();
const sliced = this.state.results.slice();
const { measured } = this.measureCardHeights(cols, sliced);
// const placements = this.packRankAware(measured, cols, 6);
const placements = this.packStandard(measured, cols);
// 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));
colEls[p.col].appendChild(this.createCardElement(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();
});
createCardElement(r) {
const div = document.createElement("div");
div.className = "pexpo-core-card";
div.innerHTML = `
<div class="pexpo-core-imageWrap">
<img src="${this.escapeHtml(r.image || '')}"
alt="${this.escapeHtml(r.title)}"
style="width:100%; border-radius:10px; object-fit:cover;" />
</div>
<div class="pexpo-core-cardTop">
<a href="${this.escapeHtml(r.url)}" class="pexpo-core-title">${this.escapeHtml(r.title)}</a>
</div>
<div class="pexpo-core-desc">${this.escapeHtml(r.excerpt)}</div>
<div class="pexpo-core-foot">
<div class="pexpo-core-tags">
${
Array.isArray(r.tag)
? r.tag.map(element => `<span class="pexpo-core-tag">${this.escapeHtml(element)}</span>`).join('')
: ''
}
</div>
<div>${this.escapeHtml(r.date)}</div>
</div>
`;
return div;
}
// Search
qInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
filters.q = qInput.value.trim();
fetchData();
}
});
// ---------- Utilities ----------
// 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");
escapeHtml(str) {
return String(str).replace(/[&<>"']/g, s => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
}[s]));
}
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();
encodeDataToURL(data) {
return Object
.keys(data)
.map(value => `${value}=${encodeURIComponent(data[value])}`)
.join('&');
}
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;
}
});
}