From 3accc85817f94f8bbef01926bedad7668b02138d Mon Sep 17 00:00:00 2001 From: Duskell Date: Thu, 5 Feb 2026 22:21:31 +0100 Subject: [PATCH] oh my, searching actually works --- includes/class-partnerexpo-core.php | 6 +- .../dependency/class-query-w-relevance.php | 130 ++++ public/class-partnerexpo-core-public.php | 27 +- public/css/searchbox.css | 40 +- public/js/searchbox.js | 596 +++++++++--------- .../partnerexpo-core-public-searchbox.php | 49 +- public/search_demo.html | 1 - 7 files changed, 532 insertions(+), 317 deletions(-) create mode 100644 includes/dependency/class-query-w-relevance.php 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.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() { - // 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.title)} +
+
+ ${this.escapeHtml(r.title)} +
+
${this.escapeHtml(r.excerpt)}
+
+
+ ${ + Array.isArray(r.tag) + ? r.tag.map(element => `${this.escapeHtml(element)}`).join('') + : '' + } +
+
${this.escapeHtml(r.date)}
+
+ `; + 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, +]); + ?> -
-
-
- - +
-
0 találat
-
Elrendezés:
+
0
-
+
+ +
+
\ 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)