418 lines
12 KiB
JavaScript
418 lines
12 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// We instantiate the class when the DOM is ready
|
|
new PartnerExpoSearch();
|
|
});
|
|
|
|
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"),
|
|
filterCompanies: document.getElementById("pexpo-core-companies"),
|
|
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"),
|
|
pageNum: document.getElementById("pexpo-core-page-number"),
|
|
pagePrev: document.getElementById("pexpo-core-page-prev"),
|
|
pageNext: document.getElementById("pexpo-core-page-next"),
|
|
};
|
|
|
|
this.state = {
|
|
filters: {
|
|
q: "",
|
|
sort: "relevance",
|
|
force_tags: false,
|
|
tags: [],
|
|
companies: [],
|
|
resultsPerPage: 20,
|
|
page: 1,
|
|
},
|
|
activeSortKey: "relevance",
|
|
total: '',
|
|
pages: 1,
|
|
results: [],
|
|
layoutQueued: false
|
|
};
|
|
|
|
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)",
|
|
};
|
|
|
|
if (this.els.shell) {
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
init() {
|
|
this.initMultiSelect();
|
|
this.initSortDropdown();
|
|
this.bindEvents();
|
|
this.syncTopbarHeight();
|
|
this.initResizeObserver();
|
|
|
|
// Initial Fetch
|
|
this.fetchData();
|
|
}
|
|
|
|
// ---------- Initialization Helpers ----------
|
|
|
|
initMultiSelect() {
|
|
if (typeof MultiSelect !== 'undefined') {
|
|
new MultiSelect(this.els.filterTags, {
|
|
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);
|
|
}
|
|
});
|
|
|
|
new MultiSelect(this.els.filterCompanies, {
|
|
search: true,
|
|
selectAll: false,
|
|
onSelect: (value) => {
|
|
if (!this.state.filters.companies.includes(value)) {
|
|
this.state.filters.companies.push(value);
|
|
}
|
|
},
|
|
onUnselect: (value) => {
|
|
this.state.filters.companies = this.state.filters.companies.filter(company => company !== 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);
|
|
}
|
|
});
|
|
|
|
this.els.pageNext.addEventListener("click", () => {
|
|
if (this.state.filters.page < this.state.pages) {
|
|
this.state.filters.page += 1;
|
|
this.fetchData();
|
|
}
|
|
});
|
|
|
|
this.els.pagePrev.addEventListener("click", () => {
|
|
if (this.state.filters.page > 1) {
|
|
this.state.filters.page -= 1;
|
|
this.fetchData();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------- 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['results'] || [];
|
|
this.state.total = data['found'] || 0;
|
|
this.state.pages = data['pages'] || 1;
|
|
console.log("Fetched data:", data['pages']);
|
|
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)));
|
|
}
|
|
|
|
measureCardHeights(cols, list) {
|
|
const w = this.els.masonry.clientWidth || 1;
|
|
const g = this.gap();
|
|
const colW = Math.floor((w - (cols - 1) * g) / cols);
|
|
|
|
this.els.measure.style.width = colW + "px";
|
|
this.els.measure.innerHTML = "";
|
|
|
|
const measured = list.map((r, idx) => {
|
|
const el = this.createCardElement(r);
|
|
this.els.measure.appendChild(el);
|
|
const h = el.offsetHeight;
|
|
this.els.measure.removeChild(el);
|
|
return { r, idx, h };
|
|
});
|
|
|
|
return { measured, colW };
|
|
}
|
|
|
|
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 = [];
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
// }
|
|
|
|
// 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;
|
|
const page = this.state.filters.page || 1;
|
|
const pages = this.state.pages;
|
|
this.els.count.textContent = this.state.total ? this.state.total : '';
|
|
this.els.pageNum.textContent = page;
|
|
this.els.masonry.innerHTML = "";
|
|
|
|
if (page >= pages) {
|
|
this.els.pageNext.setAttribute("disabled", "disabled");
|
|
} else {
|
|
this.els.pageNext.removeAttribute("disabled");
|
|
}
|
|
|
|
if (page <= 1) {
|
|
this.els.pagePrev.setAttribute("disabled", "disabled");
|
|
} else {
|
|
this.els.pagePrev.removeAttribute("disabled");
|
|
}
|
|
|
|
if (n === 0) {
|
|
if(this.els.emptyResult) {
|
|
this.els.count.textContent = this.state.total ? this.state.total : 0;
|
|
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";
|
|
this.els.masonry.appendChild(col);
|
|
colEls.push(col);
|
|
}
|
|
|
|
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);
|
|
|
|
for (const p of placements) {
|
|
colEls[p.col].appendChild(this.createCardElement(p.item.r));
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ---------- Utilities ----------
|
|
|
|
escapeHtml(str) {
|
|
return String(str).replace(/[&<>"']/g, s => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
}[s]));
|
|
}
|
|
|
|
encodeDataToURL(data) {
|
|
return Object
|
|
.keys(data)
|
|
.map(value => `${value}=${encodeURIComponent(data[value])}`)
|
|
.join('&');
|
|
}
|
|
} |