diff --git a/includes/class-partnerexpo-core.php b/includes/class-partnerexpo-core.php
index 792be21..c367b3f 100644
--- a/includes/class-partnerexpo-core.php
+++ b/includes/class-partnerexpo-core.php
@@ -121,6 +121,8 @@ class Partnerexpo_Core {
*/
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'public/class-partnerexpo-core-public.php';
+ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/dependency/class-query-w-relevance.php';
+
$this->loader = new Partnerexpo_Core_Loader();
}
@@ -167,8 +169,8 @@ class Partnerexpo_Core {
private function define_public_hooks() {
$plugin_public = new Partnerexpo_Core_Public( $this->get_plugin_name(), $this->get_version() );
- $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
- $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'register_styles' );
+ $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'register_scripts' );
$this->loader->add_action( 'rest_api_init', $plugin_public, 'register_endpoint' );
$this->loader->add_action( 'init', $this, 'define_cpts' );
$this->loader->add_action( 'init', $this, 'define_taxonomies' );
diff --git a/includes/dependency/class-query-w-relevance.php b/includes/dependency/class-query-w-relevance.php
new file mode 100644
index 0000000..081820d
--- /dev/null
+++ b/includes/dependency/class-query-w-relevance.php
@@ -0,0 +1,130 @@
+orderby = $args['orderby'];
+ $this->order = 'DESC';
+ unset($args['orderby']);
+ unset($args['order']);
+ }
+
+ // perform a typical WP_Query
+ // then if we weren't using a relevance sorting, we're actually done
+ $this->process_args($args);
+ parent::__construct($args);
+ if (! $this->orderby) return;
+
+ // okay, we're doing relevance postprocessing
+ $this->initialize_relevance_scores();
+ $this->score_keyword_relevance();
+ $this->score_taxonomy_relevance();
+ $this->orderby_relevance();
+
+ // debugging; you can display this at any time to just dump the list of results
+ //$this->display_results_so_far();
+ }
+
+ // initializing all posts' relevance scores to 0
+ private function initialize_relevance_scores() {
+ foreach ($this->posts as $post) {
+ $post->relevance = 0;
+ }
+ }
+
+ private function score_keyword_relevance() {
+ if (! $this->query_vars['s']) return; // no keyword string = this is a noop
+
+ $weight_title = @$this->query_vars['relevance_scoring']['title_keyword'];
+ $weight_content = @$this->query_vars['relevance_scoring']['content_keyword'];
+ if ($weight_title == NULL) $weight_title = $this->DEFAULT_WEIGHTING_TITLE_KEYWORD;
+ if ($weight_content == NULL) $weight_content = $this->DEFAULT_WEIGHTING_CONTENT_KEYWORD;
+ // print "score_keyword_relevance() Title keyword weight {$weight_title}\n";
+ // print "score_keyword_relevance() Content keyword weight {$weight_content}\n";
+
+ $words = strtoupper(trim($this->query_vars['s']));
+ $words = preg_split('/\s+/', $words);
+
+ foreach ($this->posts as $post) {
+ $title = strtoupper($post->post_title);
+ $content = strtoupper($post->post_content);
+
+ foreach ($words as $thisword) {
+ $post->relevance += substr_count($title, $thisword) * $weight_title;
+ $post->relevance += substr_count($content, $thisword) * $weight_content;
+ }
+ }
+ }
+
+ private function score_taxonomy_relevance() {
+ if (!isset($this->query_vars['tax_query']) || !is_array($this->query_vars['tax_query'])) return; // no taxo query = skip it
+
+ // taxonomy relevance is only calculated for IN-list operations
+ // for other types of queries, all posts match that value and further scoring would be pointless
+
+ // go over each taxo and each post
+ // increase the post relevance, based on number of terms it has in common with the terms we asked about
+ // this is done one taxo at a time, so we can match terms by ID, by slug, or by name ... and so we can apply individual weighting by that taxo
+ foreach ($this->query_vars['tax_query'] as $taxo) {
+ if (strtoupper($taxo['operator']) !== 'IN' or ! is_array($taxo['terms'])) continue; // not a IN-list query, so relevance scoring is not useful for this taxo
+
+ $taxoslug = $taxo['taxonomy'];
+ $whichfield = $taxo['field'];
+ $wantterms = $taxo['terms'];
+
+ $taxo_weighting = @$this->query_vars['relevance_scoring']['tax_query'][$taxoslug];
+ if ($taxo_weighting === NULL) $taxo_weighting = $this->DEFAULT_WEIGHTING_TAXONOMY_RATIO;
+ // print "score_taxonomy_relevance() Taxo {$taxoslug} weight {$taxo_weighting}\n";
+
+ foreach ($this->posts as $post) {
+ // find number of terms in common between this post and this taxo's list
+ $terms_in_common = 0;
+ $thispostterms = get_the_terms($post->ID, $taxo['taxonomy']);
+
+ foreach ($thispostterms as $hasthisterm) {
+ if (in_array($hasthisterm->{$whichfield}, $wantterms)) $terms_in_common += 1;
+ }
+
+ // express that terms-in-common as a percentage, and add to this post's relevance score
+ $ratio = (float) $terms_in_common / sizeof($wantterms);
+ $post->relevance += ($ratio * $ratio * $taxo_weighting);
+ }
+ }
+ }
+
+ private function orderby_relevance() {
+ usort($this->posts, array($this, 'usort_sorting'));
+ }
+
+ private function display_results_so_far () { // for debugging
+ foreach ($this->posts as $post) {
+ printf('%d %s = %.1f' . "\n", $post->ID, $post->post_title, $post->relevance) . "\n";
+ }
+ }
+
+ private function usort_sorting ($p, $q) {
+ // we force DESC and only trigger if orderby==='relevance' so we can keep this simple
+ if ($p->relevance == $q->relevance) return 0;
+ return $p->relevance > $q->relevance ? -1 : 1;
+ }
+}
\ No newline at end of file
diff --git a/public/class-partnerexpo-core-public.php b/public/class-partnerexpo-core-public.php
index 6e070df..415b3bb 100644
--- a/public/class-partnerexpo-core-public.php
+++ b/public/class-partnerexpo-core-public.php
@@ -79,6 +79,28 @@ class Partnerexpo_Core_Public {
'pexpo_tags' => $params['tags'] ?? '',
];
+ if ( ! empty( $params['tags'] ) ) {
+ $tag_string = $params['tags'];
+ $operator = 'IN';
+ $delimiter = ',';
+
+ if ( strpos( $tag_string, '+' ) !== false ) {
+ $operator = 'AND';
+ $delimiter = '+';
+ }
+
+ $tag_slugs = explode( $delimiter, $tag_string );
+
+ $args['tax_query'] = [
+ [
+ 'taxonomy' => 'pexpo_tags',
+ 'field' => 'slug',
+ 'terms' => $tag_slugs,
+ 'operator' => $operator,
+ ],
+ ];
+ }
+
switch ($params['sort'] ?? 'relevance') {
case 'date_asc':
$args['orderby'] = 'date';
@@ -97,12 +119,13 @@ class Partnerexpo_Core_Public {
$args['order'] = 'DESC';
break;
case 'relevance':
+ $args['orderby'] = 'relevance';
+ break;
default:
- // Default WordPress search sorting
break;
}
- $query = new WP_Query($args);
+ $query = new WP_Query_WithRelevance($args);
$posts = [];
diff --git a/public/css/searchbox.css b/public/css/searchbox.css
index 1e918a8..88086d3 100644
--- a/public/css/searchbox.css
+++ b/public/css/searchbox.css
@@ -2,6 +2,9 @@
--accent1: #950000;
--accent2: #2c3489;
+ --darker: 30%;
+ --lighter: 40%;
+
--bg: #f6f7fb;
--panel: #ffffff;
--panel2: #f2f4fb;
@@ -20,9 +23,9 @@
--filterW: 170px;
--filterWOpen: 360px;
-
- --toggle-bg-color: #4281A4;
- --toggle-nub-color: #FF686B;
+ --toggle-bg-off: #ca0000;
+ --toggle-bg-on: #10bb2d;
+ --toggle-nub-color: #f6f7fb;
--topbarH: 64px;
@@ -272,6 +275,8 @@
flex-wrap: wrap;
gap: 6px;
row-gap: 6px;
+ padding-right: 3px;
+ max-width: 170px;
}
.pexpo-core-tag {
@@ -281,7 +286,11 @@
border: 1px solid #333333ce;
color: var(--text);
background: #99999930;
- white-space:nowrap;
+ white-space: wrap;
+ word-wrap: break-word;
+ hyphens: auto;
+ overflow: hidden;
+ max-width: 100%;
}
.pexpo-core-desc {
@@ -363,7 +372,7 @@
.pexpo-core-filterPanelHeader {
display:flex;
align-items:center;
- justify-content:space-between;
+ justify-content:center;
padding: 10px 10px;
border-bottom: 1px solid var(--stroke);
color: rgba(16,24,40,.60);
@@ -377,7 +386,7 @@
padding: 10px;
flex-direction: column;
overflow:auto;
- min-height: 0;
+ height: 100%;
gap: 10px;
}
@@ -424,7 +433,7 @@
left: 2px;
transition: all 0.2s ease-in;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
-}
+}
.pexpo-core-toggle input:checked + label::before {
background: var(--toggle-bg-on);
@@ -435,6 +444,17 @@
left: 27px;
}
+#pexpo-core-filterApply {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border-radius: 10px;
+ border: none;
+ background: var(--accent1);
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+}
+
/* .pexpo-core-textarea {
width: 100%;
height: 210px;
@@ -456,6 +476,12 @@
box-shadow: 0 0 0 3px rgba(44,52,137,.14);
} */
+#pexpo-core-emptyResult {
+ padding:20px;
+ color:var(--text);
+ display:none;
+}
+
.pexpo-core-hint {
margin-top: 8px;
color: rgba(16,24,40,.60);
diff --git a/public/js/searchbox.js b/public/js/searchbox.js
index 07d9f43..056f9f1 100644
--- a/public/js/searchbox.js
+++ b/public/js/searchbox.js
@@ -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 => ({
- "&": "&", "<": "<", ">": ">", '"': """, "'": "'"
- }[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 = `
-
-
})
-
-
- ${escapeHtml(r.excerpt)}
-
- `;
- 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 = `Hiba történt a keresés során.
`;
- } 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]) => ``)
+ .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 = `Hiba történt a keresés során.
`;
+ } 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 = `Nincs találat.
`;
+ }
+ 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]) => ``)
- .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 = `
+
+
})
+
+
+ ${this.escapeHtml(r.excerpt)}
+
+ `;
+ 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 => ({
+ "&": "&", "<": "<", ">": ">", '"': """, "'": "'"
+ }[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 'pexpo_tags',
+ 'hide_empty' => false,
+]);
+
?>
-
-
-
-
+
+
+
-
-
@@ -33,41 +39,44 @@
-
+
-
+
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/search_demo.html b/public/search_demo.html
index 3e6f26f..489e2eb 100644
--- a/public/search_demo.html
+++ b/public/search_demo.html
@@ -6,7 +6,6 @@
Mock Search UI (Masonry)