diff --git a/public/class-partnerexpo-core-public.php b/public/class-partnerexpo-core-public.php index 1649e0f..6e070df 100644 --- a/public/class-partnerexpo-core-public.php +++ b/public/class-partnerexpo-core-public.php @@ -47,6 +47,8 @@ class Partnerexpo_Core_Public { public function searchbox_shortcode() { wp_enqueue_style( $this->plugin_name . '-searchbox-css' ); wp_enqueue_script( $this->plugin_name . '-searchbox-js' ); + wp_enqueue_style( $this->plugin_name . '-multiselect-css' ); + wp_enqueue_script( $this->plugin_name . '-multiselect-js' ); ob_start(); include plugin_dir_path( __FILE__ ) . 'partials/partnerexpo-core-public-searchbox.php'; @@ -62,12 +64,44 @@ class Partnerexpo_Core_Public { } function query_partners(WP_REST_Request $request) { + $params = $request->get_query_params(); + + if (isset($params['force_tags']) && isset($params['tags']) && $params['force_tags'] === 'true') { + $params['tags'] = str_replace(',', '+', $params['tags']); + } + $args = [ 'post_type' => 'pexpo_partners', - 'posts_per_page' => 15, + 'posts_per_page' => $params['resultsPerPage'] ?? 10, + 'paged' => $params['page'] ?? 1, 'post_status' => 'publish', + 's' => $params['q'] ?? '', + 'pexpo_tags' => $params['tags'] ?? '', ]; + switch ($params['sort'] ?? 'relevance') { + case 'date_asc': + $args['orderby'] = 'date'; + $args['order'] = 'ASC'; + break; + case 'date_desc': + $args['orderby'] = 'date'; + $args['order'] = 'DESC'; + break; + case 'title_asc': + $args['orderby'] = 'title'; + $args['order'] = 'ASC'; + break; + case 'title_desc': + $args['orderby'] = 'title'; + $args['order'] = 'DESC'; + break; + case 'relevance': + default: + // Default WordPress search sorting + break; + } + $query = new WP_Query($args); $posts = []; @@ -83,6 +117,7 @@ class Partnerexpo_Core_Public { 'date' => date('Y-m-d', strtotime($post->post_date)), 'tag' => $tags ?? null, 'url' => get_permalink($post), + 'order' => $params['sort'] ?? 'relevance', ]; } @@ -96,7 +131,7 @@ class Partnerexpo_Core_Public { * * @since 1.0.0 */ - public function enqueue_styles() { + public function register_styles() { wp_register_style( $this->plugin_name . '-searchbox-css', plugin_dir_url( __FILE__ ) . 'css/searchbox.css', @@ -104,6 +139,14 @@ class Partnerexpo_Core_Public { $this->version, 'all' ); + + wp_register_style( + $this->plugin_name . '-multiselect-css', + plugin_dir_url( __FILE__ ) . 'css/multiselect.css', + [], + $this->version, + 'all' + ); } /** @@ -111,7 +154,7 @@ class Partnerexpo_Core_Public { * * @since 1.0.0 */ - public function enqueue_scripts() { + public function register_scripts() { wp_register_script( $this->plugin_name . '-searchbox-js', plugin_dir_url( __FILE__ ) . 'js/searchbox.js', @@ -119,6 +162,14 @@ class Partnerexpo_Core_Public { $this->version, true ); + + wp_register_script( + $this->plugin_name . '-multiselect-js', + plugin_dir_url( __FILE__ ) . 'js/multiselect.js', + [], + $this->version, + true + ); } diff --git a/public/css/multiselect.css b/public/css/multiselect.css new file mode 100644 index 0000000..b4ce404 --- /dev/null +++ b/public/css/multiselect.css @@ -0,0 +1,206 @@ +:root { + --spacing-smaller: 3px; + --spacing-small: 5px; + --spacing-medium: 7px; + --spacing-large: 12px; + --font-size: 12px; + --font-size-large: 14px; + --font-size-larger: 16px; + --line-height: 16px; + --line-height-larger: 20px; + --primary-color: #40c979; + --text-color-dark: #212529; + --text-color: #585858; + --text-color-light: #1018289e; + --border-color: #bebebe; + --border-color-light: #f1f3f5; + --input-placeholder: #65727e; + --input-background: #e9e9ed; + --input-border: #dee2e6; + --input-border-active: #c1c9d0; + --input-border-invalid: #e44e4e; + --input-outline-invalid: rgba(219, 138, 138, 0.5); + --input-color: #e9e9ed; + --input-disabled: #f7f7f7; + --input-min-height: 45px; + --options-height: 40dvh; + --option-background: #f3f4f7; + --border-radius: 10px; + --icon-size: 12px; + --icon-space: 30px; + --checkbox-size: 16px; + --checkbox-radius: 4px; + --checkbox-border: #ced4da; + --checkbox-background: #fff; + --checkbox-active: #fff; + --checkbox-thickness: 2px; +} +.multi-select { + display: flex; + box-sizing: border-box; + flex-direction: column; + position: relative; + width: 100%; + user-select: none; +} +.multi-select .multi-select-header { + border: 1px solid var(--input-border); + border-radius: var(--border-radius); + padding: var(--spacing-medium) var(--spacing-large); + padding-right: var(--icon-space); + overflow: hidden; + gap: var(--spacing-medium); + min-height: var(--input-min-height); +} +.multi-select .multi-select-header::after { + content: ""; + display: block; + position: absolute; + top: 50%; + right: 15px; + transform: translateY(-50%); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23949ba3' viewBox='0 0 16 16'%3E%3Cpath d='M8 13.1l-8-8 2.1-2.2 5.9 5.9 5.9-5.9 2.1 2.2z'/%3E%3C/svg%3E"); + height: var(--icon-size); + width: var(--icon-size); +} +.multi-select .multi-select-header.multi-select-header-active { + border-color: var(--input-border-active); +} +.multi-select .multi-select-header.multi-select-header-active::after { + transform: translateY(-50%) rotate(180deg); +} +.multi-select .multi-select-header.multi-select-header-active + .multi-select-options { + display: flex; +} +.multi-select .multi-select-header .multi-select-header-placeholder { + color: var(--text-color-light); +} +.multi-select .multi-select-header .multi-select-header-option { + display: inline-flex; + align-items: center; + background-color: var(--option-background); + font-size: var(--font-size-large); + padding: var(--spacing-smaller) var(--spacing-small); + border-radius: var(--border-radius); +} +.multi-select .multi-select-header .multi-select-header-max { + font-size: var(--font-size-large); + color: var(--text-color-light); +} +.multi-select .multi-select-options { + display: none; + box-sizing: border-box; + flex-flow: wrap; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 999; + margin-top: var(--spacing-small); + padding: var(--spacing-small); + background-color: #fff; + border-radius: var(--border-radius); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + max-height: var(--options-height); + overflow-y: auto; + overflow-x: hidden; +} +.multi-select .multi-select-options::-webkit-scrollbar { + width: 5px; +} +.multi-select .multi-select-options::-webkit-scrollbar-track { + background: #f0f1f3; +} +.multi-select .multi-select-options::-webkit-scrollbar-thumb { + background: #cdcfd1; +} +.multi-select .multi-select-options::-webkit-scrollbar-thumb:hover { + background: #b2b6b9; +} +.multi-select .multi-select-options .multi-select-option, +.multi-select .multi-select-options .multi-select-all { + padding: var(--spacing-large); +} +.multi-select .multi-select-options .multi-select-option .multi-select-option-radio, +.multi-select .multi-select-options .multi-select-all .multi-select-option-radio { + background: var(--checkbox-background); + margin-right: var(--spacing-large); + height: var(--checkbox-size); + width: var(--checkbox-size); + border: 1px solid var(--checkbox-border); + border-radius: var(--checkbox-radius); +} +.multi-select .multi-select-options .multi-select-option .multi-select-option-text, +.multi-select .multi-select-options .multi-select-all .multi-select-option-text { + box-sizing: border-box; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: inherit; + font-size: var(--font-size-larger); + line-height: var(--line-height); +} +.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio, +.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio { + border-color: var(--primary-color); + background-color: var(--primary-color); +} +.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio::after, +.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio::after { + content: ""; + display: block; + width: calc(var(--checkbox-size) / 4); + height: calc(var(--checkbox-size) / 2); + border: solid var(--checkbox-active); + border-width: 0 var(--checkbox-thickness) var(--checkbox-thickness) 0; + transform: rotate(45deg) translate(50%, -25%); +} +.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-text, +.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-text { + color: var(--text-color-dark); +} +.multi-select .multi-select-options .multi-select-option:hover, .multi-select .multi-select-options .multi-select-option:active, +.multi-select .multi-select-options .multi-select-all:hover, .multi-select .multi-select-options .multi-select-all:active { + background-color: var(--option-background); +} +.multi-select .multi-select-options .multi-select-all { + border-bottom: 1px solid var(--border-color-light); + border-radius: 0; +} +.multi-select .multi-select-options .multi-select-search { + padding: var(--spacing-medium) var(--spacing-large); + border: 1px solid var(--input-border); + border-radius: var(--border-radius); + margin: 10px 10px 5px 10px; + width: 100%; + outline: none; + font-size: var(--font-size-larger); +} +.multi-select .multi-select-options .multi-select-search::placeholder { + color: var(--text-color-light); +} +.multi-select .multi-select-header, +.multi-select .multi-select-option, +.multi-select .multi-select-all { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: center; + border-radius: var(--border-radius); + cursor: pointer; + display: flex; + align-items: center; + width: 100%; + font-size: var(--font-size-larger); + color: var(--text-color-dark); +} +.multi-select.disabled { + opacity: 0.6; + pointer-events: none; + background-color: var(--input-disabled); +} +.multi-select.multi-select-invalid .multi-select-header { + border-color: var(--input-border-invalid); + outline: var(--input-outline-invalid) solid 1px; +} diff --git a/public/css/searchbox.css b/public/css/searchbox.css index fdce544..1e918a8 100644 --- a/public/css/searchbox.css +++ b/public/css/searchbox.css @@ -6,13 +6,13 @@ --panel: #ffffff; --panel2: #f2f4fb; - --stroke: rgba(16, 24, 40, .10); - --stroke2: rgba(16, 24, 40, .14); + --stroke: #1018281a; + --stroke2: #10182824; - --text: rgba(16, 24, 40, .92); - --muted: rgba(16, 24, 40, .62); + --text: #101828eb; + --muted: #1018289e; - --shadow: 0 18px 45px rgba(16,24,40,.12); + --shadow: 0 18px 45px #1018281f; --r: 14px; --cardMin: 240; @@ -21,6 +21,9 @@ --filterW: 170px; --filterWOpen: 360px; + --toggle-bg-color: #4281A4; + --toggle-nub-color: #FF686B; + --topbarH: 64px; /* Basic font reset for the component only */ @@ -369,14 +372,70 @@ } .pexpo-core-filterPanelHeader b { color: var(--text); } -.pexpo-core-jsonBox { +.pexpo-core-filterPanelBody { + display: flex; padding: 10px; - flex: 1 1 auto; + flex-direction: column; overflow:auto; min-height: 0; + gap: 10px; } -.pexpo-core-textarea { +.pexpo-core-toggle { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} + +.pexpo-core-toggle input[type="checkbox"] { + display: none; +} + +.pexpo-core-toggle label { + position: relative; + display: block; + width: 50px; + height: 25px; + cursor: pointer; +} + +.pexpo-core-toggle label::before { + content: ''; + display: block; + width: 100%; + height: 100%; + border-radius: 4px; + background: var(--toggle-bg-off); + border: 1.5px solid color-mix(in srgb, var(--toggle-bg-off), black var(--darker)); + box-sizing: border-box; + transition: all 0.2s ease-in; +} + +.pexpo-core-toggle label::after { + content: ''; + display: block; + height: 21px; + width: 21px; + border-radius: 2px; + background: var(--toggle-nub-color); + position: absolute; + top: 2px; + 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); + border: 1.5px solid color-mix(in srgb, var(--toggle-bg-on), black var(--darker)); +} + +.pexpo-core-toggle input:checked + label::after { + left: 27px; +} + +/* .pexpo-core-textarea { width: 100%; height: 210px; resize: vertical; @@ -395,7 +454,7 @@ .pexpo-core-textarea:focus { border-color: rgba(44,52,137,.45); box-shadow: 0 0 0 3px rgba(44,52,137,.14); -} +} */ .pexpo-core-hint { margin-top: 8px; diff --git a/public/js/multiselect.js b/public/js/multiselect.js new file mode 100644 index 0000000..befbac4 --- /dev/null +++ b/public/js/multiselect.js @@ -0,0 +1,375 @@ +/* + * Created by David Adams + * https://codeshack.io/multi-select-dropdown-html-javascript/ + * + * Released under the MIT license + */ +class MultiSelect { + + constructor(element, options = {}) { + let defaults = { + searchText: 'Search...', + placeholder: 'Select item(s)', + max: null, + min: null, + disabled: false, + search: true, + selectAll: true, + listAll: true, + closeListOnItemSelect: false, + name: '', + width: '', + height: '', + dropdownWidth: '', + dropdownHeight: '', + data: [], + onChange: function() {}, + onSelect: function() {}, + onUnselect: function() {}, + onMaxReached: function() {} + }; + this.options = Object.assign(defaults, options); + this.selectElement = typeof element === 'string' ? document.querySelector(element) : element; + this.originalSelectElement = this.selectElement.cloneNode(true); + for(const prop in this.selectElement.dataset) { + if (this.options[prop] !== undefined) { + if (typeof this.options[prop] === 'boolean') { + this.options[prop] = this.selectElement.dataset[prop] === 'true'; + } else { + this.options[prop] = this.selectElement.dataset[prop]; + } + } + } + this.name = this.selectElement.getAttribute('name') ? this.selectElement.getAttribute('name') : 'multi-select-' + Math.floor(Math.random() * 1000000); + if (!this.options.data.length) { + let options = this.selectElement.querySelectorAll('option'); + for (let i = 0; i < options.length; i++) { + this.options.data.push({ + value: options[i].value, + text: options[i].innerHTML, + selected: options[i].selected, + html: options[i].getAttribute('data-html') + }); + } + } + this.originalData = JSON.parse(JSON.stringify(this.options.data)); + this.element = this._template(); + this.selectElement.replaceWith(this.element); + this.outsideClickHandler = this._outsideClick.bind(this); + this._updateSelected(); + this._eventHandlers(); + if (this.options.disabled) { + this.disable(); + } + } + + _template() { + let optionsHTML = ''; + for (let i = 0; i < this.data.length; i++) { + const isSelected = this.data[i].selected; + optionsHTML += ` +
+ + ${this.data[i].html ? this.data[i].html : this.data[i].text} +
+ `; + } + let selectAllHTML = ''; + if (this.options.selectAll) { + selectAllHTML = `
+ + Select all +
`; + } + let template = ` + + `; + let element = document.createElement('div'); + element.innerHTML = template; + return element.firstElementChild; + } + + _eventHandlers() { + let headerElement = this.element.querySelector('.multi-select-header'); + const toggleDropdown = (forceClose = false) => { + if (this.element.classList.contains('disabled')) return; + if (forceClose || headerElement.classList.contains('multi-select-header-active')) { + headerElement.classList.remove('multi-select-header-active'); + this.element.setAttribute('aria-expanded', 'false'); + } else { + headerElement.classList.add('multi-select-header-active'); + this.element.setAttribute('aria-expanded', 'true'); + } + }; + this.element.querySelectorAll('.multi-select-option').forEach(option => { + option.onclick = (e) => { + e.stopPropagation(); + if (this.element.classList.contains('disabled')) return; + let selected = true; + if (!option.classList.contains('multi-select-selected')) { + if (this.options.max && this.selectedValues.length >= this.options.max) { + this.options.onMaxReached(this.options.max); + return; + } + option.classList.add('multi-select-selected'); + option.setAttribute('aria-selected', 'true'); + this.element.insertAdjacentHTML('afterbegin', ``); + this.data.find(data => data.value == option.dataset.value).selected = true; + } else { + option.classList.remove('multi-select-selected'); + option.setAttribute('aria-selected', 'false'); + this.element.querySelector(`input[value="${option.dataset.value}"]`).remove(); + this.data.find(data => data.value == option.dataset.value).selected = false; + selected = false; + } + this._updateHeader(); + if (this.options.search) { + this.element.querySelector('.multi-select-search').value = ''; + this.element.querySelectorAll('.multi-select-option').forEach(opt => opt.style.display = 'flex'); + } + if (this.options.closeListOnItemSelect) { + toggleDropdown(true); + } + this.options.onChange(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option); + if (selected) { + this.options.onSelect(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option); + } else { + this.options.onUnselect(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option); + } + this._validate(); + }; + }); + headerElement.onclick = () => toggleDropdown(); + if (this.options.search) { + let search = this.element.querySelector('.multi-select-search'); + search.oninput = () => { + this.element.querySelectorAll('.multi-select-option').forEach(option => { + const text = option.querySelector('.multi-select-option-text').innerHTML.toLowerCase(); + option.style.display = text.includes(search.value.toLowerCase()) ? 'flex' : 'none'; + }); + }; + } + if (this.options.selectAll) { + let selectAllButton = this.element.querySelector('.multi-select-all'); + selectAllButton.onclick = (e) => { + e.stopPropagation(); + if (this.element.classList.contains('disabled')) return; + let allSelected = selectAllButton.classList.contains('multi-select-selected'); + this.element.querySelectorAll('.multi-select-option').forEach(option => { + let dataItem = this.data.find(data => data.value == option.dataset.value); + if (dataItem && ((allSelected && dataItem.selected) || (!allSelected && !dataItem.selected))) { + option.click(); + } + }); + selectAllButton.classList.toggle('multi-select-selected'); + }; + } + if (this.selectElement.id && document.querySelector('label[for="' + this.selectElement.id + '"]')) { + document.querySelector('label[for="' + this.selectElement.id + '"]').onclick = () => { + toggleDropdown(); + }; + } + document.addEventListener('click', this.outsideClickHandler); + headerElement.addEventListener('keydown', (e) => { + if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) { + e.preventDefault(); + toggleDropdown(); + const firstElement = this.element.querySelector('[role="searchbox"]') || this.element.querySelector('[role="option"]'); + if (firstElement) firstElement.focus(); + } + }); + this.element.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + toggleDropdown(true); + headerElement.focus(); + } + }); + const optionsContainer = this.element.querySelector('.multi-select-options'); + optionsContainer.addEventListener('keydown', (e) => { + const currentFocused = document.activeElement; + if (currentFocused.closest('.multi-select-options')) { + if (['ArrowDown', 'ArrowUp'].includes(e.key)) { + e.preventDefault(); + const direction = e.key === 'ArrowDown' ? 'nextElementSibling' : 'previousElementSibling'; + let nextElement = currentFocused[direction]; + while (nextElement && (nextElement.style.display === 'none' || !nextElement.matches('[role="option"], [role="searchbox"]'))) { + nextElement = nextElement[direction]; + } + if (nextElement) nextElement.focus(); + } else if (['Enter', ' '].includes(e.key) && currentFocused.matches('[role="option"]')) { + e.preventDefault(); + currentFocused.click(); + } + } + }); + } + + _updateHeader() { + this.element.querySelectorAll('.multi-select-header-option, .multi-select-header-placeholder').forEach(el => el.remove()); + if (this.selectedValues.length > 0) { + if (this.options.listAll) { + this.selectedItems.forEach(item => { + const el = document.createElement('span'); + el.className = 'multi-select-header-option'; + el.dataset.value = item.value; + el.innerHTML = item.text; + this.element.querySelector('.multi-select-header').prepend(el); + }); + } else { + this.element.querySelector('.multi-select-header').insertAdjacentHTML('afterbegin', `${this.selectedValues.length} selected`); + } + } else { + this.element.querySelector('.multi-select-header').insertAdjacentHTML('beforeend', `${this.placeholder}`); + } + if (this.options.max) { + this.element.querySelector('.multi-select-header-max').innerHTML = this.selectedValues.length + '/' + this.options.max; + } + } + + _updateSelected() { this._updateHeader(); } + + _validate() { + if (this.options.min && this.selectedValues.length < this.options.min) { + this.element.classList.add('multi-select-invalid'); + } else { + this.element.classList.remove('multi-select-invalid'); + } + } + + _outsideClick(event) { + if (!this.element.contains(event.target) && !event.target.closest('label[for="' + this.selectElement.id + '"]')) { + let headerElement = this.element.querySelector('.multi-select-header'); + if (headerElement.classList.contains('multi-select-header-active')) { + headerElement.classList.remove('multi-select-header-active'); + this.element.setAttribute('aria-expanded', 'false'); + } + } + } + + select(value) { + const option = this.element.querySelector(`.multi-select-option[data-value="${value}"]`); + if (option && !option.classList.contains('multi-select-selected')) { + option.click(); + } + } + + unselect(value) { + const option = this.element.querySelector(`.multi-select-option[data-value="${value}"]`); + if (option && option.classList.contains('multi-select-selected')) { + option.click(); + } + } + + setValues(values) { + this.data.forEach(item => { + item.selected = values.includes(item.value); + }); + this.refresh(); + } + + disable() { + this.element.classList.add('disabled'); + this.element.querySelector('.multi-select-header').removeAttribute('tabindex'); + const searchInput = this.element.querySelector('.multi-select-search'); + if (searchInput) searchInput.disabled = true; + } + + enable() { + this.element.classList.remove('disabled'); + this.element.querySelector('.multi-select-header').setAttribute('tabindex', '0'); + const searchInput = this.element.querySelector('.multi-select-search'); + if (searchInput) searchInput.disabled = false; + } + + destroy() { + this.element.replaceWith(this.originalSelectElement); + document.removeEventListener('click', this.outsideClickHandler); + } + + refresh() { + const newElement = this._template(); + this.element.replaceWith(newElement); + this.element = newElement; + this._updateSelected(); + this._eventHandlers(); + this._validate(); + } + + addItem(item) { + this.options.data.push(item); + this.refresh(); + } + + addItems(items) { + this.options.data.push(...items); + this.refresh(); + } + + async fetch(url, options = {}) { + const response = await fetch(url, options); + const data = await response.json(); + this.addItems(data); + if (this.options.onload) { + this.options.onload(data, this.options); + } + } + + removeItem(value) { + this.options.data = this.options.data.filter(item => item.value !== value); + this.refresh(); + } + + clear() { + this.options.data = []; + this.refresh(); + } + + reset() { + this.data = JSON.parse(JSON.stringify(this.originalData)); + this.refresh(); + } + + selectAll() { + this.data.forEach(item => item.selected = true); + this.refresh(); + } + + get selectedValues() { return this.data.filter(d => d.selected).map(d => d.value); } + get selectedItems() { return this.data.filter(d => d.selected); } + get data() { return this.options.data; } + set data(value) { this.options.data = value; } + + set selectElement(value) { this.options.selectElement = value; } + get selectElement() { return this.options.selectElement; } + + set element(value) { this.options.element = value; } + get element() { return this.options.element; } + + set searchText(value) { this.options.searchText = value; } + get searchText() { return this.options.searchText; } + + set placeholder(value) { this.options.placeholder = value; } + get placeholder() { return this.options.placeholder; } + + set name(value) { this.options.name = value; } + get name() { return this.options.name; } + + set width(value) { this.options.width = value; } + get width() { return this.options.width; } + + set height(value) { this.options.height = value; } + get height() { return this.options.height; } + +} +document.querySelectorAll('[data-multi-select]').forEach(select => new MultiSelect(select)); diff --git a/public/js/searchbox.js b/public/js/searchbox.js index 523ec38..07d9f43 100644 --- a/public/js/searchbox.js +++ b/public/js/searchbox.js @@ -1,54 +1,67 @@ document.addEventListener('DOMContentLoaded', () => { // ---------- DOM Elements ---------- - const shell = document.getElementById("shell"); - const topbar = document.getElementById("topbar"); + const shell = document.getElementById("pexpo-core-shell"); + const topbar = document.getElementById("pexpo-core-topbar"); - const filterWrap = document.getElementById("filterWrap"); - const filterBtn = document.getElementById("filterBtn"); - const filterDrawer = document.getElementById("filterDrawer"); - const drawerBackdrop = document.getElementById("drawerBackdrop"); + 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 filterJson = document.getElementById("filterJson"); - const jsonError = document.getElementById("jsonError"); - const filterLabel = document.getElementById("filterLabel"); + // 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("q"); - const masonry = document.getElementById("masonry"); - const measure = document.getElementById("measure"); + 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("count"); - const layoutMeta = document.getElementById("layoutMeta"); - const sortSelect = document.getElementById("sortSelect"); + 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", - status: ["active", "archived"], - tags: ["ui", "mock"], - maxResults: 20 + force_tags: false, + tags: [], + resultsPerPage: 20, + page: 1, }; let activeSortKey = "relevance"; - let results = []; // Fills via Ajax + let results = []; - // ---------- Sort Definitions ---------- LEFTOVER FROM TESTING REMOVE IN PROD + // ---------- 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: { - label: "Relevance", - compare: (a, b) => (b.relevance - a.relevance) || (b.score - a.score) - }, - score_desc: { - label: "Score (high → low)", - compare: (a, b) => (b.score - a.score) - }, - date_desc: { - label: "Date (new → old)", - compare: (a, b) => new Date(b.updated) - new Date(a.updated) - }, - title_asc: { - label: "Title (A → Z)", - compare: (a, b) => (a.title || "").localeCompare(b.title || "") - } + 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 ---------- @@ -58,6 +71,13 @@ document.addEventListener('DOMContentLoaded', () => { }[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"; @@ -86,12 +106,12 @@ document.addEventListener('DOMContentLoaded', () => { } // ---------- Ajax Function ---------- - async function fetchData(query = "") { + async function fetchData() { // 1. Show loading state if needed masonry.style.opacity = "0.5"; try { - const response = await fetch(`/wp-json/pexpo/v1/query?q=${encodeURIComponent(query)}&limit=${filters.maxResults}`); + const response = await fetch(`/wp-json/pexpo/v1/query?${encodeDataToURL(filters)}`); const data = await response.json(); results = data; @@ -106,21 +126,6 @@ document.addEventListener('DOMContentLoaded', () => { } } - // FOR TESTING ONLY REMOVE IN PROD - // function computeLocalRelevance(query) { - // if (!query) { - // results.forEach(r => r.relevance = r.score); - // return; - // } - // const q = query.toLowerCase(); - // results.forEach(r => { - // let hits = 0; - // if (r.title.toLowerCase().includes(q)) hits += 50; - // if (r.desc.toLowerCase().includes(q)) hits += 20; - // r.relevance = hits + r.score; - // }); - // } - // ---------- Layout & Masonry Logic ---------- const minCard = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--cardMin")) || 240; const gap = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--gap")) || 12; @@ -215,7 +220,7 @@ document.addEventListener('DOMContentLoaded', () => { colEls[p.col].appendChild(cardEl(p.item.r)); } - layoutMeta.textContent = `${cols} cols • ${SORTS[activeSortKey]?.label || "Relevance"}`; + // layoutMeta.textContent = `${cols} oszlop • ${SORTS[activeSortKey]?.label || "Relevance"}`; } // ---------- UI Events ---------- @@ -223,22 +228,35 @@ document.addEventListener('DOMContentLoaded', () => { // Sort Dropdown if (sortSelect) { sortSelect.innerHTML = Object.entries(SORTS) - .map(([k, def]) => ``) + .map(([k, label]) => ``) .join(""); sortSelect.value = activeSortKey; sortSelect.addEventListener("change", () => { activeSortKey = sortSelect.value; - requestLayout(); + filters.sort = activeSortKey; + fetchData(); }); } // Search qInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { - fetchData(qInput.value.trim()); + 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; @@ -256,7 +274,6 @@ document.addEventListener('DOMContentLoaded', () => { shell.classList.toggle("pexpo-core-drawerOpen", open); syncTopbarHeight(); - if (open) filterJson.focus(); } filterBtn.addEventListener("click", () => setOpen(!filterDrawer.classList.contains("pexpo-core-open"))); @@ -265,30 +282,31 @@ document.addEventListener('DOMContentLoaded', () => { 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"); + // // 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 - fetchData(qInput.value.trim()); + // // 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"); - } - } + // // 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(); - } - }); + // filterJson.addEventListener("keydown", (e) => { + // if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + // e.preventDefault(); + // applyJson(); + // } + // }); // Resize Observer const ro = new ResizeObserver(() => { @@ -299,8 +317,9 @@ document.addEventListener('DOMContentLoaded', () => { ro.observe(masonry); // Initial Load + initFilters(); syncTopbarHeight(); - fetchData(""); + fetchData(); // --- Temporary Mock Response Generator (DELETE ME IN PRODUCTION) --- function simulateBackendResponse(q) { diff --git a/public/partials/partnerexpo-core-public-searchbox.php b/public/partials/partnerexpo-core-public-searchbox.php index 11ff94a..fdb522a 100644 --- a/public/partials/partnerexpo-core-public-searchbox.php +++ b/public/partials/partnerexpo-core-public-searchbox.php @@ -32,42 +32,42 @@
- -