Merged in feature/plugins-update (pull request #9)

wp plugin updates from pantheon

* wp plugin updates from pantheon
This commit is contained in:
Tony Volpe
2023-12-15 18:08:21 +00:00
parent 28c21bf9b1
commit 779393381f
577 changed files with 154305 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
.DS_Store
node_modules/
yarn.lock

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
.facetwp-facet {
margin-bottom: 40px;
}
.facetwp-facet.is-loading {
opacity: 0.6;
}
.facetwp-overlay {
position: absolute;
}
.facetwp-pager-label {
display: inline-block;
margin-right: 12px;
}
.facetwp-page {
display: inline-block;
padding: 0px 4px;
margin-right: 6px;
cursor: pointer;
}
.facetwp-page.dots {
cursor: default;
}
.facetwp-page.active {
font-weight: bold;
cursor: default;
}
/* Checkboxes */
.facetwp-type-checkboxes .facetwp-depth {
display: none;
}
.facetwp-type-checkboxes .facetwp-depth.visible {
display: inherit;
}
.facetwp-checkbox {
background: url('../images/checkbox.png') 0 50% no-repeat;
background-size: 14px 14px;
margin-bottom: 4px;
padding-left: 20px;
cursor: pointer;
}
.facetwp-checkbox.checked {
background-image: url('../images/checkbox-on.png');
}
.facetwp-checkbox.disabled,
.facetwp-radio.disabled {
opacity: 0.4;
cursor: default;
}
.facetwp-checkbox .facetwp-expand {
float: right;
}
.facetwp-display-value {
padding-right: 5px;
}
/* Radio */
.facetwp-radio {
background: url('../images/radio.png') 0 50% no-repeat;
background-size: 14px 14px;
margin-bottom: 4px;
padding-left: 20px;
cursor: pointer;
}
.facetwp-radio.checked {
background-image: url('../images/radio-on.png');
}
/* fSelect */
.facetwp-type-fselect.is-loading {
opacity: 1; /* prevent stack order issues */
}
.facetwp-type-fselect.is-loading .fs-label-wrap,
.facetwp-type-fselect.is-loading .fs-search,
.facetwp-type-fselect.is-loading .fs-no-results,
.facetwp-type-fselect.is-loading .fs-options {
opacity: 0.6;
}
.facetwp-type-fselect.is-loading .fs-option {
cursor: wait;
}
.facetwp-type-fselect .fs-wrap.fs-disabled .fs-option {
opacity: 0.4;
cursor: wait;
}
.facetwp-type-fselect .fs-option .fs-option-label {
white-space: nowrap;
}
.facetwp-type-fselect .fs-option.d1 .fs-option-label {
padding-left: 20px;
}
.facetwp-type-fselect .fs-option.d2 .fs-option-label {
padding-left: 40px;
}
.facetwp-type-fselect .fs-option.d3 .fs-option-label {
padding-left: 60px;
}
/* Hierarchy */
.facetwp-depth {
margin-left: 12px;
}
.facetwp-link {
cursor: pointer;
}
.facetwp-link.checked {
font-weight: bold;
cursor: default;
}
.facetwp-toggle {
cursor: pointer;
}
.facetwp-hidden {
display: none;
}
/* Slider */
.facetwp-slider-wrap {
padding-bottom: 15px;
}
.facetwp-slider-reset {
border: 1px solid #d9d9d9;
border-radius: 3px;
background: #fff;
box-shadow: inset 0 0 1px #fff, inset 0 1px 7px #ebebeb, 0 3px 6px -3px #bbb;
padding: 4px 8px;
cursor: pointer;
}
.facetwp-slider[data-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
}
.facetwp-slider[data-disabled="true"] .noUi-handle {
cursor: not-allowed;
}
/* Search */
.facetwp-input-wrap {
display: inline-block;
position: relative;
}
.facetwp-facet input.facetwp-search,
.facetwp-facet input.facetwp-location {
margin: 0;
padding-right: 30px;
min-width: 240px;
}
.facetwp-icon {
right: 0;
height: 100%;
line-height: 1;
position: absolute;
cursor: pointer;
opacity: 0.5;
}
.facetwp-icon:before {
display: inline-block;
content: '';
width: 30px;
height: 100%;
background: url('../images/icon-search.png') no-repeat;
background-position: 5px 50%;
background-size: 20px 20px;
}
/* Proximity */
.location-results {
position: absolute;
background: #fff;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
overflow: hidden;
width: 100%;
}
.location-result {
font-size: 11px;
border-bottom: 1px solid #ddd;
padding: 5px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #888;
}
.location-result:hover {
background-color: #f8f8f8;
}
.location-result.active {
background-color: #EBF2FE;
}
.location-result .result-main {
font-size: 13px;
color: #222;
}
.facetwp-icon.locate-me:before {
background-image: url('../images/icon-locate.png');
}
.facetwp-icon.f-reset:before {
background-image: url('../images/icon-close.png');
}
.facetwp-icon.f-loading:before {
background-image: url('../images/loading.png');
animation: spin 700ms infinite linear;
}
.location-attribution {
border-bottom: 1px solid #ddd;
padding: 5px;
}
.powered-by-google {
height: 15px;
background: url('../images/powered-by-google.png') top right no-repeat;
background-size: auto 15px;
}
/* Rating */
.facetwp-stars {
display: inline-block;
line-height: 1;
padding-right: 4px;
user-select: none;
unicode-bidi: bidi-override;
direction: rtl;
}
.facetwp-star {
cursor: pointer;
font-size: 20px;
color: #ccc;
}
.facetwp-star:hover,
.facetwp-star:hover ~ .facetwp-star,
.facetwp-star.selected,
.facetwp-star.selected ~ .facetwp-star {
color: #000;
}
.facetwp-star.selected:hover,
.facetwp-star.selected:hover ~ .facetwp-star {
color: red;
}
/* CSS animations */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Selections shortcode */
.facetwp-selections li {
display: inline-block;
line-height: 1;
}
.facetwp-selections .facetwp-selection-value {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding-right: 16px;
background-image: url('../images/icon-close.png');
background-size: 12px 12px;
background-repeat: no-repeat;
background-position: right center;
}
/* Layout builder */
@media (max-width: 480px) {
body .facetwp-template .fwpl-layout,
body .facetwp-template-static .fwpl-layout {
grid-template-columns: 1fr;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="100" height="78" xmlns="http://www.w3.org/2000/svg">
<g fill="#ffffff">
<path d="M99.991,26.908c0.017-0.176,0.009-0.353-0.02-0.527c-0.009-0.06-0.02-0.119-0.033-0.178c-0.05-0.213-0.122-0.422-0.235-0.616L89.269,7.73c-0.011-0.019-0.029-0.032-0.039-0.052c-0.011-0.014-0.014-0.032-0.021-0.047c-0.014-0.021-0.034-0.034-0.051-0.055c-0.033-0.048-0.074-0.092-0.111-0.138c-0.076-0.091-0.153-0.174-0.241-0.252c-0.048-0.042-0.095-0.083-0.145-0.12c-0.034-0.027-0.067-0.055-0.103-0.079c-0.097-0.064-0.2-0.111-0.31-0.159c-0.056-0.027-0.108-0.057-0.167-0.079c-0.041-0.015-0.082-0.031-0.124-0.044c-0.014-0.004-0.022-0.012-0.033-0.017L64.389,0.084C64.189,0.028,63.982,0,63.776,0H34.784c-0.216,0-0.431,0.032-0.638,0.093L11.637,6.698C11.626,6.7,11.617,6.71,11.606,6.713C11.571,6.724,11.535,6.736,11.5,6.75c-0.06,0.021-0.114,0.056-0.172,0.083c-0.111,0.052-0.22,0.104-0.32,0.171c-0.031,0.022-0.059,0.045-0.088,0.068c-0.052,0.038-0.098,0.083-0.146,0.126c-0.089,0.08-0.17,0.166-0.247,0.259c-0.035,0.045-0.074,0.084-0.106,0.132c-0.012,0.02-0.031,0.03-0.045,0.049c-0.012,0.019-0.015,0.04-0.025,0.059c-0.015,0.024-0.037,0.043-0.052,0.069L0.275,25.621c-0.106,0.187-0.171,0.387-0.22,0.593c-0.013,0.056-0.02,0.111-0.029,0.167C0,26.554-0.007,26.728,0.006,26.902c0.004,0.048,0.001,0.095,0.008,0.143c0.027,0.191,0.072,0.38,0.148,0.563c0.008,0.02,0.022,0.035,0.03,0.056c0.019,0.042,0.046,0.079,0.068,0.12c0.086,0.164,0.19,0.312,0.312,0.449c0.022,0.026,0.034,0.058,0.057,0.083l47.748,48.979c0.113,0.114,0.235,0.215,0.365,0.303c0.012,0.008,0.025,0.013,0.038,0.021c0.155,0.1,0.32,0.173,0.49,0.23c0.027,0.01,0.05,0.027,0.078,0.035c0.027,0.007,0.053,0.007,0.08,0.013c0.187,0.05,0.377,0.082,0.57,0.085c0,0,0.001,0,0.002,0l0,0l0,0l0,0l0,0c0,0,0.001,0,0.003,0c0.193-0.003,0.384-0.034,0.57-0.085c0.027-0.006,0.053-0.005,0.081-0.013c0.026-0.008,0.05-0.026,0.076-0.035c0.17-0.058,0.337-0.131,0.49-0.23c0.011-0.006,0.024-0.009,0.032-0.019c0.001,0,0.001,0,0.001,0c0.002,0,0.003-0.002,0.004-0.002c0.13-0.087,0.252-0.188,0.366-0.303l47.747-48.979c0.026-0.026,0.035-0.058,0.058-0.084c0.12-0.135,0.222-0.281,0.308-0.441c0.025-0.045,0.053-0.087,0.074-0.134c0.011-0.022,0.026-0.041,0.035-0.064c0.073-0.179,0.119-0.366,0.143-0.553C99.994,26.995,99.989,26.953,99.991,26.908z M81.532,12.666l-2.729,3.546L66.955,31.606l-12.8-12.06l13.981-3.514L81.532,12.666z M48.795,67.046c-3.293-7.789-8.291-19.604-11.321-26.771c-0.442-1.047-0.837-1.98-1.185-2.801c-0.032-0.079-0.066-0.156-0.097-0.23h27.611L53.173,62.381l-3.175,7.509C49.634,69.029,49.226,68.063,48.795,67.046z M43.744,20.945L32.985,31.577L18.17,12.713l26.968,6.854L43.744,20.945z M73.118,31.036l7.756-10.079l6.179-8.029l7.241,12.387L73.118,31.036z M35.109,4.539h28.354L78.49,8.756l-7.704,1.937l-21.198,5.328l-28.727-7.3L35.109,4.539z M9.674,31.099l21.464,5.834c0.108,0.255,0.24,0.568,0.404,0.958c1.244,2.941,4.275,10.111,11.682,27.625L9.674,31.099z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,104 @@
(function($) {
var last_checked = null;
if ('undefined' !== typeof FWP.hooks) {
FWP.hooks.addAction('facetwp/loaded', function() {
// checkbox, radio, fselect
$('.facetwp-checkbox, .facetwp-radio, .fs-option').each(function() {
let $el = $(this);
if (! $el.hasClass('disabled')) {
$el.attr('role', 'checkbox');
$el.attr('aria-checked', $el.hasClass('checked') ? 'true' : 'false');
$el.attr('aria-label', $el.text());
$el.attr('tabindex', 0);
}
});
// pager, show more, user selections, hierarchy
$('.facetwp-page, .facetwp-toggle, .facetwp-selection-value, .facetwp-link').each(function() {
let $el = $(this);
let label = $el.text();
if ($el.hasClass('facetwp-page')) {
label = FWP_JSON.a11y.label_page + ' ' + label;
if ($el.hasClass('next')) {
label = FWP_JSON.a11y.label_page_next;
}
else if ($el.hasClass('prev')) {
label = FWP_JSON.a11y.label_page_prev;
}
}
$el.attr('role', 'link');
$el.attr('aria-label', label);
$el.attr('tabindex', 0);
});
// dropdown, sort facet, old sort feature
$('.facetwp-type-dropdown select, .facetwp-type-sort select, .facetwp-sort-select select').each(function() {
$(this).attr('aria-label', $(this).find('option:selected').text());
});
// search, date
$('.facetwp-search, .facetwp-date').each(function() {
$(this).attr('aria-label', $(this).attr('placeholder'));
});
// checkbox group
$('.facetwp-type-checkboxes').each(function() {
let facet_name = $(this).attr('data-name');
$(this).attr('aria-label', FWP.settings.labels[facet_name]);
$(this).attr('role', 'group');
});
// fselect
$('.fs-wrap').each(function() {
$(this).attr('role', 'button');
$(this).attr('aria-haspopup', 'true');
$(this).attr('aria-expanded', $(this).hasClass('fs-open') ? 'true' : 'false');
});
$('.facetwp-type-fselect .facetwp-dropdown').attr('aria-hidden', 'true');
// pager
$('.facetwp-pager').attr('role', 'navigation');
$('.facetwp-page.active').attr('aria-current', 'true');
// focus on selection
if (null != last_checked) {
var $el = $('.facetwp-facet [data-value="' + last_checked + '"]');
if ($el.len()) {
$el.nodes[0].focus();
}
last_checked = null;
}
}, 999);
}
// keyboard support
$().on('keydown', '.facetwp-checkbox, .facetwp-radio, .facetwp-link', function(e) {
if (32 == e.keyCode || 13 == e.keyCode) {
last_checked = $(this).attr('data-value');
e.preventDefault();
this.click();
}
});
$().on('keydown', '.facetwp-page, .facetwp-toggle, .facetwp-selection-value', function(e) {
if (32 == e.keyCode || 13 == e.keyCode) {
e.preventDefault();
this.click();
}
});
// fselect - determine "aria-expanded"
function toggleExpanded(e) {
var $fs = $(e.detail[0]);
$fs.attr('aria-expanded', $fs.hasClass('fs-open') ? 'true' : 'false');
}
$().on('fs:opened', toggleExpanded);
$().on('fs:closed', toggleExpanded);
})(fUtil);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
FWP.deprecated = (old_method, new_method, ...args) => {
console.warn('FWP.' + old_method + '() has changed to FWP.' + new_method + '()');
return FWP[new_method](...args);
};
FWP.build_post_data = (...args) => FWP.deprecated('build_post_data', 'buildPostData', ...args);
FWP.build_query_string = (...args) => FWP.deprecated('build_query_string', 'buildQueryString', ...args);
FWP.fetch_data = (...args) => FWP.deprecated('fetch_data', 'fetchData', ...args);
FWP.load_from_hash = (...args) => FWP.deprecated('load_from_hash', 'loadFromHash', ...args);
FWP.parse_facets = (...args) => FWP.deprecated('parse_facets', 'parseFacets', ...args);
FWP.set_hash = (...args) => FWP.deprecated('set_hash', 'setHash', ...args);

View File

@@ -0,0 +1,6 @@
/**
* WP-JS-Hooks
* @version 1.0.0
* @author Carl Danley & 10up
*/
!function(t,n){"use strict";t.FWP=t.FWP||{},t.FWP.hooks=t.FWP.hooks||new function(){function t(t,n,r,i){var e,o,c;if(f[t][n])if(r)if(e=f[t][n],i)for(c=e.length;c--;)(o=e[c]).callback===r&&o.context===i&&e.splice(c,1);else for(c=e.length;c--;)e[c].callback===r&&e.splice(c,1);else f[t][n]=[]}function n(t,n,i,e,o){var c={callback:i,priority:e,context:o},l=f[t][n];l?(l.push(c),l=r(l)):l=[c],f[t][n]=l}function r(t){for(var n,r,i,e=1,o=t.length;e<o;e++){for(n=t[e],r=e;(i=t[r-1])&&i.priority>n.priority;)t[r]=t[r-1],--r;t[r]=n}return t}function i(t,n,r){var i,e,o=f[t][n];if(!o)return"filters"===t&&r[0];if(e=o.length,"filters"===t)for(i=0;i<e;i++)r[0]=o[i].callback.apply(o[i].context,r);else for(i=0;i<e;i++)o[i].callback.apply(o[i].context,r);return"filters"!==t||r[0]}var e=Array.prototype.slice,o={removeFilter:function(n,r){return"string"==typeof n&&t("filters",n,r),o},applyFilters:function(){var t=e.call(arguments),n=t.shift();return"string"==typeof n?i("filters",n,t):o},addFilter:function(t,r,i,e){return"string"==typeof t&&"function"==typeof r&&n("filters",t,r,i=parseInt(i||10,10),e),o},removeAction:function(n,r){return"string"==typeof n&&t("actions",n,r),o},doAction:function(){var t=e.call(arguments),n=t.shift();return"string"==typeof n&&i("actions",n,t),o},addAction:function(t,r,i,e){return"string"==typeof t&&"function"==typeof r&&n("actions",t,r,i=parseInt(i||10,10),e),o}},f={actions:{},filters:{}};return o}}(window);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,659 @@
window.FWP = (($) => {
class FacetWP {
constructor() {
this.import();
this.bindEvents();
}
import() {
if ('undefined' !== typeof FWP) {
$.each(FWP, (val, key) => this[key] = val);
}
}
init() {
var FWP = this;
this.setDefaults();
if (0 < $('.facetwp-sort').len()) {
FWP.extras.sort = 'default';
}
if (0 < $('.facetwp-pager').len()) {
FWP.extras.pager = true;
}
if (0 < $('.facetwp-per-page').len()) {
FWP.extras.per_page = 'default';
}
if (0 < $('.facetwp-counts').len()) {
FWP.extras.counts = true;
}
if (0 < $('.facetwp-selections').len()) {
FWP.extras.selections = true;
}
// Make sure there's a template
var has_template = $('.facetwp-template').len() > 0;
if (! has_template) {
var has_loop = FWP.helper.detectLoop(document.body);
if (has_loop) {
$(has_loop).addClass('facetwp-template');
}
else {
return;
}
}
var $div = $('.facetwp-template').first();
FWP.template = $div.attr('data-name') ? $div.attr('data-name') : 'wp';
// Facets inside the template?
if ($div.find('.facetwp-facet').len() > 0) {
console.error('Facets should not be inside the "facetwp-template" container');
}
FWP.hooks.doAction('facetwp/ready');
// Generate the user selections
if (FWP.extras.selections) {
FWP.hooks.addAction('facetwp/loaded', () => {
var selections = '';
var skipped = ['pager', 'reset', 'sort'];
$.each(FWP.facets, (val, key) => {
if (val.length < 1 || ! $.isset(FWP.settings.labels[key]) || skipped.includes(FWP.facet_type[key])) {
return true; // skip facet
}
var choices = val;
var $el = $('.facetwp-facet-' + key);
var facet_type = $el.attr('data-ui') || $el.attr('data-type');
choices = FWP.hooks.applyFilters('facetwp/selections/' + facet_type, choices, {
'el': $el,
'selected_values': choices
});
if (choices.length) {
if ('string' === typeof choices) {
choices = [{ value: '', label: choices }];
}
else if (! $.isset(choices[0].label)) {
choices = [{ value: '', label: choices[0] }];
}
}
var values = '';
$.each(choices, (choice) => {
values += '<span class="facetwp-selection-value" data-value="' + choice.value + '">' + FWP.helper.escapeHtml(choice.label) + '</span>';
});
selections += '<li data-facet="' + key + '"><span class="facetwp-selection-label">' + FWP.settings.labels[key] + ':</span> ' + values + '</li>';
});
if ('' !== selections) {
selections = '<ul>' + selections + '</ul>';
}
$('.facetwp-selections').html(selections);
});
}
FWP.refresh();
}
setDefaults() {
let defaults = {
'facets': {},
'template': null,
'settings': {},
'is_reset': false,
'is_refresh': false,
'is_bfcache': false,
'is_hash_click': false,
'is_load_more': false,
'auto_refresh': true,
'soft_refresh': false,
'frozen_facets': {},
'active_facet': null,
'facet_type': {},
'loaded': false,
'extras': {},
'paged': 1
};
for (var prop in defaults) {
if (!$.isset(this[prop])) {
this[prop] = defaults[prop];
}
}
}
refresh() {
FWP.is_refresh = true;
// Add the loading overlay
FWP.toggleOverlay('on');
// Load facet DOM values
if (! FWP.is_reset) {
FWP.parseFacets();
}
// Check the URL on pageload
if (! FWP.loaded) {
FWP.loadFromHash();
}
// Fire a notification event
$().trigger('facetwp-refresh');
// Trigger window.onpopstate
if (FWP.loaded && ! FWP.is_popstate && ! FWP.is_load_more) {
FWP.setHash();
}
// Preload?
if (! FWP.loaded && ! FWP.is_bfcache && $.isset(FWP_JSON.preload_data)) {
FWP.render(FWP_JSON.preload_data);
}
else {
FWP.fetchData();
}
// Unfreeze any soft-frozen facets
$.each(FWP.frozen_facets, (type, name) => {
if ('hard' !== type) {
delete FWP.frozen_facets[name];
}
});
// Cleanup
FWP.paged = 1;
FWP.soft_refresh = false;
FWP.is_refresh = false;
FWP.is_reset = false;
}
autoload() {
if (FWP.auto_refresh && ! FWP.is_refresh) {
FWP.refresh();
}
}
parseFacets() {
FWP.facets = {};
$('.facetwp-facet').each(function() {
var $this = $(this);
var facet_name = $this.attr('data-name');
var facet_type = $this.attr('data-type');
var is_ignored = $this.hasClass('facetwp-ignore');
if (null !== $this.attr('data-ui')) {
facet_type = $this.attr('data-ui');
}
// Store the facet type
FWP.facet_type[facet_name] = facet_type;
// Plugin hook
if (! is_ignored) {
FWP.hooks.doAction('facetwp/refresh/' + facet_type, $this, facet_name);
}
});
}
buildQueryString() {
var query_string = '';
// Non-FacetWP URL variables
var hash = [];
var get_str = window.location.search.replace('?', '').split('&');
$.each(get_str, (val) => {
var param_name = val.split('=')[0];
if (0 !== param_name.indexOf(FWP_JSON.prefix)) {
hash.push(val);
}
});
hash = hash.join('&');
// FacetWP URL variables
var fwp_vars = Object.assign({}, FWP.facets);
// Add pagination to the URL hash
if (1 < FWP.paged) {
fwp_vars['paged'] = FWP.paged;
}
// Add "per page" to the URL hash
if (FWP.extras.per_page && 'default' !== FWP.extras.per_page) {
fwp_vars['per_page'] = FWP.extras.per_page;
}
// Add sorting to the URL hash
if (FWP.extras.sort && 'default' !== FWP.extras.sort) {
fwp_vars['sort'] = FWP.extras.sort;
}
fwp_vars = FWP.helper.serialize(fwp_vars, FWP_JSON.prefix);
if ('' !== hash) {
query_string += hash;
}
if ('' !== fwp_vars) {
query_string += ('' !== hash ? '&' : '') + fwp_vars;
}
return query_string;
}
setHash() {
var query_string = FWP.buildQueryString();
if ('' !== query_string) {
query_string = '?' + query_string;
}
if (history.pushState) {
history.pushState(null, null, window.location.pathname + query_string);
}
// Update FWP_HTTP.get
FWP_HTTP.get = {};
window.location.search.replace('?', '').split('&').forEach((el) => {
var item = el.split('=');
if ('' != item[0]) {
FWP_HTTP.get[item[0]] = item[1];
}
});
}
loadFromHash() {
var hash = [];
var get_str = window.location.search.replace('?', '').split('&');
$.each(get_str, (val) => {
var param_name = val.split('=')[0];
if (0 === param_name.indexOf(FWP_JSON.prefix)) {
hash.push(val.replace(FWP_JSON.prefix, ''));
}
});
hash = hash.join('&');
// Reset facet values
$.each(FWP.facets, (val, key) => {
FWP.facets[key] = [];
});
FWP.paged = 1;
FWP.extras.sort = 'default';
if ('' !== hash) {
hash = hash.split('&');
$.each(hash, (chunk) => {
var obj = chunk.split('=')[0];
var val = chunk.split('=')[1];
if ('paged' === obj) {
FWP.paged = val;
}
else if ('per_page' === obj || 'sort' === obj) {
FWP.extras[obj] = val;
}
else if ('' !== val) {
var type = $.isset(FWP.facet_type[obj]) ? FWP.facet_type[obj] : '';
if ('search' === type || 'autocomplete' === type) {
FWP.facets[obj] = decodeURIComponent(val);
}
else {
FWP.facets[obj] = decodeURIComponent(val).split(',');
}
}
});
}
}
buildPostData() {
return {
'facets': FWP.facets,
'frozen_facets': FWP.frozen_facets,
'http_params': FWP_HTTP,
'template': FWP.template,
'extras': FWP.extras,
'soft_refresh': FWP.soft_refresh ? 1 : 0,
'is_bfcache': FWP.is_bfcache ? 1 : 0,
'first_load': FWP.loaded ? 0 : 1,
'paged': FWP.paged
};
}
fetchData() {
var endpoint = ('wp' === FWP.template) ? document.URL : FWP_JSON.ajaxurl;
var data = {
action: 'facetwp_refresh',
data: FWP.buildPostData()
};
var settings = {
dataType: 'text', // better JSON error handling
done: (resp) => {
try {
var json = JSON.parse(resp);
FWP.render(json);
}
catch(e) {
var pos = resp.indexOf('{"facets');
if (-1 < pos) {
var json = JSON.parse(resp.substr(pos));
FWP.render(json);
}
else {
$('.facetwp-template').text('FacetWP was unable to auto-detect the post listing');
console.log(resp);
}
}
},
fail: (err) => {
console.log(err);
}
};
settings = FWP.hooks.applyFilters('facetwp/ajax_settings', settings);
$.post(endpoint, data, settings);
}
render(response) {
FWP.response = response;
// Don't render CSS-based (or empty) templates on pageload
// The template has already been pre-loaded
if (('wp' === FWP.template || '' === response.template) && ! FWP.loaded && ! FWP.is_bfcache) {
var inject = false;
}
else {
var inject = response.template;
if ('wp' === FWP.template) {
var obj = $(response.template);
var $tpl = obj.find('.facetwp-template');
if ($tpl.len() < 1) {
var loop = FWP.helper.detectLoop(obj.nodes[0]);
if (loop) {
$tpl = $(loop).addClass('facetwp-template');
}
}
if ($tpl.len() > 0) {
var inject = $tpl.html();
}
else {
// Fallback until "loop_no_results" action is added to WP core
var inject = FWP_JSON['no_results_text'];
}
}
}
if (false !== inject) {
if (! FWP.hooks.applyFilters('facetwp/template_html', false, { 'response': response, 'html': inject })) {
$('.facetwp-template').html(inject);
}
}
// Populate each facet box
$.each(response.facets, (val, name) => {
$('.facetwp-facet-' + name).html(val);
});
// Populate the counts
if ($.isset(response.counts)) {
$('.facetwp-counts').html(response.counts);
}
// Populate the pager
if ($.isset(response.pager)) {
$('.facetwp-pager').html(response.pager);
}
// Populate the "per page" box
if ($.isset(response.per_page)) {
$('.facetwp-per-page').html(response.per_page);
if ('default' !== FWP.extras.per_page) {
$('.facetwp-per-page-select').val(FWP.extras.per_page);
}
}
// Populate the sort box
if ($.isset(response.sort)) {
$('.facetwp-sort').html(response.sort);
$('.facetwp-sort-select').val(FWP.extras.sort);
}
// Populate the settings object (iterate to preserve static facet settings)
$.each(response.settings, (val, key) => {
FWP.settings[key] = val;
});
// WP Playlist support
if ('function' === typeof WPPlaylistView) {
$('.facetwp-template .wp-playlist').each((item) => {
return new WPPlaylistView({ el: item });
});
}
// Fire a notification event
$().trigger('facetwp-loaded');
// Allow final actions
FWP.hooks.doAction('facetwp/loaded');
// Remove the loading overlay
FWP.toggleOverlay('off');
// Clear the active facet
FWP.active_facet = null;
// Detect "back-forward" cache
FWP.is_bfcache = true;
// Done loading?
FWP.loaded = true;
}
reset(facets) {
FWP.parseFacets();
var opts = {};
if ('string' === typeof facets) {
opts[facets] = '';
}
else if (Array.isArray(facets)) {
$.each(facets, (facet_name) => {
opts[facet_name] = '';
});
}
else if ('object' === typeof facets && !! facets) {
opts = facets;
}
var reset_all = Object.keys(opts).length < 1;
$.each(FWP.facets, (vals, facet_name) => {
var has_reset = $.isset(opts[facet_name]);
var selected_vals = Array.isArray(vals) ? vals : [vals];
if (has_reset && -1 < selected_vals.indexOf(opts[facet_name])) {
var pos = selected_vals.indexOf(opts[facet_name]);
selected_vals.splice(pos, 1); // splice() is mutable!
FWP.facets[facet_name] = selected_vals;
}
if (has_reset && (selected_vals.length < 1 || '' === opts[facet_name])) {
delete FWP.frozen_facets[facet_name];
}
if (reset_all || (has_reset && '' === opts[facet_name])) {
FWP.facets[facet_name] = [];
}
});
if (reset_all) {
FWP.extras.per_page = 'default';
FWP.extras.sort = 'default';
FWP.frozen_facets = {};
}
FWP.hooks.doAction('facetwp/reset');
FWP.is_reset = true;
FWP.refresh();
}
toggleOverlay(which) {
var method = ('on' === which) ? 'addClass' : 'removeClass';
$('.facetwp-facet')[method]('is-loading');
}
bindEvents() {
window.addEventListener('popstate', () => {
// Detect browser "back-foward" cache
if (FWP.is_bfcache) {
FWP.loaded = false;
}
if ((FWP.loaded || FWP.is_bfcache) && ! FWP.is_refresh && ! FWP.is_hash_click) {
FWP.is_popstate = true;
FWP.refresh();
FWP.is_popstate = false;
}
FWP.is_hash_click = false;
});
// Prevent hash clicks from triggering a refresh
$().on('click', 'a[href^="#"]', () => {
FWP.is_hash_click = true;
});
// Click on a user selection
$().on('click', '.facetwp-selections .facetwp-selection-value', function() {
if (FWP.is_refresh) {
return;
}
var facet_name = $(this).closest('li').attr('data-facet');
var facet_value = $(this).attr('data-value');
if ('' != facet_value) {
var obj = {};
obj[facet_name] = facet_value;
FWP.reset(obj);
}
else {
FWP.reset(facet_name);
}
});
// Pagination
$().on('click', '.facetwp-page[data-page]', function() {
$('.facetwp-page').removeClass('active');
$(this).addClass('active');
FWP.paged = $(this).attr('data-page');
FWP.soft_refresh = true;
FWP.refresh();
});
// Use jQuery if available for select2
var $f = ('function' === typeof jQuery) ? jQuery : fUtil;
// Per page
$f(document).on('change', '.facetwp-per-page-select', function() {
FWP.extras.per_page = $(this).val();
FWP.soft_refresh = true;
FWP.autoload();
});
// Sorting
$f(document).on('change', '.facetwp-sort-select', function() {
FWP.extras.sort = $(this).val();
FWP.soft_refresh = true;
FWP.autoload();
});
$f(() => {
this.init();
});
}
}
FacetWP.prototype.helper = {
getUrlVar: (name) => {
var name = FWP_JSON.prefix + name;
var url_vars = window.location.search.replace('?', '').split('&');
for (var i = 0; i < url_vars.length; i++) {
var item = url_vars[i].split('=');
if (item[0] === name) {
return item[1];
}
}
return false;
},
debounce: (func, wait) => {
var timeout;
return function(...args) {
var boundFunc = func.bind(this, ...args);
clearTimeout(timeout);
timeout = setTimeout(boundFunc, wait);
};
},
serialize: (obj, prefix) => {
var str = [];
var prefix = $.isset(prefix) ? prefix : '';
for (var p in obj) {
if ('' != obj[p]) { // Needs to be "!=" instead of "!=="
str.push(prefix + encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
}
}
return str.join('&');
},
escapeHtml: (text) => {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g,(m) => map[m]).trim();
},
detectLoop: (node) => {
var curNode = null;
var iterator = document.createNodeIterator(node, NodeFilter.SHOW_COMMENT, () => {
return NodeFilter.FILTER_ACCEPT; /* IE expects a function */
}, false);
while (curNode = iterator.nextNode()) {
if (8 === curNode.nodeType && 'fwp-loop' === curNode.nodeValue) {
return curNode.parentNode;
}
}
return false;
}
};
return new FacetWP();
})(fUtil);

View File

@@ -0,0 +1 @@
{"facets":[{"name":"categories","label":"Categories","type":"checkboxes","source":"tax/category","parent_term":"","hierarchical":"no","show_expanded":"no","ghosts":"no","preserve_ghosts":"no","operator":"and","orderby":"count","count":"10","soft_limit":"5"}],"templates":[{"name":"blog_posts","label":"Blog posts","query":"","template":"","layout":{"items":[{"type":"row","items":[{"type":"col","items":[{"type":"item","source":"post_title","settings":{"link":{"type":"post","href":"","target":""},"prefix":"","suffix":"","border":{"style":"none","color":"","width":{"unit":"px","top":0,"right":0,"bottom":0,"left":0}},"background_color":"","padding":{"unit":"px","top":0,"right":0,"bottom":0,"left":0},"text_color":"","text_style":{"align":"","bold":false,"italic":false},"font_size":{"unit":"px","size":0},"name":"el-fz703r","css_class":"","is_hidden":""}}],"settings":{"border":{"style":"none","color":"","width":{"unit":"px","top":0,"right":0,"bottom":0,"left":0}},"background_color":"","padding":{"unit":"px","top":0,"right":0,"bottom":0,"left":0},"text_color":"","text_style":{"align":"","bold":false,"italic":false},"font_size":{"unit":"px","size":0},"name":"el-1tvcxf","css_class":""}}],"settings":{"grid_template_columns":"1fr","border":{"style":"none","color":"","width":{"unit":"px","top":0,"right":0,"bottom":0,"left":0}},"background_color":"","padding":{"unit":"px","top":0,"right":0,"bottom":0,"left":0},"text_color":"","text_style":{"align":"","bold":false,"italic":false},"font_size":{"unit":"px","size":0},"name":"el-8cjrpw","css_class":""}}],"settings":{"num_columns":1,"grid_gap":10,"border":{"style":"none","color":"","width":{"unit":"px","top":0,"right":0,"bottom":0,"left":0}},"background_color":"","padding":{"unit":"px","top":0,"right":0,"bottom":0,"left":0},"text_color":"","text_style":{"align":"","bold":false,"italic":false},"font_size":{"unit":"px","size":0},"name":"el-hkhimk","css_class":"","custom_css":""}},"query_obj":{"post_type":[{"label":"Posts","value":"post"}],"posts_per_page":10,"orderby":[{"key":"date","order":"DESC","type":"CHAR"}],"filters":[]},"modes":{"display":"visual","query":"visual"}}],"settings":{"thousands_separator":",","decimal_separator":".","prefix":"_"}}

View File

@@ -0,0 +1,29 @@
.fcomplete-wrap {
position: absolute;
border: 1px solid #ddd;
border-top: none;
background-color: #fff;
max-width: 400px;
}
.fcomplete-result,
.fcomplete-status {
padding: 6px 8px;
}
.fcomplete-result {
cursor: pointer;
}
.fcomplete-result:hover {
background-color: #f5f5f5;
}
.fcomplete-status {
font-size: 13px;
font-style: italic;
}
.fcomplete-hidden {
display: none;
}

View File

@@ -0,0 +1,283 @@
window.fComplete = (() => {
class fComplete {
constructor(selector, options) {
let that = this;
var defaults = {
data: [],
minChars: 3,
maxResults: 10,
searchDelay: 200,
loadingText: 'Loading...',
minCharsText: 'Enter {n} or more characters',
noResultsText: 'No results',
beforeRender: null,
onSelect: null
};
that.settings = Object.assign({}, defaults, options);
that.settings.minChars = Math.max(1, that.settings.minChars);
that.settings.maxResults = Math.max(1, that.settings.maxResults);
that.settings.searchDelay = Math.max(0, that.settings.searchDelay);
if ('string' === typeof selector) {
var nodes = Array.from(document.querySelectorAll(selector));
}
else if (selector instanceof Node) {
var nodes = [selector];
}
else if (Array.isArray(selector)) {
var nodes = selector;
}
else {
var nodes = [];
}
if ('undefined' === typeof window.fCompleteInit) {
window.fCompleteInit = {
lastFocus: null,
eventsBound: true
};
that.bindEvents();
}
nodes.forEach((input) => {
that.input = input;
input.fcomplete = that;
input.classList.add('fcomplete-enabled');
that.create();
});
}
create() {
var that = this;
var html = `
<div class="fcomplete-wrap fcomplete-hidden">
<div class="fcomplete-status"></div>
<div class="fcomplete-results"></div>
</div>
`;
var rect = that.input.getBoundingClientRect();
var tpl = document.createElement('template');
tpl.innerHTML = html;
var wrap = tpl.content.querySelector('.fcomplete-wrap');
wrap.style.minWidth = rect.width + 'px';
that.input.parentNode.insertBefore(wrap, that.input.nextSibling);
// add a relationship link
that.input._rel = wrap;
wrap._rel = that.input;
}
destroy() {
this.input._rel.remove();
delete this.input._rel;
}
reload() {
this.destroy();
this.create();
}
open() {
this.input._rel.classList.remove('fcomplete-hidden');
}
close() {
window.fCompleteInit.lastFocus = null;
this.input._rel.classList.add('fcomplete-hidden');
}
setStatus(text) {
var text = text.replace('{n}', this.settings.minChars);
var node = this.input._rel.querySelector('.fcomplete-status');
node.innerHTML = text;
var method = (text) ? 'remove' : 'add';
node.classList[method]('fcomplete-hidden');
}
render(data) {
var data = (this.settings.beforeRender) ? this.settings.beforeRender(data) : data;
var wrap = this.input._rel;
if (data.length) {
var html = '';
var len = Math.min(data.length, this.settings.maxResults);
for (var i = 0; i < len; i++) {
html += `<div class="fcomplete-result" data-value="${data[i].value}" tabindex="-1">${data[i].label}</div>`;
}
wrap.querySelector('.fcomplete-results').innerHTML = html;
this.setStatus('');
}
else {
wrap.querySelector('.fcomplete-results').innerHTML = '';
this.setStatus(this.settings.noResultsText);
}
this.input.fcomplete.open();
}
getAdjacentSibling(which) {
var that = this;
var which = which || 'next';
var sibling = window.fCompleteInit.lastFocus;
var selector = '.fcomplete-result';
if (sibling) {
sibling = sibling[which + 'ElementSibling'];
while (sibling) {
if (sibling.matches(selector)) break;
sibling = sibling[which + 'ElementSibling'];
}
return sibling;
}
else if ('next' == which) {
sibling = that.input._rel.querySelector(selector);
}
return sibling;
}
debounce(func, wait) {
var timeout;
return (...args) => {
var boundFunc = func.bind(this, ...args);
clearTimeout(timeout);
timeout = setTimeout(boundFunc, wait);
}
}
trigger(eventName, ...args) {
document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]}));
}
on(eventName, elementSelector, handler) {
document.addEventListener(eventName, function(e) {
// loop parent nodes from the target to the delegation node
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
handler.call(target, e);
break;
}
}
}, false);
}
bindEvents() {
let that = this;
that.on('click', '*', function(e) {
var wrap = this.closest('.fcomplete-wrap');
var isInput = this.classList.contains('fcomplete-enabled');
if (isInput) {
var input = this;
var settings = input.fcomplete.settings;
var status = (settings.minChars > input.value.length) ? settings.minCharsText : '';
input.fcomplete.setStatus(status);
this.fcomplete.open();
}
else if (!wrap && !isInput) {
document.querySelectorAll('.fcomplete-wrap').forEach((node) => node.classList.add('fcomplete-hidden'));
}
});
that.on('click', '.fcomplete-result', function(e) {
var wrap = e.target.closest('.fcomplete-wrap');
var input = wrap._rel;
input.value = e.target.getAttribute('data-value');
if (typeof input.fcomplete.settings.onSelect === 'function') {
input.fcomplete.settings.onSelect();
}
input.fcomplete.close();
});
that.on('keydown', '*', function(e) {
var wrap = this.closest('.fcomplete-wrap');
var isInput = this.classList.contains('fcomplete-enabled');
if (!wrap && !isInput) return;
var input = (wrap) ? wrap._rel : this;
wrap = (wrap) ? wrap : input._rel;
if (-1 < [13, 38, 40, 27].indexOf(e.which)) {
e.preventDefault();
}
if (13 == e.which) { // enter
if (this.classList.contains('fcomplete-result')) {
this.click();
}
}
else if (38 == e.which) { // up
if (this.classList.contains('fcomplete-result')) {
var sibling = wrap._rel.fcomplete.getAdjacentSibling('previous');
window.fCompleteInit.lastFocus = sibling; // stop at the search box
(sibling) ? sibling.focus() : input.focus();
}
}
else if (40 == e.which) { // down
if (this === input) {
var firstResult = wrap.querySelector('.fcomplete-result');
if (firstResult) {
firstResult.focus();
window.fCompleteInit.lastFocus = firstResult;
}
}
else if (this.classList.contains('fcomplete-result')) {
var sibling = wrap._rel.fcomplete.getAdjacentSibling('next');
if (sibling) {
sibling.focus();
window.fCompleteInit.lastFocus = sibling; // stop at the bottom
}
}
}
else if (9 == e.which || 27 == e.which) { // tab, esc
wrap._rel.fcomplete.close();
}
});
that.on('keyup', '.fcomplete-enabled', that.debounce(function(e) {
if (-1 < [13, 38, 40, 27].indexOf(e.which)) {
return;
}
var input = e.target;
var settings = input.fcomplete.settings;
if (settings.minChars <= input.value.length) {
if (Array.isArray(settings.data)) {
input.fcomplete.render(settings.data);
}
else if (typeof settings.data === 'function') {
input.fcomplete.setStatus(settings.loadingText);
settings.data();
}
}
else {
input.fcomplete.render([]);
input.fcomplete.setStatus(settings.minCharsText);
}
}, that.settings.searchDelay));
}
}
var $ = (selector, options) => new fComplete(selector, options);
return $;
})();

View File

@@ -0,0 +1,7 @@
Copyright 2021 FacetWP, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,87 @@
.fdate-input {
outline: none;
}
.fdate-wrap {
width: 300px;
display: none;
background: #fff;
border-radius: 5px;
border: 1px solid #ddd;
font-size: 14px;
user-select: none;
-webkit-user-select: none;
z-index: 10000;
}
.fdate-wrap.opened {
display: block;
}
.fdate-wrap .disabled {
opacity: 0.1;
}
.fdate-nav {
display: grid;
grid-template-columns: 1fr 5fr 1fr;
}
.fdate-nav > div,
.fdate-clear {
padding: 10px 0;
text-align: center;
cursor: pointer;
}
.fdate-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
text-align: center;
}
.fdate-grid.grid-day {
grid-template-columns: repeat(7, 1fr);
}
.fdate-grid > div {
padding: 20px 0;
opacity: 0.3;
}
.fdate-grid > div:hover {
background-color: #ddd;
cursor: pointer;
}
.fdate-grid .fdate-day {
padding: 8px 0;
}
.fdate-grid .weekday,
.fdate-grid .inner {
opacity: 1;
}
.fdate-grid .today {
background-color: #F8F8F8;
}
.fdate-grid .selected {
background-color: #DDD6FE;
}
.fdate-day.weekday {
font-weight: bold;
padding-top: 0;
}
.fdate-grid .weekday:hover,
.fdate-grid .disabled:hover {
background-color: transparent;
cursor: default;
}
.fdate-wrap .disabled:hover {
cursor: not-allowed;
}

View File

@@ -0,0 +1,679 @@
window.fDate = (() => {
var qs = (selector) => document.querySelector(selector);
var isset = (input) => 'undefined' !== typeof input;
var ymd = (...args) => {
var d = new Date(...args);
var zeroed = (num) => (num > 9) ? num : '0' + num;
// toJSON() produces unexpected results due to timezones
return d.getFullYear() + '-' + zeroed(d.getMonth() + 1) + '-' + zeroed(d.getDate());
};
class fDate {
constructor(selector, options) {
let that = this;
var defaults = {
i18n: {
weekdays_short: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
months_short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
clearText: 'Clear',
firstDayOfWeek: 0
},
minDate: '',
maxDate: '',
altFormat: '',
onChange: null
};
that.settings = Object.assign({}, defaults, options);
if ('string' === typeof selector) {
var inputs = document.querySelectorAll(selector);
}
else if (selector instanceof Node) {
var inputs = [selector];
}
else {
var inputs = selector;
}
if (inputs.length) {
inputs.forEach(function(input) {
input.setAttribute('readonly', 'readonly');
if ('' !== that.settings.altFormat) {
that.el = input;
let altInput = input.cloneNode();
altInput.classList.add('fdate-alt-input');
altInput.value = that.getAltDate();
altInput._input = input;
input._altInput = altInput;
input.setAttribute('type', 'hidden');
input.parentNode.insertBefore(altInput, input.nextSibling); // append()
}
input.classList.add('fdate-input');
input._input = input;
input.fdate = {
settings: that.settings,
refresh() {
input.click();
},
open() {
input.click();
},
close() {
that.setCalVisibility('hide');
},
clear() {
input.value = '';
if (isset(input._altInput)) {
input._altInput.value = '';
}
that.triggerEvent('onChange');
},
destroy() {
input.classList.remove('fdate-input');
delete input._altInput;
delete input._input;
delete input.fdate;
}
};
});
}
if (null === qs('.fdate-wrap')) {
this.initCalendar();
this.bindEvents();
}
}
initCalendar() {
var html = `
<div class="fdate-wrap">
<div class="fdate-nav">
<div class="fdate-nav-prev">&lt;</div>
<div class="fdate-nav-label" tabindex="-1"></div>
<div class="fdate-nav-next">&gt;</div>
</div>
<div class="fdate-grid"></div>
<div class="fdate-clear" tabindex="-1">${this.settings.i18n.clearText}</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', html);
}
setInput(input) {
this.el = input;
this.mode = 'day';
this.settings = input.fdate.settings;
this.setDateBounds();
// valid YYYY-MM-DD?
if (null !== input.value.match(/^\d{4}-\d{2}-\d{2}$/)) {
var date_str = input.value;
}
// use the min date or today, whichever is higher
else {
var today = ymd();
var date_str = (this.min.str < today) ? today : this.min.str;
}
// rewind the calendar if beyond the maxDate
if (date_str < this.max.str) {
var temp_date = new Date(date_str + 'T00:00');
this.year = temp_date.getFullYear();
this.month = temp_date.getMonth();
}
else {
this.year = this.max.year;
this.month = this.max.month;
}
}
setDateBounds() {
let min = this.settings.minDate || '1000-01-01';
let max = this.settings.maxDate || '3000-01-01';
let minDate = new Date(min + 'T00:00');
let maxDate = new Date(max + 'T00:00');
this.min = {
year: minDate.getFullYear(),
month: minDate.getMonth(),
str: min
};
this.max = {
year: maxDate.getFullYear(),
month: maxDate.getMonth(),
str: max
};
}
isInBounds(val) {
if ('year' == this.mode) {
let year = parseInt(val);
if (year < this.min.year || year > this.max.year) {
return false;
}
}
else if ('month' == this.mode) {
let month = parseInt(val);
let valStr = ymd(this.year, month).substr(0, 7);
let monthMin = this.min.str.substr(0, 7);
let monthMax = this.max.str.substr(0, 7);
if (valStr < monthMin || valStr > monthMax) {
return false;
}
}
else if ('day' == this.mode) {
if (val < this.min.str || val > this.max.str) {
return false;
}
}
return true;
}
isNavAllowed(type) {
if ('year' == this.mode) {
let decade = parseInt(this.year.toString().substr(0, 3) + '0');
return ('next' == type) ?
decade < parseInt(this.max.str.substr(0, 4)) :
decade > parseInt(this.min.str.substr(0, 4));
}
else if ('month' == this.mode) {
return ('next' == type) ?
ymd(this.year + 1, 0, 0) < this.max.str :
ymd(this.year, 0) > this.min.str;
}
else if ('day' == this.mode) {
return ('next' == type) ?
ymd(this.year, this.month + 1, 0) < this.max.str :
ymd(this.year, this.month) > this.min.str;
}
}
setDisplay(which) {
var that = this;
this.mode = which;
qs('.fdate-grid').classList.remove('grid-day');
// show or hide the nav arrows
qs('.fdate-nav-prev').classList.add('disabled');
qs('.fdate-nav-next').classList.add('disabled');
if (that.isNavAllowed('prev')) {
qs('.fdate-nav-prev').classList.remove('disabled');
}
if ( that.isNavAllowed('next')) {
qs('.fdate-nav-next').classList.remove('disabled');
}
// month
if ('month' == which) {
var output = '';
this.settings.i18n.months_short.forEach(function(item, index) {
var css = that.isInBounds(index) ? ' inner' : ' disabled';
output += '<div class="fdate-month' + css + '" data-value="' + index + '" tabindex="-1">' + item + '</div>';
});
qs('.fdate-grid').innerHTML = output;
qs('.fdate-nav-label').innerHTML = this.year;
}
// year
else if ('year' == which) {
var output = '';
var decade = parseInt(this.year.toString().substr(0, 3) + '0');
for (var i = 0; i < 10; i++) {
var css = that.isInBounds(decade + i) ? ' inner' : ' disabled';
output += '<div class="fdate-year' + css + '" data-value="' + (decade + i) + '" tabindex="-1">' + (decade + i) + '</div>';
}
qs('.fdate-grid').innerHTML = output;
var prefix = this.year.toString().substr(0, 3);
var decade = prefix + '0 - ' + prefix + '9';
qs('.fdate-nav-label').innerHTML = decade;
}
// day
else {
qs('.fdate-grid').classList.add('grid-day');
var output = '';
var days = this.generateDays(this.year, this.month);
days.forEach(function(item) {
output += '<div class="fdate-day' + item.class + '" data-value="' + item.value + '" tabindex="-1">' + item.text + '</div>';
});
qs('.fdate-grid').innerHTML = output;
qs('.fdate-nav-label').innerHTML = this.settings.i18n.months[this.month] + ' ' + this.year;
}
}
generateDays(year, month) {
let that = this;
var output = [];
let i18n = that.settings.i18n;
let weekdays = i18n.weekdays_short;
let firstDayOfWeek = i18n.firstDayOfWeek; // 0 = Sunday
let firstDayNum = new Date(year, month).getDay(); // between 0 and 6
let offset = firstDayNum - firstDayOfWeek;
offset = (offset < 0) ? 7 + offset : offset; // negative offset (e.g. August 2021)
let num_days = new Date(year, month + 1, 0).getDate();
let today = ymd();
// shift weekdays according to firstDayOfWeek
if (0 < firstDayOfWeek) {
let temp = JSON.parse(JSON.stringify(weekdays));
let append = temp.splice(0, firstDayOfWeek);
weekdays = temp.concat(append);
}
// get weekdays
weekdays.forEach(function(item) {
output.push({
text: item,
value: '',
class: ' weekday'
});
});
// get days from the previous month
if (0 < offset) {
let year_prev = (0 == month) ? year - 1 : year;
let month_prev = (0 == month) ? 11 : month - 1;
let num_days_prev = new Date(year_prev, month_prev + 1, 0).getDate();
for (var i = (num_days_prev - offset + 1); i <= num_days_prev; i++) {
var val = ymd(year_prev, month_prev, i);
var css = that.isInBounds(val) ? '' : ' disabled';
output.push({
text: i,
value: val,
class: css
});
}
}
// get days from the current month
for (var i = 1; i <= num_days; i++) {
var val = ymd(year, month, i);
if ( that.isInBounds(val)) {
var css = ' inner';
css += (val == today) ? ' today' : '';
css += (val == this.el.value) ? ' selected' : '';
}
else {
var css = ' disabled';
}
output.push({
text: i,
value: val,
class: css
});
}
// get days from the next month
let year_next = (11 == month) ? year + 1 : year;
let month_next = (11 == month) ? 0 : month + 1;
let num_filler = 42 - num_days - offset;
for (var i = 1; i <= num_filler; i++) {
var val = ymd(year_next, month_next, i);
var css = that.isInBounds(val) ? '' : ' disabled';
output.push({
text: i,
value: val,
class: css
});
}
return output;
}
adjustDate(increment, unit) {
var temp_year = ('year' == unit) ? this.year + increment : this.year;
var temp_month = ('month' == unit) ? this.month + increment : this.month;
var temp_date = new Date(temp_year, temp_month);
this.year = temp_date.getFullYear();
this.month = temp_date.getMonth();
}
on(eventName, elementSelector, handler) {
document.addEventListener(eventName, function(e) {
// loop parent nodes from the target to the delegation node
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
handler.call(target, e);
break;
}
}
}, false);
}
getAltDate() {
let that = this;
if ('' === that.el.value) {
return '';
}
let date_array = that.el.value.split('-');
let format_array = that.settings.altFormat.split('');
let output = '';
let escaped = false;
format_array.forEach(function(token) {
if ('\\' === token) {
escaped = true;
return;
}
output += escaped ? token : that.parseDateToken(token, date_array);
escaped = false;
});
return output;
}
parseDateToken(token, date_array) {
let i18n = this.settings.i18n;
let tokens = {
'd': () => date_array[2],
'j': () => parseInt(date_array[2]),
'm': () => date_array[1],
'n': () => parseInt(date_array[1]),
'F': () => i18n.months[parseInt(date_array[1]) - 1],
'M': () => i18n.months_short[parseInt(date_array[1]) - 1],
'y': () => date_array[0].substring(2),
'Y': () => date_array[0]
};
return isset(tokens[token]) ? tokens[token]() : token;
}
setPosition(input) {
let wrap = qs('.fdate-wrap');
let inputBounds = input.getBoundingClientRect();
let calendarWidth = wrap.getBoundingClientRect().width;
let calendarHeight = wrap.getBoundingClientRect().height;
let distanceFromRight = document.body.clientWidth - inputBounds.left;
let distanceFromBottom = document.body.clientHeight - inputBounds.bottom;
let showOnTop = (distanceFromBottom < calendarHeight && inputBounds.top > calendarHeight);
let showOnLeft = (distanceFromRight < calendarWidth && inputBounds.left > calendarWidth);
let top = window.pageYOffset + inputBounds.top + (!showOnTop ? input.offsetHeight + 2 : -calendarHeight - 2);
let left = window.pageXOffset + inputBounds.left;
let right = window.pageXOffset + inputBounds.right - calendarWidth;
let pixels = showOnLeft ? right : left;
wrap.style.position = 'absolute';
wrap.style.top = top + 'px';
wrap.style.left = pixels + 'px';
}
setCalVisibility(which) {
var wrap = qs('.fdate-wrap');
if ('hide' === which) {
if (wrap.classList.contains('opened')) {
wrap.classList.remove('opened');
}
}
else {
if (! wrap.classList.contains('opened')) {
wrap.classList.add('opened');
}
}
}
triggerEvent(name) {
if (typeof this.settings[name] === 'function') {
this.settings[name](this);
}
}
bindEvents() {
var that = this;
that.on('click', '.fdate-day:not(.disabled):not(.weekday)', function(e) {
that.el.value = e.target.getAttribute('data-value');
if (isset(that.el._altInput)) {
that.el._altInput.value = that.getAltDate();
}
that.triggerEvent('onChange');
that.setCalVisibility('hide');
e.stopImmediatePropagation(); // important
});
that.on('click', '.fdate-month:not(.disabled)', function(e) {
that.month = parseInt(e.target.getAttribute('data-value'));
that.setDisplay('day');
e.stopImmediatePropagation(); // important
});
that.on('click', '.fdate-year:not(.disabled)', function(e) {
that.year = parseInt(e.target.getAttribute('data-value'));
that.setDisplay('month');
e.stopImmediatePropagation(); // important
});
that.on('click', '.fdate-nav-prev:not(.disabled)', function() {
var incr = ('year' == that.mode) ? -10 : -1;
var unit = ('day' == that.mode) ? 'month' : 'year';
that.adjustDate(incr, unit);
that.setDisplay(that.mode);
});
that.on('click', '.fdate-nav-next:not(.disabled)', function() {
var incr = ('year' == that.mode) ? 10 : 1;
var unit = ('day' == that.mode) ? 'month' : 'year';
that.adjustDate(incr, unit);
that.setDisplay(that.mode);
});
that.on('click', '.fdate-nav-label', function() {
if ('day' == that.mode) {
that.setDisplay('month');
}
else if ('month' == that.mode) {
that.setDisplay('year');
}
else if ('year' == that.mode) {
that.setDisplay('day');
}
});
that.on('click', '.fdate-clear', function() {
that.el.fdate.clear();
});
that.on('click', '*', function(e) {
var is_input = e.target.classList.contains('fdate-input') || e.target.classList.contains('fdate-alt-input');
var is_cal = (null !== e.target.closest('.fdate-wrap'));
var is_clear = e.target.classList.contains('fdate-clear');
if (is_input || (is_cal && ! is_clear)) {
that.setCalVisibility('show');
// set position and render calendar
if (is_input) {
let visibleInput = e.target._altInput || e.target;
that.setInput(e.target._input);
that.setDisplay('day');
that.setPosition(visibleInput);
}
}
else {
that.setCalVisibility('hide');
}
});
// a11y support
window.addEventListener('keyup', function(e) {
if ('Tab' === e.key) {
if (e.target.classList.contains('fdate-input') || e.target.classList.contains('fdate-alt-input')) {
e.target._input.click();
}
else {
that.setCalVisibility('hide');
}
}
});
window.addEventListener('keydown', function(e) {
if ('Enter' === e.key) {
if (e.target.closest('.fdate-grid')) {
qs('.fdate-nav-label').focus();
}
if (e.target.closest('.fdate-wrap')) {
e.target.click();
}
}
else if ('Escape' === e.key) {
if (e.target.closest('.fdate-wrap') || e.target.classList.contains('fdate-input') || e.target.classList.contains('fdate-alt-input')) {
that.el.fdate.close();
}
}
else if ('ArrowUp' === e.key) {
if (e.target.classList.contains('fdate-input') || e.target.classList.contains('fdate-alt-input')) { // from input
qs('.fdate-clear').focus();
e.preventDefault();
}
else if (e.target.classList.contains('fdate-nav-label')) {
that.el.focus();
e.preventDefault();
}
else if (e.target.classList.contains('fdate-clear')) {
let days = document.querySelectorAll('.fdate-day.inner');
let item = (days.length) ? days[days.length - 1] : qs('.fdate-nav-label');
item.focus();
e.preventDefault();
}
else if (e.target.closest('.fdate-grid')) {
let offset = ('day' === that.mode) ? -7 : -4;
let el = that.getSibling(e.target, offset);
if (el) {
el.focus();
}
else {
qs('.fdate-nav-label').focus();
}
e.preventDefault();
}
}
else if ('ArrowDown' === e.key) {
if (e.target.classList.contains('fdate-input') || e.target.classList.contains('fdate-alt-input')) { // from input
let selected = qs('.fdate-grid .selected');
let today = qs('.fdate-grid .today');
if (selected) {
selected.focus();
}
else if (today) {
today.focus();
}
else {
qs('.fdate-nav-label').focus();
}
e.preventDefault();
}
else if (e.target.classList.contains('fdate-nav-label')) { // from nav
qs('.fdate-grid .inner').focus();
e.preventDefault();
}
else if (e.target.classList.contains('fdate-clear')) {
that.el.focus();
e.preventDefault();
}
else if (e.target.closest('.fdate-grid')) { // from grid
let offset = ('day' === that.mode) ? 7 : 4;
let el = that.getSibling(e.target, offset);
if (el) {
el.focus();
}
else {
qs('.fdate-clear').focus();
}
e.preventDefault();
}
}
else if ('ArrowLeft' === e.key) {
if (e.target.classList.contains('fdate-nav-label')) { // into the past
qs('.fdate-nav-prev').click();
e.preventDefault();
}
if (e.target.closest('.fdate-grid')) { // previous grid item
let prev = e.target.previousElementSibling;
if (prev && prev.classList.contains('inner')) {
prev.focus();
}
else {
let days = document.querySelectorAll('.fdate-day.inner');
days[days.length - 1].focus(); // last valid day of month
}
e.preventDefault();
}
}
else if ('ArrowRight' === e.key) {
if (e.target.classList.contains('fdate-nav-label')) { // into the future
qs('.fdate-nav-next').click();
e.preventDefault();
}
if (e.target.closest('.fdate-grid')) { // next grid item
let next = e.target.nextElementSibling;
if (next && next.classList.contains('inner')) {
next.focus();
}
else {
qs('.fdate-day.inner').focus(); // first valid day of month
}
e.preventDefault();
}
}
});
}
getSibling(orig, offset) {
let el = orig;
for (var i = 0; i < Math.abs(offset); i++) {
el = (0 < offset) ? el.nextElementSibling : el.previousElementSibling;
if (null === el || !el.classList.contains('inner')) {
return null;
}
}
return el;
}
}
return fDate;
})();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,147 @@
.fs-wrap {
width: 220px;
display: inline-block;
position: relative;
cursor: pointer;
line-height: 1;
}
.fs-label-wrap {
position: relative;
background-color: #fff;
border: 1px solid #ddd;
cursor: default;
}
.fs-label-wrap,
.fs-dropdown {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.fs-label-wrap .fs-label {
padding: 6px 22px 6px 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.fs-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #333;
position: absolute;
top: 0;
right: 5px;
bottom: 0;
margin: auto;
transition: ease-in 0.15s;
}
.fs-open .fs-arrow {
transform: rotate(-180deg);
}
.fs-dropdown {
width: 100%;
position: absolute;
background-color: #fff;
border: 1px solid #ddd;
border-top: none;
z-index: 1000;
}
.fs-dropdown .fs-options {
max-height: 200px;
overflow: auto;
}
.fs-search {
background-color: #f8f8f8;
padding: 0 8px;
}
.fs-wrap .fs-search input {
border: none;
box-shadow: none;
background-color: transparent;
outline: none;
padding: 0;
width: 100%;
}
.fs-option,
.fs-search,
.fs-optgroup-label {
padding: 6px 8px;
cursor: default;
}
.fs-option:last-child {
border-bottom: none;
}
.fs-no-results {
padding: 6px 8px;
}
.fs-option {
cursor: pointer;
word-break: break-all;
}
.fs-option.disabled {
opacity: 0.4;
cursor: default;
}
.fs-wrap.single .fs-option.selected {
background-color: #dff3ff;
}
.fs-wrap.multiple .fs-option {
position: relative;
padding-left: 30px;
}
.fs-wrap.multiple .fs-checkbox {
position: absolute;
display: block;
width: 30px;
top: 0;
left: 0;
bottom: 0;
}
.fs-wrap.multiple .fs-option .fs-checkbox i {
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 14px;
height: 14px;
border: 1px solid #aeaeae;
border-radius: 2px;
background-color: #fff;
}
.fs-wrap.multiple .fs-option.selected .fs-checkbox i {
background-color: rgb(108, 138, 255);
border-color: transparent;
}
.fs-optgroup-label {
font-weight: bold;
text-align: center;
background-color: #f8f8f8;
}
.fs-hidden {
display: none;
}

View File

@@ -0,0 +1,420 @@
window.fSelect = (() => {
var build = {};
class fSelect {
constructor(selector, options) {
let that = this;
var defaults = {
placeholder: 'Select some options',
numDisplayed: 3,
overflowText: '{n} selected',
searchText: 'Search',
noResultsText: 'No results found',
showSearch: true,
optionFormatter: false
};
that.settings = Object.assign({}, defaults, options);
build = {output: '', optgroup: 0, idx: 0};
if ('string' === typeof selector) {
var nodes = Array.from(document.querySelectorAll(selector));
}
else if (selector instanceof Node) {
var nodes = [selector];
}
else if (Array.isArray(selector)) {
var nodes = selector;
}
else {
var nodes = [];
}
if ('undefined' === typeof window.fSelectInit) {
window.fSelectInit = {
searchCache: '',
lastChoice: null,
lastFocus: null,
activeEl: null
};
that.bindEvents();
}
nodes.forEach((input) => {
if (typeof input.fselect === 'object') {
input.fselect.destroy();
}
that.settings.multiple = input.matches('[multiple]');
input.fselect = that;
that.input = input;
that.create();
});
}
create() {
var that = this;
var options = that.buildOptions();
var label = that.getDropdownLabel();
var mode = (that.settings.multiple) ? 'multiple' : 'single';
var searchClass = (that.settings.showSearch) ? '' : ' fs-hidden';
var noResultsClass = (build.idx < 2) ? '' : ' fs-hidden';
var html = `
<div class="fs-wrap ${mode}" tabindex="0">
<div class="fs-label-wrap">
<div class="fs-label">${label}</div>
<span class="fs-arrow"></span>
</div>
<div class="fs-dropdown fs-hidden">
<div class="fs-search${searchClass}">
<input type="text" placeholder="${that.settings.searchText}" />
</div>
<div class="fs-no-results${noResultsClass}">${that.settings.noResultsText}</div>
<div class="fs-options">${options}</div>
</div>
</div>
`;
var tpl = document.createElement('template');
tpl.innerHTML = html;
var wrap = tpl.content.querySelector('.fs-wrap');
that.input.parentNode.insertBefore(wrap, that.input.nextSibling);
that.input.classList.add('fs-hidden');
// add a relationship link
that.input._rel = wrap;
wrap._rel = that.input;
}
destroy() {
this.input._rel.remove();
this.input.classList.remove('fs-hidden');
delete this.input._rel;
}
reload() {
this.destroy();
this.create();
}
open() {
var wrap = this.input._rel;
wrap.classList.add('fs-open');
wrap.querySelector('.fs-dropdown').classList.remove('fs-hidden');
// don't auto-focus for touch devices
if (! window.matchMedia("(pointer: coarse)").matches) {
wrap.querySelector('.fs-search input').focus();
}
window.fSelectInit.lastChoice = this.getSelectedOptions('value');
window.fSelectInit.activeEl = wrap;
this.trigger('fs:opened', wrap);
}
close() {
this.input._rel.classList.remove('fs-open');
this.input._rel.querySelector('.fs-dropdown').classList.add('fs-hidden');
window.fSelectInit.searchCache = '';
window.fSelectInit.lastChoice = null;
window.fSelectInit.lastFocus = null;
window.fSelectInit.activeEl = null;
this.trigger('fs:closed', this.input._rel);
}
buildOptions(parent) {
var that = this;
var parent = parent || that.input;
Array.from(parent.children).forEach((node) => {
if ('optgroup' === node.nodeName.toLowerCase()) {
var opt = `<div class="fs-optgroup-label" data-group="${build.optgroup}">${node.label}</div>`;
build.output += opt;
that.buildOptions(node);
build.optgroup++;
}
else {
var val = node.value;
// skip the first choice in multi-select mode
if (0 === build.idx && '' === val && that.settings.multiple) {
build.idx++;
return;
}
var classes = ['fs-option', 'g' + build.optgroup];
// append existing classes
node.className.split(' ').forEach((className) => {
if ('' !== className) {
classes.push(className);
}
});
if (node.matches('[disabled]')) classes.push('disabled');
if (node.matches('[selected]')) classes.push('selected');
classes = classes.join(' ');
if ('function' === typeof that.settings.optionFormatter) {
node.label = that.settings.optionFormatter(node.label, node);
}
var opt = `<div class="${classes}" data-value="${val}" data-idx="${build.idx}" tabindex="-1"><span class="fs-checkbox"><i></i></span><div class="fs-option-label">${node.label}</div></div>`;
build.output += opt;
build.idx++;
}
});
return build.output;
}
getSelectedOptions(field, context) {
var context = context || this.input;
return Array.from(context.selectedOptions).map((opt) => {
return (field) ? opt[field] : opt;
});
}
getAdjacentSibling(which) {
var that = this;
var which = which || 'next';
var sibling = window.fSelectInit.lastFocus;
var selector = '.fs-option:not(.fs-hidden):not(.disabled)';
if (sibling) {
sibling = sibling[which + 'ElementSibling'];
while (sibling) {
if (sibling.matches(selector)) break;
sibling = sibling[which + 'ElementSibling'];
}
return sibling;
}
else if ('next' == which) {
sibling = that.input._rel.querySelector(selector);
}
return sibling;
}
getDropdownLabel() {
var that = this;
var labelText = that.getSelectedOptions('text');
if (labelText.length < 1) {
labelText = that.settings.placeholder;
}
else if (labelText.length > that.settings.numDisplayed) {
labelText = that.settings.overflowText.replace('{n}', labelText.length);
}
else {
labelText = labelText.join(', ');
}
return labelText;
}
debounce(func, wait) {
var timeout;
return (...args) => {
var boundFunc = func.bind(this, ...args);
clearTimeout(timeout);
timeout = setTimeout(boundFunc, wait);
}
}
trigger(eventName, ...args) {
document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]}));
}
on(eventName, elementSelector, handler) {
document.addEventListener(eventName, function(e) {
// loop parent nodes from the target to the delegation node
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
handler.call(target, e);
break;
}
}
}, false);
}
bindEvents() {
var that = this;
var optionSelector = '.fs-option:not(.fs-hidden):not(.disabled)';
var unaccented = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
// debounce search for better performance
that.on('keyup', '.fs-search input', that.debounce(function(e) {
var wrap = e.target.closest('.fs-wrap');
var options = wrap._rel.options;
var matchOperators = /[|\\{}()[\]^$+*?.]/g;
var keywords = e.target.value.replace(matchOperators, '\\$&');
keywords = unaccented(keywords);
// if the searchCache already has a prefixed version of this search
// then don't un-hide the existing exclusions
if (0 !== keywords.indexOf(window.fSelectInit.searchCache)) {
wrap.querySelectorAll('.fs-option, .fs-optgroup-label').forEach((node) => node.classList.remove('fs-hidden'));
}
window.fSelectInit.searchCache = keywords;
for (var i = 0; i < options.length; i++) {
if ('' === options[i].value) continue;
var needle = new RegExp(keywords, 'gi');
var haystack = unaccented(options[i].text);
if (null === haystack.match(needle)) {
wrap.querySelector('.fs-option[data-idx="' + i + '"]').classList.add('fs-hidden');
}
}
// hide optgroups if no choices
wrap.querySelectorAll('.fs-optgroup-label').forEach((node) => {
var group = node.getAttribute('data-group');
var container = node.closest('.fs-options');
var count = container.querySelectorAll('.fs-option.g' + group + ':not(.fs-hidden)').length;
if (count < 1) {
node.classList.add('fs-hidden');
}
});
// toggle the noResultsText div
if (wrap.querySelectorAll('.fs-option:not(.fs-hidden').length) {
wrap.querySelector('.fs-no-results').classList.add('fs-hidden');
}
else {
wrap.querySelector('.fs-no-results').classList.remove('fs-hidden');
}
}, 100));
that.on('click', optionSelector, function(e) {
var wrap = this.closest('.fs-wrap');
var value = this.getAttribute('data-value');
var input = wrap._rel;
var isMultiple = wrap.classList.contains('multiple');
if (!isMultiple) {
input.value = value;
wrap.querySelectorAll('.fs-option.selected').forEach((node) => node.classList.remove('selected'));
}
else {
var idx = parseInt(this.getAttribute('data-idx'));
input.options[idx].selected = !this.classList.contains('selected');
}
this.classList.toggle('selected');
var label = input.fselect.getDropdownLabel();
wrap.querySelector('.fs-label').innerHTML = label;
// fire a change event
input.dispatchEvent(new Event('change', { bubbles: true }));
input.fselect.trigger('fs:changed', wrap);
if (!isMultiple) {
input.fselect.close();
}
e.stopImmediatePropagation();
});
that.on('keydown', '*', function(e) {
var wrap = this.closest('.fs-wrap');
if (!wrap) return;
if (-1 < [38, 40, 27].indexOf(e.which)) {
e.preventDefault();
}
if (32 == e.which || 13 == e.which) { // space, enter
if (e.target.closest('.fs-search')) {
// preserve spaces for search
}
else if (e.target.matches(optionSelector)) {
e.target.click();
e.preventDefault();
}
else {
wrap.querySelector('.fs-label').click();
e.preventDefault();
}
}
else if (38 == e.which) { // up
var sibling = wrap._rel.fselect.getAdjacentSibling('previous');
window.fSelectInit.lastFocus = sibling; // stop at the search box
if (sibling) {
sibling.focus();
}
else {
wrap.querySelector('.fs-search input').focus();
}
}
else if (40 == e.which) { // down
var sibling = wrap._rel.fselect.getAdjacentSibling('next');
if (sibling) {
sibling.focus();
window.fSelectInit.lastFocus = sibling; // stop at the bottom
}
}
else if (9 == e.which || 27 == e.which) { // tab, esc
wrap._rel.fselect.close();
}
});
that.on('click', '*', function(e) {
var wrap = this.closest('.fs-wrap');
var lastActive = window.fSelectInit.activeEl;
if (wrap) {
var labelWrap = this.closest('.fs-label-wrap');
if (labelWrap) {
if (lastActive) {
lastActive._rel.fselect.close();
}
if (wrap !== lastActive) {
wrap._rel.fselect.open();
}
}
}
else {
if (lastActive) {
lastActive._rel.fselect.close();
}
}
});
}
}
var $ = (selector, options) => new fSelect(selector, options);
return $;
})();
if ('undefined' !== typeof fUtil) {
fUtil.fn.fSelect = function(opts) {
this.each(function() { // no arrow function to preserve "this"
fSelect(this, opts);
});
return this;
};
}

View File

@@ -0,0 +1,18 @@
.ftip-wrap {
position: absolute;
display: none;
max-width: 400px;
padding: 6px 8px;
background-color: #222;
border-radius: 6px;
opacity: 0;
color: #fff;
cursor: default;
transition: height 0s, opacity 1s;
}
.ftip-wrap.active {
height: auto;
display: inline-block;
opacity: 0.9;
}

View File

@@ -0,0 +1,100 @@
window.fTip = (() => {
var qs = (selector) => document.querySelector(selector);
class fTip {
constructor(selector, options) {
let that = this;
var defaults = {
content: (node) => node.getAttribute('title')
};
that.settings = Object.assign({}, defaults, options);
var inputs = [];
if ('string' === typeof selector) {
var inputs = Array.from(document.querySelectorAll(selector));
}
else if (selector instanceof Node) {
var inputs = [selector];
}
else if (Array.isArray(selector)) {
var inputs = selector;
}
if (null === qs('.ftip-wrap')) {
that.buildHtml();
that.bindEvents();
}
inputs.forEach(function(input) {
that.input = input;
input.ftip = that;
input.classList.add('ftip-enabled');
});
}
open() {
let input = this.input;
let wrap = qs('.ftip-wrap');
wrap.innerHTML = this.settings.content(input);
wrap.classList.add('active');
let wrapBounds = wrap.getBoundingClientRect();
let inputBounds = input.getBoundingClientRect();
let top = window.pageYOffset + inputBounds.top;
let left = window.pageXOffset + inputBounds.right;
let centered = ((inputBounds.height - wrapBounds.height) / 2);
wrap.style.top = (top + centered) + 'px';
wrap.style.left = (left + 10) + 'px';
}
buildHtml() {
let html = '<div class="ftip-wrap"></div>';
document.body.insertAdjacentHTML('beforeend', html);
}
bindEvents() {
var that = this;
let wrap = qs('.ftip-wrap');
var delayHandler = () => {
that.delay = setTimeout(() => {
wrap.classList.remove('active');
}, 250);
};
that.on('mouseover', '.ftip-enabled', function(e) {
clearTimeout(that.delay);
this.ftip.open();
});
that.on('mouseout', '.ftip-enabled', delayHandler);
that.on('mouseout', '.ftip-wrap', delayHandler);
that.on('mouseover', '.ftip-wrap', () => {
clearTimeout(that.delay);
});
}
on(eventName, elementSelector, handler) {
document.addEventListener(eventName, function(e) {
// loop parent nodes from the target to the delegation node
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
handler.call(target, e);
break;
}
}
}, false);
}
}
var $ = (selector, options) => new fTip(selector, options);
return $;
})();

View File

@@ -0,0 +1,7 @@
Copyright 2021 FacetWP, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,451 @@
window.fUtil = (() => {
class fUtil {
constructor(selector) {
if (typeof selector === 'string' || selector instanceof String) { // string
var selector = selector.replace(':selected', ':checked');
if ('' === selector) {
this.nodes = [];
}
else if (this.isValidSelector(selector)) {
this.nodes = Array.from(document.querySelectorAll(selector));
}
else {
var tpl = document.createElement('template');
tpl.innerHTML = selector;
this.nodes = [tpl.content];
}
}
else if (Array.isArray(selector)) { // array of nodes
this.nodes = selector;
}
else if (typeof selector === 'object' && selector.nodeName) { // node
this.nodes = [selector];
}
else if (typeof selector === 'function') { // function
this.ready(selector);
}
else if (selector === window) { // window
this.nodes = [window];
}
else { // document
this.nodes = [document];
}
// custom plugins
$.each($.fn, (handler, method) => {
this[method] = handler;
});
}
static isset(input) {
return typeof input !== 'undefined';
}
static post(url, data, settings) {
var settings = Object.assign({}, {
dataType: 'json',
contentType: 'application/json',
headers: {},
done: () => {},
fail: () => {}
}, settings);
settings.headers['Content-Type'] = settings.contentType;
data = ('application/json' === settings.contentType) ? JSON.stringify(data) : $.toEncoded(data);
fetch(url, {
method: 'POST',
headers: settings.headers,
body: data
})
.then(response => response[settings.dataType]())
.then(json => settings.done(json))
.catch(err => settings.fail(err));
}
static toEncoded(obj, prefix, out) {
var out = out || [];
var prefix = prefix || '';
if (Array.isArray(obj)) {
if (obj.length) {
obj.forEach((val) => {
$.toEncoded(val, prefix + '[]', out);
});
}
else {
$.toEncoded('', prefix, out);
}
}
else if (typeof obj === 'object' && obj !== null) {
Object.keys(obj).forEach((key) => {
var new_prefix = prefix ? prefix + '[' + key + ']' : key;
$.toEncoded(obj[key], new_prefix, out);
});
}
else {
out.push(encodeURIComponent(prefix) + '=' + encodeURIComponent(obj));
}
return out.join('&');
}
static forEach(items, callback) {
if (typeof items === 'object' && items !== null) {
if (Array.isArray(items)) {
items.forEach((val, key) => callback.bind(val)(val, key));
}
else {
Object.keys(items).forEach(key => {
var val = items[key];
callback.bind(val)(val, key);
});
}
}
return items;
}
isValidSelector(string) {
try {
document.createDocumentFragment().querySelector(string);
}
catch(err) {
return false;
}
return true;
}
clone() {
return $(this.nodes);
}
len() {
return this.nodes.length;
}
each(callback) {
this.nodes.forEach((node, key) => {
let func = callback.bind(node); // set "this"
func(node, key);
});
return this;
}
ready(callback) {
if (typeof callback !== 'function') return;
if (document.readyState === 'complete') {
return callback();
}
document.addEventListener('DOMContentLoaded', callback, false);
}
addClass(className) {
this.each(node => node.classList.add(className));
return this;
}
removeClass(className) {
this.each(node => node.classList.remove(className));
return this;
}
hasClass(className) {
return $.isset(this.nodes.find(node => node.classList.contains(className)));
}
toggleClass(className) {
this.each(node => node.classList.toggle(className));
return this;
}
is(selector) {
for (let i = 0; i < this.len(); i++) { // forEach prevents loop exiting
if (this.nodes[i].matches(selector)) {
return true;
}
}
return false;
}
find(selector) {
var selector = selector.replace(':selected', ':checked');
let nodes = [];
let clone = this.clone();
clone.each(node => {
nodes = nodes.concat(Array.from(node.querySelectorAll(selector)));
});
clone.nodes = nodes;
return clone;
}
first() {
let clone = this.clone();
if (clone.len()) {
clone.nodes = this.nodes.slice(0, 1);
}
return clone;
}
last() {
let clone = this.clone();
if (clone.len()) {
clone.nodes = this.nodes.slice(-1);
}
return clone;
}
prev(selector) {
let nodes = [];
let clone = this.clone();
clone.each(node => {
let sibling = node.previousElementSibling;
while (sibling) {
if (!$.isset(selector) || sibling.matches(selector)) break;
sibling = sibling.previousElementSibling;
}
if (sibling) {
nodes.push(sibling);
}
});
clone.nodes = nodes;
return clone;
}
next(selector) {
let nodes = [];
let clone = this.clone();
clone.each(node => {
let sibling = node.nextElementSibling;
while (sibling) {
if (!$.isset(selector) || sibling.matches(selector)) break;
sibling = sibling.nextElementSibling;
}
if (sibling) {
nodes.push(sibling);
}
});
clone.nodes = nodes;
return clone;
}
prepend(html) {
this.each(node => node.insertAdjacentHTML('afterbegin', html));
return this;
}
append(html) {
this.each(node => node.insertAdjacentHTML('beforeend', html));
return this;
}
parents(selector) {
let parents = [];
let clone = this.clone();
clone.each(node => {
let parent = node.parentNode;
while (parent && parent !== document) {
if (parent.matches(selector)) parents.push(parent);
parent = parent.parentNode;
}
});
clone.nodes = [...new Set(parents)]; // remove dupes
return clone;
}
closest(selector) {
let nodes = [];
let clone = this.clone();
clone.each(node => {
let closest = node.closest(selector);
if (closest) {
nodes.push(closest);
}
});
clone.nodes = nodes;
return clone;
}
remove() {
this.each(node => node.remove());
return this;
}
on(eventName, selector, callback) {
if (!$.isset(selector)) return;
if (!$.isset(callback)) {
var callback = selector;
var selector = null;
}
// Reusable callback
var checkForMatch = (e) => {
if (null === selector || e.target.matches(selector)) {
callback.bind(e.target)(e);
}
else if (e.target.closest(selector)) {
var $this = e.target.closest(selector);
callback.bind($this)(e);
}
};
this.each(node => {
// Attach a unique ID to each node
if (!$.isset(node._id)) {
node._id = $.event.count;
$.event.store[$.event.count] = node;
$.event.count++;
}
var id = node._id;
// Store the raw callback, needed for .off()
checkForMatch._str = callback.toString();
if (!$.isset($.event.map[id])) {
$.event.map[id] = {};
}
if (!$.isset($.event.map[id][eventName])) {
$.event.map[id][eventName] = {};
}
if (!$.isset($.event.map[id][eventName][selector])) {
$.event.map[id][eventName][selector] = [];
}
// Use $.event.map to store event references
// removeEventListener needs named callbacks, so we're creating
// one for every handler
let length = $.event.map[id][eventName][selector].push(checkForMatch);
node.addEventListener(eventName, $.event.map[id][eventName][selector][length - 1]);
});
return this;
}
off(eventName, selector, callback) {
if (!$.isset(callback)) {
var callback = selector;
var selector = null;
}
this.each(node => {
var id = node._id;
$.each($.event.map[id], (selectors, theEventName) => {
$.each(selectors, (callbacks, theSelector) => {
$.each(callbacks, (theCallback, index) => {
if (
(!eventName || theEventName === eventName) &&
(!selector || theSelector === selector) &&
(!callback || theCallback._str === callback.toString())
) {
node.removeEventListener(theEventName, $.event.map[id][theEventName][theSelector][index]);
delete $.event.map[id][theEventName][theSelector][index];
}
});
});
});
});
return this;
}
trigger(eventName, extraData) {
this.each(node => node.dispatchEvent(new CustomEvent(eventName, {
detail: extraData,
bubbles: true
})));
return this;
}
attr(attributeName, value) {
if (!$.isset(value)) {
return (this.len()) ? this.nodes[0].getAttribute(attributeName) : null;
}
this.each(node => node.setAttribute(attributeName, value));
return this;
}
data(key, value) {
if (!$.isset(value)) {
return (this.len()) ? this.nodes[0]._fdata[key] : null;
}
this.each(node => node._fdata[key] = value);
return this;
}
html(htmlString) {
if (!$.isset(htmlString)) {
return (this.len()) ? this.nodes[0].innerHTML : null;
}
this.each(node => node.innerHTML = htmlString);
return this;
}
text(textString) {
if (!$.isset(textString)) {
return (this.len()) ? this.nodes[0].textContent : null;
}
else {
this.each(node => node.textContent = textString);
return this;
}
}
val(value) {
if (!$.isset(value)) {
if (this.len()) {
var field = this.nodes[0];
if (field.nodeName.toLowerCase() === 'select' && field.multiple) {
return [...field.options].filter((x) => x.selected).map((x) => x.value);
}
return field.value;
}
return null;
}
else {
this.each(node => node.value = value);
return this;
}
}
}
var $ = selector => new fUtil(selector);
// Set object methods
$.fn = {};
$.post = fUtil.post;
$.isset = fUtil.isset;
$.each = fUtil.forEach;
$.toEncoded = fUtil.toEncoded;
$.event = {map: {}, store: [], count: 0};
return $;
})();

View File

@@ -0,0 +1,150 @@
.noUi-target,
.noUi-target * {
touch-action: none;
user-select: none;
box-sizing: border-box;
}
.noUi-target {
position: relative;
}
.noUi-base,
.noUi-connects {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
}
.noUi-connects {
overflow: hidden;
z-index: 0;
}
.noUi-connect,
.noUi-origin {
will-change: transform;
position: absolute;
z-index: 1;
top: 0;
right: 0;
height: 100%;
width: 100%;
transform-origin: 0 0;
transform-style: flat;
}
.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
left: 0;
right: auto;
}
.noUi-horizontal .noUi-origin {
height: 0;
}
.noUi-handle {
backface-visibility: hidden;
position: absolute;
}
.noUi-touch-area {
height: 100%;
width: 100%;
}
.noUi-state-tap .noUi-connect,
.noUi-state-tap .noUi-origin {
transition: transform 0.3s;
}
.noUi-horizontal {
height: 14px;
}
.noUi-horizontal .noUi-handle {
width: 20px;
height: 20px;
right: -10px;
top: -4px;
}
.noUi-target {
background: #FAFAFA;
border-radius: 4px;
border: 1px solid #D3D3D3;
padding: 0 8px;
}
.noUi-connects {
border-radius: 3px;
}
.noUi-connect {
background: #ddd;
}
.noUi-handle {
border: 1px solid #D9D9D9;
border-radius: 3px;
background: #FFF;
cursor: default;
}
.noUi-pips,
.noUi-pips * {
box-sizing: border-box;
}
.noUi-pips {
position: absolute;
color: #999;
}
.noUi-value {
position: absolute;
white-space: nowrap;
text-align: center;
}
.noUi-value-sub {
color: #ccc;
font-size: 10px;
}
.noUi-marker {
position: absolute;
background: #CCC;
}
.noUi-marker-sub {
background: #AAA;
}
.noUi-marker-large {
background: #AAA;
}
.noUi-pips-horizontal {
padding: 10px 0;
height: 80px;
top: 100%;
left: 0;
width: 100%;
}
.noUi-value-horizontal {
transform: translate(-50%, 50%);
}
.noUi-rtl .noUi-value-horizontal {
transform: translate(50%, 50%);
}
.noUi-marker-horizontal.noUi-marker {
margin-left: -1px;
width: 2px;
height: 5px;
}
.noUi-marker-horizontal.noUi-marker-sub {
height: 10px;
}
.noUi-marker-horizontal.noUi-marker-large {
height: 15px;
}
.noUi-tooltip {
display: block;
position: absolute;
border: 1px solid #D9D9D9;
border-radius: 3px;
background: #fff;
color: #000;
padding: 5px;
text-align: center;
white-space: nowrap;
}
.noUi-horizontal .noUi-tooltip {
transform: translate(-50%, 0);
left: 50%;
bottom: 120%;
}
.noUi-horizontal .noUi-origin > .noUi-tooltip {
transform: translate(50%, 0);
left: auto;
bottom: 10px;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
/**
* Nummy.js - A (very) lightweight number formatter
* @link https://github.com/mgibbs189/nummy
*/
(function() {
function isValid(input) {
return !isNaN(parseFloat(input)) && isFinite(input);
}
function toFixed(value, precision) {
var pow = Math.pow(10, precision);
return (Math.round(value * pow) / pow).toFixed(precision);
}
class Nummy {
constructor(value) {
this._value = isValid(value) ? value : 0;
}
format(format, opts) {
var value = this._value,
negative = false,
precision = 0,
valueStr = '',
wholeStr = '',
decimalStr = '',
abbr = '';
var opts = Object.assign({}, {
'thousands_separator': ',',
'decimal_separator': '.'
}, opts);
if (-1 < format.indexOf('a')) {
var abbrevs = ['K', 'M', 'B', 't', 'q', 'Q'];
var exp = Math.floor(Math.log(Math.abs(value)) * Math.LOG10E); // log10 polyfill
var nearest_exp = (exp - (exp % 3)); // nearest exponent divisible by 3
if (3 <= exp) {
value = value / Math.pow(10, nearest_exp);
abbr += abbrevs[Math.floor(exp / 3) - 1];
}
format = format.replace('a', '');
}
// Check for decimals format
if (-1 < format.indexOf('.')) {
precision = format.split('.')[1].length;
}
value = toFixed(value, precision);
valueStr = value.toString();
// Handle negative number
if (value < 0) {
negative = true;
value = Math.abs(value);
valueStr = valueStr.slice(1);
}
wholeStr = valueStr.split('.')[0] || '';
decimalStr = valueStr.split('.')[1] || '';
// Handle decimals
decimalStr = (0 < precision && '' != decimalStr) ? '.' + decimalStr : '';
// Use thousands separators
if (-1 < format.indexOf(',')) {
wholeStr = wholeStr.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}
var output = (negative ? '-' : '') + wholeStr + decimalStr + abbr;
output = output.replace(/\./g, '{d}');
output = output.replace(/\,/g, '{t}');
output = output.replace(/{d}/g, opts.decimal_separator);
output = output.replace(/{t}/g, opts.thousands_separator);
return output;
}
}
window.nummy = function(input) {
return new Nummy(input);
}
})();

View File

@@ -0,0 +1 @@
!function(){"use strict";var a;(a=function(a){var t;this._value=(t=a,!isNaN(parseFloat(t))&&isFinite(t)?a:0)}).prototype.format=function(a,t){var e=this._value,r=!1,i=0,n="",o="",s="",l="";if(t=Object.assign({},{thousands_separator:",",decimal_separator:"."},t),-1<a.indexOf("a")){var p=Math.floor(Math.log(Math.abs(e))*Math.LOG10E),c=p-p%3;3<=p&&(e/=Math.pow(10,c),l+=["K","M","B","t","q","Q"][Math.floor(p/3)-1]),a=a.replace("a","")}-1<a.indexOf(".")&&(i=a.split(".")[1].length),e=function(a,t){var e=Math.pow(10,t);return(Math.round(a*e)/e).toFixed(t)}(e,i),n=e.toString(),e<0&&(r=!0,e=Math.abs(e),n=n.slice(1)),o=n.split(".")[0]||"",s=n.split(".")[1]||"",s=0<i&&""!=s?"."+s:"",-1<a.indexOf(",")&&(o=o.replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1,"));var d=(r?"-":"")+o+s+l;return d=(d=(d=(d=d.replace(/\./g,"{d}")).replace(/\,/g,"{t}")).replace(/{d}/g,t.decimal_separator)).replace(/{t}/g,t.thousands_separator)},window.nummy=function(t){return new a(t)}}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,180 @@
<?php
class FacetWP_API_Fetch
{
function __construct() {
add_action( 'rest_api_init', [ $this, 'register' ] );
}
// PHP < 5.3
function register() {
register_rest_route( 'facetwp/v1', '/fetch', [
'methods' => 'POST',
'callback' => [ $this, 'callback' ],
'permission_callback' => [ $this, 'permission_callback' ]
] );
}
// PHP < 5.3
function callback( $request ) {
$data = $request->get_param( 'data' );
if ( ! $request->is_json_content_type()) {
$data = empty( $data ) ? [] : json_decode( $data, true );
}
return $this->process_request( $data );
}
// PHP < 5.3
function permission_callback( $request ) {
return apply_filters( 'facetwp_api_can_access', false, $request );
}
function process_request( $params = [] ) {
global $wpdb;
$defaults = [
'facets' => [
// 'category' => [ 'acf' ]
],
'query_args' => [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 10,
'paged' => 1,
],
'settings' => [
'first_load' => true
]
];
$params = array_merge( $defaults, $params );
$facet_types = FWP()->helper->facet_types;
$valid_facets = [];
$facets = [];
// Validate input
$page = (int) ( $params['query_args']['paged'] ?? 1 );
$per_page = (int) ( $params['query_args']['posts_per_page'] ?? 10 );
$page = max( $page, 1 );
$per_page = ( 0 === $per_page ) ? 10 : $per_page;
$per_page = ( -1 > $per_page ) ? absint( $per_page ) : $per_page;
$params['query_args']['paged'] = $page;
$params['query_args']['posts_per_page'] = $per_page;
// Generate FWP()->facet->facets
// Required by FWP()->helper->facet_setting_exists()
foreach ( $params['facets'] as $facet_name => $facet_value ) {
$facet = FWP()->helper->get_facet_by_name( $facet_name );
if ( false !== $facet ) {
$facet['selected_values'] = (array) $facet_value;
$valid_facets[ $facet_name ] = $facet;
FWP()->facet->facets[ $facet_name ] = $facet;
}
}
// Get bucket of post IDs
$query_args = $params['query_args'];
FWP()->facet->query_args = $query_args;
$post_ids = FWP()->facet->get_filtered_post_ids( $query_args );
// SQL WHERE used by facets
$where_clause = ' AND post_id IN (' . implode( ',', $post_ids ) . ')';
// Check if empty
if ( 0 === $post_ids[0] && 1 === count( $post_ids ) ) {
$post_ids = [];
}
// get_where_clause() needs "found_posts" (keep this BELOW the empty check)
FWP()->facet->query = (object) [ 'found_posts' => count( $post_ids ) ];
// Get valid facets and their values
foreach ( $valid_facets as $facet_name => $facet ) {
$args = [
'facet' => $facet,
'where_clause' => $where_clause,
'selected_values' => $facet['selected_values'],
];
$facet_data = [
'name' => $facet['name'],
'label' => $facet['label'],
'type' => $facet['type'],
'selected' => $facet['selected_values'],
];
// Load facet choices if available
if ( method_exists( $facet_types[ $facet['type'] ], 'load_values' ) ) {
$choices = $facet_types[ $facet['type'] ]->load_values( $args );
foreach ( $choices as $key => $choice ) {
$row = [
'value' => $choice['facet_value'],
'label' => $choice['facet_display_value'],
'depth' => (int) $choice['depth'],
'count' => (int) $choice['counter'],
];
if ( isset( $choice['term_id'] ) ) {
$row['term_id'] = (int) $choice['term_id'];
}
if ( isset( $choice['parent_id'] ) ) {
$row['parent_id'] = (int) $choice['parent_id'];
}
$choices[ $key ] = $row;
}
$facet_data['choices'] = $choices;
}
// Load facet settings if available
if ( method_exists( $facet_types[ $facet['type'] ], 'settings_js' ) ) {
$facet_data['settings'] = $facet_types[ $facet['type'] ]->settings_js( $args );
}
$facets[ $facet_name ] = $facet_data;
}
$total_rows = count( $post_ids );
// Paginate?
if ( 0 < $per_page ) {
$total_pages = ceil( $total_rows / $per_page );
if ( $page > $total_pages ) {
$post_ids = [];
}
else {
$offset = ( $per_page * ( $page - 1 ) );
$post_ids = array_slice( $post_ids, $offset, $per_page );
}
}
else {
$total_pages = ( 0 < $total_rows ) ? 1 : 0;
}
// Generate the output
$output = [
'results' => $post_ids,
'facets' => $facets,
'pager' => [
'page' => $page,
'per_page' => $per_page,
'total_rows' => $total_rows,
'total_pages' => $total_pages,
]
];
return apply_filters( 'facetwp_api_output', $output );
}
}

View File

@@ -0,0 +1,27 @@
<?php
add_action( 'rest_api_init', function() {
register_rest_route( 'facetwp/v1', '/refresh', [
'methods' => 'POST',
'callback' => 'facetwp_api_refresh',
'permission_callback' => '__return_true'
] );
});
function facetwp_api_refresh( $request ) {
$params = $request->get_params();
$action = $params['action'] ?? '';
$valid_actions = [
'facetwp_refresh',
'facetwp_autocomplete_load'
];
$valid_actions = apply_filters( 'facetwp_api_valid_actions', $valid_actions );
if ( in_array( $action, $valid_actions ) ) {
do_action( $action );
}
return [];
}

View File

@@ -0,0 +1,321 @@
<?php
class FacetWP_Ajax
{
function __construct() {
add_action( 'init', [ $this, 'switchboard' ], 1000 );
}
function switchboard() {
$valid_actions = [
'resume_index',
'save_settings',
'rebuild_index',
'get_info',
'get_query_args',
'heartbeat',
'license',
'backup'
];
$action = isset( $_POST['action'] ) ? sanitize_key( $_POST['action'] ) : '';
$is_valid = false;
if ( 0 === strpos( $action, 'facetwp_' ) ) {
$action = substr( $action, 8 );
$is_valid = in_array( $action, $valid_actions );
}
if ( $is_valid ) {
// Non-authenticated
if ( in_array( $action, [ 'resume_index' ] ) ) {
$this->$action();
}
// Authenticated
elseif ( current_user_can( 'manage_options' ) ) {
if ( wp_verify_nonce( $_POST['nonce'], 'fwp_admin_nonce' ) ) {
$this->$action();
}
}
}
// Listen for API refresh call
add_action( 'facetwp_refresh', [ $this, 'refresh' ] );
// Backwards compatibility
$this->url_vars = FWP()->request->url_vars;
$this->is_preload = FWP()->request->is_preload;
}
/**
* Save admin settings
*/
function save_settings() {
$settings = $_POST['data'];
if ( isset( $settings['settings'] ) ) {
update_option( 'facetwp_settings', json_encode( $settings ), 'no' );
if ( FWP()->diff->is_reindex_needed() ) {
$response = [
'code' => 'error',
'message' => __( 'Settings saved, please re-index', 'fwp' )
];
}
else {
$response = [
'code' => 'success',
'message' => __( 'Settings saved', 'fwp' )
];
}
}
else {
$response = [
'code' => 'error',
'message' => __( 'Error: invalid JSON', 'fwp' )
];
}
wp_send_json( $response );
}
/**
* Rebuild the index table
*/
function rebuild_index() {
update_option( 'facetwp_indexing_cancelled', 'no', 'no' );
FWP()->indexer->index();
exit;
}
function get_info() {
$type = $_POST['type'];
if ( 'post_types' == $type ) {
$post_types = get_post_types( [ 'exclude_from_search' => false, '_builtin' => false ] );
$post_types = [ 'post', 'page' ] + $post_types;
sort( $post_types );
$response = [
'code' => 'success',
'message' => implode( ', ', $post_types )
];
}
elseif ( 'indexer_stats' == $type ) {
$last_indexed = get_option( 'facetwp_last_indexed' );
$last_indexed = $last_indexed ? human_time_diff( $last_indexed ) . ' ago' : 'never';
$response = [
'code' => 'success',
'message' => "last indexed: $last_indexed"
];
}
elseif ( 'cancel_reindex' == $type ) {
update_option( 'facetwp_indexing_cancelled', 'yes' );
$response = [
'code' => 'success',
'message' => 'Indexing cancelled'
];
}
elseif ( 'purge_index_table' == $type ) {
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}facetwp_index" );
delete_option( 'facetwp_version' );
delete_option( 'facetwp_indexing' );
delete_option( 'facetwp_transients' );
$response = [
'code' => 'success',
'message' => __( 'Done, please re-index', 'fwp' )
];
}
wp_send_json( $response );
}
/**
* Return query arguments based on a Query Builder object
*/
function get_query_args() {
$query_obj = $_POST['query_obj'];
if ( is_array( $query_obj ) ) {
$query_args = FWP()->builder->parse_query_obj( $query_obj );
}
wp_send_json( $query_args );
}
/**
* Keep track of indexing progress
*/
function heartbeat() {
$output = [
'pct' => FWP()->indexer->get_progress()
];
if ( -1 == $output['pct'] ) {
$output['rows'] = FWP()->helper->get_row_counts();
}
wp_send_json( $output );
}
/**
* License activation
*/
function license() {
$license = sanitize_key( $_POST['license'] );
$request = wp_remote_post( 'https://api.facetwp.com', [
'body' => [
'action' => 'activate',
'slug' => 'facetwp',
'license' => $license,
'host' => FWP()->helper->get_http_host(),
]
] );
if ( ! is_wp_error( $request ) || 200 == wp_remote_retrieve_response_code( $request ) ) {
update_option( 'facetwp_license', $license );
update_option( 'facetwp_activation', $request['body'] );
update_option( 'facetwp_updater_last_checked', 0 );
echo $request['body'];
}
else {
echo json_encode( [
'status' => 'error',
'message' => __( 'Error', 'fwp' ) . ': ' . $request->get_error_message(),
] );
}
exit;
}
/**
* Import / export functionality
*/
function backup() {
$action_type = $_POST['action_type'];
$output = [];
if ( 'export' == $action_type ) {
$items = $_POST['items'];
if ( ! empty( $items ) ) {
foreach ( $items as $item ) {
if ( 'facet' == substr( $item, 0, 5 ) ) {
$item_name = substr( $item, 6 );
$output['facets'][] = FWP()->helper->get_facet_by_name( $item_name );
}
elseif ( 'template' == substr( $item, 0, 8 ) ) {
$item_name = substr( $item, 9 );
$output['templates'][] = FWP()->helper->get_template_by_name( $item_name );
}
}
}
echo json_encode( $output );
}
elseif ( 'import' == $action_type ) {
$settings = FWP()->helper->settings;
$import_code = $_POST['import_code'];
$overwrite = (int) $_POST['overwrite'];
if ( empty( $import_code ) || ! is_array( $import_code ) ) {
_e( 'Nothing to import', 'fwp' );
exit;
}
$status = [
'imported' => [],
'skipped' => [],
];
foreach ( $import_code as $object_type => $object_items ) {
foreach ( $object_items as $object_item ) {
$is_match = false;
foreach ( $settings[$object_type] as $key => $settings_item ) {
if ( $object_item['name'] == $settings_item['name'] ) {
if ( $overwrite ) {
$settings[$object_type][$key] = $object_item;
$status['imported'][] = $object_item['label'];
}
else {
$status['skipped'][] = $object_item['label'];
}
$is_match = true;
break;
}
}
if ( ! $is_match ) {
$settings[$object_type][] = $object_item;
$status['imported'][] = $object_item['label'];
}
}
}
update_option( 'facetwp_settings', json_encode( $settings ) );
if ( ! empty( $status['imported'] ) ) {
echo ' [<strong>' . __( 'Imported', 'fwp' ) . '</strong>] ' . implode( ', ', $status['imported'] );
}
if ( ! empty( $status['skipped'] ) ) {
echo ' [<strong>' . __( 'Skipped', 'fwp' ) . '</strong>] ' . implode( ', ', $status['skipped'] );
}
}
exit;
}
/**
* The AJAX facet refresh handler
*/
function refresh() {
global $wpdb;
$params = FWP()->request->process_post_data();
$output = FWP()->facet->render( $params );
$data = stripslashes_deep( $_POST['data'] );
// Ignore invalid UTF-8 characters in PHP 7.2+
if ( version_compare( phpversion(), '7.2', '<' ) ) {
$output = json_encode( $output );
}
else {
$output = json_encode( $output, JSON_INVALID_UTF8_IGNORE );
}
echo apply_filters( 'facetwp_ajax_response', $output, [
'data' => $data
] );
exit;
}
/**
* Resume stalled indexer
*/
function resume_index() {
$touch = (int) FWP()->indexer->get_transient( 'touch' );
if ( 0 < $touch && $_POST['touch'] == $touch ) {
FWP()->indexer->index();
}
exit;
}
}

View File

@@ -0,0 +1,839 @@
<?php
class FacetWP_Builder
{
public $css = [];
public $data = [];
public $custom_css;
function __construct() {
add_filter( 'facetwp_query_args', [ $this, 'hydrate_date_values' ], 999 );
add_filter( 'facetwp_builder_dynamic_tag_value', [ $this, 'dynamic_tag_value' ], 0, 3 );
}
/**
* Generate CSS class string (helper method)
* @since 3.9.3
*/
function get_classes( $type, $settings ) {
$classes = [ $type, $settings['name'], $settings['css_class'] ];
return trim( implode( ' ', $classes ) );
}
/**
* Generate the layout HTML
* @since 3.2.0
*/
function render_layout( $layout ) {
global $wp_query, $post;
$counter = 0;
$settings = $layout['settings'];
$this->custom_css = $settings['custom_css'];
$selector = '.fwpl-layout';
$selector .= empty( $settings['name'] ) ? '' : '.' . $settings['name'];
$this->css = [
'.fwpl-layout, .fwpl-row' => [
'display' => 'grid'
],
$selector => [
'grid-template-columns' => 'repeat(' . $settings['num_columns'] . ', 1fr)',
'grid-gap' => $settings['grid_gap'] . 'px'
],
$selector . ' .fwpl-result' => $this->build_styles( $settings )
];
$classes = $this->get_classes( 'fwpl-layout', $settings );
$output = '<div class="' . $classes . '">';
if ( have_posts() ) {
while ( have_posts() ) : the_post();
$counter++;
// Default dynamic tags
$tags = [
'post:id' => $post->ID,
'post:name' => $post->post_name,
'post:type' => $post->post_type,
'post:title' => $post->post_title,
'post:url' => get_permalink()
];
$params = [
'layout' => $layout,
'post' => $post
];
$this->data = apply_filters( 'facetwp_builder_dynamic_tags', $tags, $params );
$output .= '<div class="fwpl-result r' . $counter . '">';
foreach ( $layout['items'] as $row ) {
$output .= $this->render_row( $row );
}
$output .= '</div>';
$output = $this->parse_dynamic_tags( $output, $params );
endwhile;
}
else {
$no_results_text = $settings['no_results_text'] ?? '';
$output .= do_shortcode( $no_results_text );
}
$output .= '</div>';
$output .= $this->render_css();
return $output;
}
/**
* Generate the row HTML
* @since 3.2.0
*/
function render_row( $row ) {
$settings = $row['settings'];
$this->css['.fwpl-row.' . $settings['name'] ] = $this->build_styles( $settings );
$classes = $this->get_classes( 'fwpl-row', $settings );
$output = '<div class="' . $classes . '">';
foreach ( $row['items'] as $col ) {
$output .= $this->render_col( $col );
}
$output .= '</div>';
return $output;
}
/**
* Generate the col HTML
* @since 3.2.0
*/
function render_col( $col ) {
$settings = $col['settings'];
$this->css['.fwpl-col.' . $settings['name'] ] = $this->build_styles( $settings );
$classes = $this->get_classes( 'fwpl-col', $settings );
$output = '<div class="fwpl-col ' . $classes . '">';
foreach ( $col['items'] as $item ) {
if ( 'row' == $item['type'] ) {
$output .= $this->render_row( $item );
}
elseif ( 'item' == $item['type'] ) {
$output .= $this->render_item( $item );
}
}
$output .= '</div>';
return $output;
}
/**
* Generate the item HTML
* @since 3.2.0
*/
function render_item( $item ) {
global $post;
$settings = $item['settings'];
$name = $settings['name'];
$source = $item['source'];
$value = $source;
$selector = '.fwpl-item.' . $name;
$selector = ( 'button' == $source ) ? $selector . ' button' : $selector;
$this->css[ $selector ] = $this->build_styles( $settings );
if ( 0 === strpos( $source, 'post_' ) || 'ID' == $source ) {
if ( 'post_title' == $source ) {
$value = $this->linkify( $post->$source, $settings['link'] );
}
elseif ( 'post_excerpt' == $source ) {
$value = get_the_excerpt( $post->ID );
}
elseif ( 'post_content' == $source ) {
$value = apply_filters( 'the_content', $post->post_content );
}
elseif ( 'post_author' == $source ) {
$field = $settings['author_field'];
$user = get_user_by( 'id', $post->$source );
$value = $user->$field;
}
elseif ( 'post_type' == $source ) {
$pt_obj = get_post_type_object( $post->$source );
$value = $pt_obj->labels->singular_name ?? $post->$source;
}
else {
$value = $post->$source;
}
}
elseif ( 0 === strpos( $source, 'cf/' ) ) {
$value = get_post_meta( $post->ID, substr( $source, 3 ), true );
$value = $this->linkify( $value, $settings['link'] );
}
elseif ( 0 === strpos( $source, 'tax/' ) ) {
$temp = [];
$taxonomy = substr( $source, 4 );
$terms = get_the_terms( $post->ID, $taxonomy );
if ( is_array( $terms ) ) {
foreach ( $terms as $term_obj ) {
$term = $this->linkify( $term_obj->name, $settings['term_link'], [
'term_id' => $term_obj->term_id,
'taxonomy' => $taxonomy
] );
$temp[] = '<span class="fwpl-term fwpl-term-' . $term_obj->slug . ' fwpl-tax-' . $taxonomy . '">' . $term . '</span>';
}
}
$value = implode( $settings['separator'], $temp );
}
elseif ( 0 === strpos( $source, 'woo/' ) ) {
$field = substr( $source, 4 );
$product = wc_get_product( $post->ID );
// Invalid product
if ( ! is_object( $product ) ) {
$value = '';
}
// Price
elseif ( 'price' == $field || 'sale_price' == $field || 'regular_price' == $field ) {
if ( $product->is_type( 'variable' ) ) {
$method_name = "get_variation_$field";
$value = $product->$method_name( 'min' ); // get_variation_price()
}
else {
$method_name = "get_$field";
$value = $product->$method_name(); // get_price()
}
}
// Average Rating
elseif ( 'average_rating' == $field ) {
$value = $product->get_average_rating();
}
// Stock Status
elseif ( 'stock_status' == $field ) {
$value = $product->is_in_stock() ? __( 'In Stock', 'fwp' ) : __( 'Out of Stock', 'fwp' );
}
// On Sale
elseif ( 'on_sale' == $field ) {
$value = $product->is_on_sale() ? __( 'On Sale', 'fwp' ) : '';
}
// Product Type
elseif ( 'product_type' == $field ) {
$value = $product->get_type();
}
}
elseif ( 0 === strpos( $source, 'acf/' ) && isset( FWP()->acf ) ) {
$value = FWP()->acf->get_field( $source, $post->ID );
}
elseif ( 'featured_image' == $source ) {
$value = get_the_post_thumbnail( $post->ID, $settings['image_size'] );
$value = $this->linkify( $value, $settings['link'] );
}
elseif ( 'button' == $source ) {
$value = '<button>' . $settings['button_text'] . '</button>';
$value = $this->linkify( $value, $settings['link'] );
}
elseif ( 'html' == $source ) {
$value = do_shortcode( $settings['content'] );
}
// Date format
if ( ! empty( $settings['date_format'] ) && ! empty( $value ) ) {
if ( ! empty( $settings['input_format'] ) ) {
$date = DateTime::createFromFormat( $settings['input_format'], $value );
}
else {
$date = new DateTime( $value );
}
// Use wp_date() to support i18n
if ( $date ) {
$value = wp_date( $settings['date_format'], $date->getTimestamp(), new DateTimeZone( 'UTC' ) );
}
}
// Number format
if ( ! empty( $settings['number_format'] ) && ! empty( $value ) ) {
$decimals = 2;
$format = $settings['number_format'];
$decimal_sep = FWP()->helper->get_setting( 'decimal_separator' );
$thousands_sep = FWP()->helper->get_setting( 'thousands_separator' );
// No thousands separator
if ( false === strpos( $format, ',' ) ) {
$thousands_sep = '';
}
// Handle decimals
if ( false === ( $pos = strpos( $format, '.' ) ) ) {
$decimals = 0;
}
else {
$decimals = strlen( $format ) - $pos - 1;
}
$value = number_format( $value, $decimals, $decimal_sep, $thousands_sep );
}
$output = '';
$prefix = $settings['prefix'] ?? '';
$suffix = $settings['suffix'] ?? '';
// Allow value hooks
$value = apply_filters( 'facetwp_builder_item_value', $value, $item );
// Convert array to string
if ( is_array( $value ) ) {
$value = implode( ', ', $value );
}
// Store the RAW short-tag
$this->data[ "$name:raw" ] = $value;
// Attach the prefix / suffix to the value
if ( '' != $value ) {
$value = $prefix . $value . $suffix;
}
// Store the short-tag
$this->data[ $name ] = $value;
// Build the list of CSS classes
$classes = $this->get_classes( 'fwpl-item', $settings );
if ( '' == $value ) {
$classes .= ' is-empty';
}
// Prevent output
if ( ! $settings['is_hidden'] ) {
$output = '<div class="' . $classes . '">' . $value . '</div>';
}
return $output;
}
/**
* Parse dynamic tags, e.g. {{ first_name }}
*/
function parse_dynamic_tags( $output, $params ) {
$pattern = '/({{[ ]?(.*?)[ ]?}})/s';
return preg_replace_callback( $pattern, function( $matches ) use( $params ) {
$tag_name = $matches[2];
$tag_value = $this->data[ $tag_name ] ?? '';
return apply_filters( 'facetwp_builder_dynamic_tag_value', $tag_value, $tag_name, $params );
}, $output );
}
/**
* Calculate some dynamic tag values on-the-fly, to prevent
* unnecessary queries and extra load time
*/
function dynamic_tag_value( $tag_value, $tag_name, $params ) {
if ( 'post:image' == $tag_name ) {
$tag_value = get_the_post_thumbnail_url( $params['post']->ID, 'full' );
}
return $tag_value;
}
/**
* Build the redundant styles (border, padding,etc)
* @since 3.2.0
*/
function build_styles( $settings ) {
$styles = [];
if ( isset( $settings['grid_template_columns'] ) ) {
$styles['grid-template-columns'] = $settings['grid_template_columns'];
}
if ( isset( $settings['border'] ) ) {
$styles['border-style'] = $settings['border']['style'];
$styles['border-color'] = $settings['border']['color'];
$styles['border-width'] = $this->get_widths( $settings['border']['width'] );
}
if ( isset( $settings['background_color'] ) ) {
$styles['background-color'] = $settings['background_color'];
}
if ( isset( $settings['padding'] ) ) {
$styles['padding'] = $this->get_widths( $settings['padding'] );
}
if ( isset( $settings['text_style'] ) ) {
$styles['text-align'] = $settings['text_style']['align'];
$styles['font-weight'] = $settings['text_style']['bold'] ? 'bold' : '';
$styles['font-style'] = $settings['text_style']['italic'] ? 'italic' : '';
}
if ( isset( $settings['font_size'] ) ) {
$styles['font-size'] = $settings['font_size']['size'] . $settings['font_size']['unit'];
}
if ( isset( $settings['text_color'] ) ) {
$styles['color'] = $settings['text_color'];
}
if ( isset( $settings['button_border'] ) ) {
$border = $settings['button_border'];
$width = $border['width'];
$unit = $width['unit'];
$styles['color'] = $settings['button_text_color'];
$styles['background-color'] = $settings['button_color'];
$styles['padding'] = $this->get_widths( $settings['button_padding'] );
$styles['border-style'] = $border['style'];
$styles['border-color'] = $border['color'];
$styles['border-top-width'] = $width['top'] . $unit;
$styles['border-right-width'] = $width['right'] . $unit;
$styles['border-bottom-width'] = $width['bottom'] . $unit;
$styles['border-left-width'] = $width['left'] . $unit;
}
return $styles;
}
/**
* Build the CSS widths, e.g. for "padding" or "border-width"
* @since 3.2.0
*/
function get_widths( $data ) {
$unit = $data['unit'];
$top = $data['top'];
$right = $data['right'];
$bottom = $data['bottom'];
$left = $data['left'];
if ( $top == $right && $right == $bottom && $bottom == $left ) {
return "$top$unit";
}
elseif ( $top == $bottom && $left == $right ) {
return "$top$unit $left$unit";
}
return "$top$unit $right$unit $bottom$unit $left$unit";
}
/**
* Convert a value into a link
* @since 3.2.0
*/
function linkify( $value, $link_data, $term_data = [] ) {
global $post;
$type = $link_data['type'];
$href = $link_data['href'];
$target = $link_data['target'];
if ( 'none' !== $type ) {
if ( 'post' == $type ) {
$href = get_permalink();
}
if ( 'term' == $type ) {
$href = get_term_link( $term_data['term_id'], $term_data['taxonomy'] );
}
if ( ! empty( $target ) ) {
$target = ' target="' . $target . '"';
}
$value = '<a href="' . $href . '"' . $target . '>' . $value . '</a>';
}
return $value;
}
/**
* Turn the CSS array into valid CSS
* @since 3.2.0
*/
function render_css() {
$output = "\n<style>\n";
foreach ( $this->css as $selector => $props ) {
$valid_rules = $this->get_valid_css_rules( $props );
if ( ! empty( $valid_rules ) ) {
$output .= $selector . " {\n";
foreach ( $valid_rules as $prop => $value ) {
$output .= " $prop: $value;\n";
}
$output .= "}\n";
}
}
if ( ! empty( $this->custom_css ) ) {
$output .= $this->custom_css . "\n";
}
$output .= "</style>\n";
return $output;
}
/**
* Filter out empty or invalid rules
* @since 3.2.0
*/
function get_valid_css_rules( $props ) {
$rules = [];
foreach ( $props as $prop => $value ) {
if ( $this->is_valid_css_rule( $prop, $value ) ) {
$rules[ $prop ] = $value;
}
}
return $rules;
}
/**
* Optimize CSS rules
* @since 3.2.0
*/
function is_valid_css_rule( $prop, $value ) {
$return = true;
if ( empty( $value ) || 'px' === $value || '0px' === $value || 'none' === $value ) {
$return = false;
}
if ( 'font-size' === $prop && '0px' === $value ) {
$return = false;
}
return $return;
}
/**
* Make sure the query is valid
* @since 3.2.0
*/
function parse_query_obj( $query_obj ) {
$output = [];
$tax_query = [];
$meta_query = [];
$date_query = [];
$post_type = 'any';
$post_status = [ 'publish' ];
$posts_per_page = 10;
$post_in = [];
$post_not_in = [];
$author_in = [];
$author_not_in = [];
$orderby = [];
if ( ! empty( $query_obj['posts_per_page'] ) ) {
$posts_per_page = (int) $query_obj['posts_per_page'];
}
if ( ! empty( $query_obj['post_type'] ) ) {
$post_type = array_column( $query_obj['post_type'], 'value' );
}
if ( empty( $query_obj['filters'] ) ) {
$query_obj['filters'] = [];
}
if ( empty( $query_obj['orderby'] ) ) {
$query_obj['orderby'] = [];
}
foreach ( $query_obj['filters'] as $filter ) {
$key = $filter['key'];
$value = $filter['value'];
$compare = $filter['compare'];
$type = $filter['type'];
// Cast as decimal for more accuracy
$type = ( 'NUMERIC' == $type ) ? 'DECIMAL(16,4)' : $type;
$exists_bypass = false;
$value_bypass = false;
// Clear the value for certain compare types
if ( in_array( $compare, [ 'EXISTS', 'NOT EXISTS', 'EMPTY', 'NOT EMPTY' ] ) ) {
$value_bypass = true;
$value = '';
}
if ( in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ] ) ) {
$exists_bypass = true;
}
// If "EMPTY", use "=" compare type w/ empty string value
if ( in_array( $compare, [ 'EMPTY', 'NOT EMPTY' ] ) ) {
$compare = ( 'EMPTY' == $compare ) ? '=' : '!=';
}
// Handle multiple values
if ( is_array( $value ) ) {
if ( in_array( $compare, [ '=', '!=' ] ) ) {
$compare = ( '=' == $compare ) ? 'IN' : 'NOT IN';
}
if ( ! in_array( $compare, [ 'IN', 'NOT IN' ] ) ) {
$value = $value[0];
}
}
if ( empty( $value ) && ! $value_bypass ) {
continue;
}
// Support dynamic URL vars
$value = $this->parse_uri_tags( $value );
// Prepend with "date|" so we can populate with hydrate_date_values()
if ( 'DATE' == $type ) {
$value = 'date|' . $value;
}
if ( 'ID' == $key ) {
$arg_name = ( 'IN' == $compare ) ? 'post_in' : 'post_not_in';
$$arg_name = $value;
}
elseif ( 'post_author' == $key ) {
$arg_name = ( 'IN' == $compare ) ? 'author_in' : 'author_not_in';
$$arg_name = $value;
}
elseif ( 'post_status' == $key ) {
$post_status = $value;
}
elseif ( 'post_date' == $key || 'post_modified' == $key ) {
if ( '>' == $compare || '>=' == $compare ) {
$date_query[] = [
'after' => $value,
'inclusive' => ( '>=' == $compare )
];
}
if ( '<' == $compare || '<=' == $compare ) {
$date_query[] = [
'before' => $value,
'inclusive' => ( '<=' == $compare )
];
}
}
elseif ( 0 === strpos( $key, 'tax/' ) ) {
$temp = [
'taxonomy' => substr( $key, 4 ),
'field' => 'slug',
'operator' => $compare
];
if ( ! $exists_bypass ) {
$temp['terms'] = $value;
}
$tax_query[] = $temp;
}
else {
$temp = [
'key' => substr( $key, strpos( $key, '/' ) + 1 ),
'compare' => $compare,
'type' => $type
];
if ( ! $exists_bypass ) {
$temp['value'] = $value;
}
$meta_query[] = $temp;
}
}
foreach ( $query_obj['orderby'] as $index => $data ) {
if ( 'cf/' == substr( $data['key'], 0, 3 ) ) {
$type = $data['type'];
// Cast as decimal for more accuracy
$type = ( 'NUMERIC' == $type ) ? 'DECIMAL(16,4)' : $type;
$meta_query['sort_' . $index] = [
'key' => substr( $data['key'], 3 ),
'type' => $type
];
$orderby['sort_' . $index] = $data['order'];
}
else {
$orderby[ $data['key'] ] = $data['order'];
}
}
$temp = [
'post_type' => $post_type,
'post_status' => $post_status,
'meta_query' => $meta_query,
'tax_query' => $tax_query,
'date_query' => $date_query,
'post__in' => $post_in,
'post__not_in' => $post_not_in,
'author__in' => $author_in,
'author__not_in' => $author_not_in,
'orderby' => $orderby,
'posts_per_page' => $posts_per_page
];
foreach ( $temp as $key => $val ) {
if ( ! empty( $val ) ) {
$output[ $key ] = $val;
}
}
return $output;
}
/**
* Get necessary values for the layout builder
* @since 3.2.0
*/
function get_layout_data() {
$sources = FWP()->helper->get_data_sources();
unset( $sources['post'] );
// Static options
$output = [
'row' => 'Child Row',
'html' => 'HTML',
'button' => 'Button',
'featured_image' => 'Featured Image',
'ID' => 'Post ID',
'post_title' => 'Post Title',
'post_name' => 'Post Name',
'post_content' => 'Post Content',
'post_excerpt' => 'Post Excerpt',
'post_date' => 'Post Date',
'post_modified' => 'Post Modified',
'post_author' => 'Post Author',
'post_type' => 'Post Type'
];
foreach ( $sources as $group ) {
foreach ( $group['choices'] as $name => $label ) {
$output[ $name ] = $label;
}
}
return $output;
}
/**
* Get necessary data for the query builder
* @since 3.0.0
*/
function get_query_data() {
$builder_post_types = [];
$post_types = get_post_types( [ 'public' => true ], 'objects' );
$data_sources = FWP()->helper->get_data_sources( 'builder' );
// Remove ACF choices
unset( $data_sources['acf'] );
foreach ( $post_types as $type ) {
$builder_post_types[] = [
'label' => $type->labels->name,
'value' => $type->name
];
}
$data_sources['posts']['choices'] = [
'ID' => 'ID',
'post_author' => 'Post Author',
'post_status' => 'Post Status',
'post_date' => 'Post Date',
'post_modified' => 'Post Modified'
];
return apply_filters( 'facetwp_builder_query_data', [
'post_types' => $builder_post_types,
'filter_by' => $data_sources
] );
}
/**
* Replace "date|" placeholders with actual dates
*/
function hydrate_date_values( $query_args ) {
if ( isset( $query_args['meta_query'] ) ) {
foreach ( $query_args['meta_query'] as $index => $row ) {
if ( isset( $row['value'] ) && is_string( $row['value'] ) && 0 === strpos( $row['value'], 'date|' ) ) {
$value = trim( substr( $row['value'], 5 ) );
$value = date( 'Y-m-d', strtotime( $value ) );
$query_args['meta_query'][ $index ]['value'] = $value;
}
}
}
return $query_args;
}
/**
* Let users pull URI or GET params into the query builder
* E.g. "http:uri", "http:uri:0", or "http:get:year"
* @since 3.6.0
*/
function parse_uri_tags( $values ) {
$temp = (array) $values;
foreach ( $temp as $key => $value ) {
if ( 0 === strpos( $value, 'http:uri' ) ) {
$uri = FWP()->helper->get_uri();
$uri_parts = explode( '/', $uri );
$tag_parts = explode( ':', $value );
if ( isset( $tag_parts[2] ) ) {
$index = (int) $tag_parts[2];
$index = ( $index < 0 ) ? count( $uri_parts ) + $index : $index;
$temp[ $key ] = $uri_parts[ $index ] ?? '';
}
else {
$temp[ $key ] = $uri;
}
}
elseif ( 0 === strpos( $value, 'http:get:' ) ) {
$tag_parts = explode( ':', $value );
$temp[ $key ] = $_GET[ $tag_parts[2] ] ?? '';
}
}
return is_array( $values ) ? $temp : $temp[0];
}
}

View File

@@ -0,0 +1,73 @@
<?php
class FacetWP_Diff
{
/**
* Compare "facetwp_settings" with "facetwp_settings_last_index" to determine
* whether the user needs to rebuild the index
* @since 3.0.9
*/
function is_reindex_needed() {
$s1 = FWP()->helper->load_settings();
$s2 = FWP()->helper->load_settings( true );
// Compare settings
$to_check = [ 'thousands_separator', 'decimal_separator', 'wc_enable_variations', 'wc_index_all' ];
foreach ( $to_check as $name ) {
$attr1 = $this->get_attr( $name, $s1['settings'] );
$attr2 = $this->get_attr( $name, $s2['settings'] );
if ( $attr1 !== $attr2 ) {
return true;
}
}
// Get facets, removing non-indexable ones
$f1 = array_filter( $s1['facets'], [ $this, 'is_indexable' ] );
$f2 = array_filter( $s2['facets'], [ $this, 'is_indexable' ] );
// The facet count is different
if ( count( $f1 ) !== count( $f2 ) ) {
return true;
}
// Sort the facets alphabetically
usort( $f1, function( $a, $b ) {
return strcmp( $a['name'], $b['name'] );
});
usort( $f2, function( $a, $b ) {
return strcmp( $a['name'], $b['name'] );
});
// Compare facet properties
$to_check = [ 'name', 'type', 'source', 'source_other', 'parent_term', 'hierarchical', 'modifier_type', 'modifier_values' ];
foreach ( $f1 as $index => $facet ) {
foreach ( $to_check as $attr ) {
$attr1 = $this->get_attr( $attr, $facet );
$attr2 = $this->get_attr( $attr, $f2[ $index ] );
if ( $attr1 !== $attr2 ) {
return true;
}
}
}
return false;
}
function is_indexable( $facet ) {
return ! in_array( $facet['type'], [ 'search', 'pager', 'reset', 'sort' ] );
}
/**
* Get an array element
* @since 3.0.9
*/
function get_attr( $name, $collection ) {
return $collection[ $name ] ?? false;
}
}

View File

@@ -0,0 +1,244 @@
<?php
class FacetWP_Display
{
/* (array) Facet types being used on the page */
public $active_types = [];
/* (array) Facets being used on the page */
public $active_facets = [];
/* (array) Extra features used on the page */
public $active_extras = [];
/* (array) Saved shortcode attributes */
public $shortcode_atts = [];
/* (boolean) Whether to enable FacetWP for the current page */
public $load_assets = false;
/* (array) Scripts and stylesheets to enqueue */
public $assets = [];
/* (array) Data to pass to front-end JS */
public $json = [];
function __construct() {
add_filter( 'widget_text', 'do_shortcode' );
add_action( 'loop_start', [ $this, 'add_template_tag' ] );
add_action( 'loop_no_results', [ $this, 'add_template_tag' ] );
add_action( 'wp_footer', [ $this, 'front_scripts' ], 25 );
add_shortcode( 'facetwp', [ $this, 'shortcode' ] );
}
/**
* Detect the loop container if the "facetwp-template" class is missing
*/
function add_template_tag( $wp_query ) {
if ( true === $wp_query->get( 'facetwp' ) && did_action( 'wp_head' ) ) {
echo "<!--fwp-loop-->\n";
}
}
/**
* Set default values for atts
*
* Old: [facetwp template="foo" static]
* New: [facetwp template="foo" static="true"]
*/
function normalize_atts( $atts ) {
foreach ( $atts as $key => $val ) {
if ( is_int( $key ) ) {
$atts[ $val ] = true;
unset( $atts[ $key ] );
}
}
return $atts;
}
/**
* Register shortcodes
*/
function shortcode( $atts ) {
$atts = $this->normalize_atts( $atts );
$this->shortcode_atts[] = $atts;
$output = '';
if ( isset( $atts['facet'] ) ) {
$facet = FWP()->helper->get_facet_by_name( $atts['facet'] );
if ( $facet ) {
$ui = empty( $facet['ui_type'] ) ? $facet['type'] : $facet['ui_type'];
$ui_attr = empty( $facet['ui_type'] ) ? '' : ' data-ui="' . $ui . '"';
$output = '<div class="facetwp-facet facetwp-facet-' . $facet['name'] . ' facetwp-type-' . $ui . '" data-name="' . $facet['name'] . '" data-type="' . $facet['type'] . '"' . $ui_attr . '></div>';
// Build list of active facet types
$this->active_types[ $facet['type'] ] = $facet['type'];
$this->active_facets[ $facet['name'] ] = $facet['name'];
$this->load_assets = true;
}
}
elseif ( isset( $atts['template'] ) ) {
$template = FWP()->helper->get_template_by_name( $atts['template'] );
if ( $template ) {
$class_name = 'facetwp-template';
// Static template
if ( isset( $atts['static'] ) ) {
$renderer = new FacetWP_Renderer();
$renderer->template = $template;
$renderer->query_args = $renderer->get_query_args();
$renderer->query = new WP_Query( $renderer->query_args );
$html = $renderer->get_template_html();
$class_name .= '-static';
}
// Preload template (search engine visible)
else {
global $wp_query;
$temp_query = $wp_query;
$args = FWP()->request->process_preload_data( $template['name'] );
$preload_data = FWP()->facet->render( $args );
$html = $preload_data['template'];
$wp_query = $temp_query;
}
$output = '<div class="{class}" data-name="{name}">{html}</div>';
$output = str_replace( '{class}', $class_name, $output );
$output = str_replace( '{name}', $atts['template'], $output );
$output = str_replace( '{html}', $html, $output );
$this->load_assets = true;
}
}
elseif ( isset( $atts['sort'] ) ) {
$this->active_extras['sort'] = true;
$output = '<div class="facetwp-sort"></div>';
}
elseif ( isset( $atts['selections'] ) ) {
$output = '<div class="facetwp-selections"></div>';
}
elseif ( isset( $atts['counts'] ) ) {
$this->active_extras['counts'] = true;
$output = '<div class="facetwp-counts"></div>';
}
elseif ( isset( $atts['pager'] ) ) {
$this->active_extras['pager'] = true;
$output = '<div class="facetwp-pager"></div>';
}
elseif ( isset( $atts['per_page'] ) ) {
$this->active_extras['per_page'] = true;
$output = '<div class="facetwp-per-page"></div>';
}
$output = apply_filters( 'facetwp_shortcode_html', $output, $atts );
return $output;
}
/**
* Output facet scripts
*/
function front_scripts() {
// Not enqueued - front.js needs to load before front_scripts()
if ( apply_filters( 'facetwp_load_assets', $this->load_assets ) ) {
// Load CSS?
if ( apply_filters( 'facetwp_load_css', true ) ) {
$this->assets['front.css'] = FACETWP_URL . '/assets/css/front.css';
}
// Load required JS
$this->assets['front.js'] = FACETWP_URL . '/assets/js/dist/front.min.js';
// Backwards compat?
if ( apply_filters( 'facetwp_load_deprecated', false ) ) {
$this->assets['front-deprecated.js'] = FACETWP_URL . '/assets/js/src/deprecated.js';
}
// Load a11y?
$a11y = FWP()->helper->get_setting( 'load_a11y', 'no' );
$a11y_hook = apply_filters( 'facetwp_load_a11y', false );
if ( 'yes' == $a11y || $a11y_hook ) {
$this->assets['accessibility.js'] = FACETWP_URL . '/assets/js/src/accessibility.js';
$this->json['a11y'] = [
'label_page' => __( 'Go to page', 'fwp-front' ),
'label_page_next' => __( 'Go to next page', 'fwp-front' ),
'label_page_prev' => __( 'Go to previous page', 'fwp-front' )
];
}
// Pass GET and URI params
$http_params = [
'get' => $_GET,
'uri' => FWP()->helper->get_uri(),
'url_vars' => FWP()->request->url_vars,
];
// See FWP()->facet->get_query_args()
if ( ! empty( FWP()->facet->archive_args ) ) {
$http_params['archive_args'] = FWP()->facet->archive_args;
}
// Populate the FWP_JSON object
$this->json['prefix'] = FWP()->helper->get_setting( 'prefix' );
$this->json['no_results_text'] = __( 'No results found', 'fwp-front' );
$this->json['ajaxurl'] = get_rest_url() . 'facetwp/v1/refresh';
$this->json['nonce'] = wp_create_nonce( 'wp_rest' );
if ( apply_filters( 'facetwp_use_preloader', true ) ) {
$overrides = FWP()->request->process_preload_overrides([
'facets' => $this->active_facets,
'extras' => $this->active_extras,
]);
$args = FWP()->request->process_preload_data( false, $overrides );
$this->json['preload_data'] = FWP()->facet->render( $args );
}
ob_start();
foreach ( $this->active_types as $type ) {
$facet_class = FWP()->helper->facet_types[ $type ];
if ( method_exists( $facet_class, 'front_scripts' ) ) {
$facet_class->front_scripts();
}
}
$inline_scripts = ob_get_clean();
$assets = apply_filters( 'facetwp_assets', $this->assets );
foreach ( $assets as $slug => $data ) {
$data = (array) $data;
$is_css = ( 'css' == substr( $slug, -3 ) );
$version = empty( $data[1] ) ? FACETWP_VERSION : $data[1];
$url = $data[0];
if ( false !== strpos( $url, 'facetwp' ) ) {
$prefix = ( false !== strpos( $url, '?' ) ) ? '&' : '?';
$url .= $prefix . 'ver=' . $version;
}
$html = $is_css ? '<link href="{url}" rel="stylesheet">' : '<script src="{url}"></script>';
$html = apply_filters( 'facetwp_asset_html', $html, $url );
echo str_replace( '{url}', $url, $html ) . "\n";
}
echo $inline_scripts;
?>
<script>
window.FWP_JSON = <?php echo json_encode( $this->json ); ?>;
window.FWP_HTTP = <?php echo json_encode( $http_params ); ?>;
</script>
<?php
}
}
}

View File

@@ -0,0 +1,600 @@
<?php
final class FacetWP_Helper
{
/* (array) The facetwp_settings option (after hooks) */
public $settings;
/* (array) Associative array of facet objects */
public $facet_types;
/* (array) Cached data sources */
public $data_sources;
/* (array) Cached terms */
public $term_cache;
/* (array) Index table row counts */
public $row_counts;
function __construct() {
$this->facet_types = $this->get_facet_types();
$this->settings = $this->load_settings();
}
/**
* Parse the URL hostname
*/
function get_http_host() {
return parse_url( get_option( 'home' ), PHP_URL_HOST );
}
/**
* Get the current page URI
*/
function get_uri() {
if ( isset( FWP()->facet->http_params ) ) {
return FWP()->facet->http_params['uri'];
}
$uri = parse_url( $_SERVER['REQUEST_URI'] );
return isset( $uri['path'] ) ? trim( $uri['path'], '/' ) : '';
}
/**
* Get available facet types
*/
function get_facet_types() {
if ( ! empty( $this->facet_types ) ) {
return $this->facet_types;
}
include( FACETWP_DIR . '/includes/facets/base.php' );
$types = [
'checkboxes' => 'Facetwp_Facet_Checkboxes',
'dropdown' => 'Facetwp_Facet_Dropdown',
'radio' => 'Facetwp_Facet_Radio_Core',
'fselect' => 'Facetwp_Facet_fSelect',
'hierarchy' => 'Facetwp_Facet_Hierarchy',
'slider' => 'Facetwp_Facet_Slider',
'search' => 'Facetwp_Facet_Search',
'autocomplete' => 'Facetwp_Facet_Autocomplete',
'date_range' => 'Facetwp_Facet_Date_Range',
'number_range' => 'Facetwp_Facet_Number_Range',
'rating' => 'FacetWP_Facet_Rating',
'proximity' => 'Facetwp_Facet_Proximity_Core',
'pager' => 'FacetWP_Facet_Pager',
'reset' => 'FacetWP_Facet_Reset',
'sort' => 'FacetWP_Facet_Sort'
];
$facet_types = [];
foreach ( $types as $slug => $class_name ) {
include( FACETWP_DIR . "/includes/facets/$slug.php" );
$facet_types[ $slug ] = new $class_name();
}
return apply_filters( 'facetwp_facet_types', $facet_types );
}
/**
* Get settings and allow for developer hooks
*/
function load_settings( $last_index = false ) {
$name = $last_index ? 'facetwp_settings_last_index' : 'facetwp_settings';
$option = get_option( $name );
$defaults = [
'facets' => [],
'templates' => [],
'settings' => [
'thousands_separator' => ',',
'decimal_separator' => '.',
'prefix' => '_',
'load_jquery' => 'no'
]
];
$settings = ( false !== $option ) ? json_decode( $option, true ) : [];
$settings = array_merge( $defaults, $settings );
$settings['settings'] = array_merge( $defaults['settings'], $settings['settings'] );
// Store DB-based facet & template names
$db_names = [];
foreach ( $settings['facets'] as $facet ) {
$db_names[ 'facet-' . $facet['name'] ] = true;
}
foreach ( $settings['templates'] as $template ) {
$db_names[ 'template-' . $template['name'] ] = true;
}
// Programmatically registered
$facets = apply_filters( 'facetwp_facets', $settings['facets'] );
$templates = apply_filters( 'facetwp_templates', $settings['templates'] );
$tmp_facets = [];
$tmp_templates = [];
// Merge DB + code-based facets
foreach ( $facets as $facet ) {
$name = $facet['name'];
$is_db_based = isset( $db_names[ "facet-$name" ] );
if ( ! $is_db_based ) {
$facet['_code'] = true;
}
if ( ! $is_db_based || empty( $tmp_facets[ $name ] ) ) {
// Valid facet type?
if ( in_array( $facet['type'], array_keys( $this->facet_types ) ) ) {
$tmp_facets[ $name ] = $facet;
}
}
}
// Merge DB + code-based templates
foreach ( $templates as $template ) {
$name = $template['name'];
$is_db_based = isset( $db_names[ "template-$name" ] );
if ( ! $is_db_based ) {
$template['_code'] = true;
}
if ( ! $is_db_based || empty( $tmp_templates[ $name ] ) ) {
$tmp_templates[ $name ] = $template;
}
}
// Convert back to numerical arrays
$settings['facets'] = array_values( $tmp_facets );
$settings['templates'] = array_values( $tmp_templates );
// Filtered settings
return $settings;
}
/**
* Get a general setting value
*
* @param string $name The setting name
* @param mixed $default The default value
* @since 1.9
*/
function get_setting( $name, $default = '' ) {
return $this->settings['settings'][ $name ] ?? $default;
}
/**
* Get an array of all facets
* @return array
*/
function get_facets() {
return $this->settings['facets'];
}
/**
* Get an array of all templates
* @return array
*/
function get_templates() {
return $this->settings['templates'];
}
/**
* Get all properties for a single facet
* @param string $facet_name
* @return mixed An array of facet info, or false
*/
function get_facet_by_name( $facet_name ) {
foreach ( $this->get_facets() as $facet ) {
if ( $facet_name == $facet['name'] ) {
return $facet;
}
}
return false;
}
/**
* Get all properties for a single template
*
* @param string $template_name
* @return mixed An array of template info, or false
*/
function get_template_by_name( $template_name ) {
foreach ( $this->get_templates() as $template ) {
if ( $template_name == $template['name'] ) {
return $template;
}
}
return false;
}
/**
* Fetch facets using one of its settings
* @param string $setting_name
* @param mixed $setting_value
* @return array
*/
function get_facets_by( $setting, $value ) {
$matches = [];
foreach ( $this->get_facets() as $facet ) {
if ( isset( $facet[ $setting ] ) && $value === $facet[ $setting ] ) {
$matches[] = $facet;
}
}
return $matches;
}
/**
* Get terms across all languages (thanks, WPML)
* @since 3.8.5
*/
function get_terms( $taxonomy ) {
global $wpdb;
$sql = "
SELECT t.term_id, t.name, t.slug, tt.parent FROM {$wpdb->term_taxonomy} tt
INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
WHERE tt.taxonomy = %s";
return $wpdb->get_results( $wpdb->prepare( $sql, $taxonomy ) );
}
/**
* Get an array of term information, including depth
* @param string $taxonomy The taxonomy name
* @return array Term information
* @since 0.9.0
*/
function get_term_depths( $taxonomy ) {
if ( isset( $this->term_cache[ $taxonomy ] ) ) {
return $this->term_cache[ $taxonomy ];
}
$output = [];
$parents = [];
$terms = $this->get_terms( $taxonomy );
// Get term parents
foreach ( $terms as $term ) {
$parents[ $term->term_id ] = $term->parent;
}
// Build the term array
foreach ( $terms as $term ) {
$output[ $term->term_id ] = [
'term_id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'parent_id' => $term->parent,
'depth' => 0
];
$current_parent = $term->parent;
while ( 0 < (int) $current_parent ) {
$current_parent = $parents[ $current_parent ];
$output[ $term->term_id ]['depth']++;
// Prevent an infinite loop
if ( 50 < $output[ $term->term_id ]['depth'] ) {
break;
}
}
}
$this->term_cache[ $taxonomy ] = $output;
return $output;
}
/**
* Finish sorting the facet values
* The results are already sorted by depth and (name OR count), we just need
* to move the children directly below their parents
*/
function sort_taxonomy_values( $values = [], $orderby = 'count' ) {
$final = [];
$cache = [];
// Create an "order" sort value based on the top-level items
foreach ( $values as $key => $val ) {
if ( 0 == $val['depth'] ) {
$val['order'] = $key;
$cache[ $val['term_id'] ] = $key;
$final[] = $val;
}
elseif ( isset( $cache[ $val['parent_id'] ] ) ) { // skip orphans
$val['order'] = $cache[ $val['parent_id'] ] . ".$key"; // dot-separated hierarchy string
$cache[ $val['term_id'] ] = $val['order'];
$final[] = $val;
}
}
// Sort the array based on the new "order" element
// Since this is a dot-separated hierarchy string, use version_compare
usort( $final, function( $a, $b ) {
return version_compare( $a['order'], $b['order'] );
});
return $final;
}
/**
* Sanitize SQL data
* @return mixed The sanitized value(s)
* @since 3.0.7
*/
function sanitize( $input ) {
global $wpdb;
if ( is_array( $input ) ) {
$output = [];
foreach ( $input as $key => $val ) {
$output[ $key ] = $this->sanitize( $val );
}
}
else {
if ( $wpdb->dbh && $wpdb->use_mysqli ) {
$output = mysqli_real_escape_string( $wpdb->dbh, $input );
}
else {
$output = addslashes( $input );
}
}
return $output;
}
/**
* Does an active facet with the specified setting exist?
* @return boolean
* @since 1.4.0
*/
function facet_setting_exists( $setting_name, $setting_value ) {
foreach ( FWP()->facet->facets as $f ) {
if ( isset( $f[ $setting_name ] ) && $f[ $setting_name ] == $setting_value ) {
return true;
}
}
return false;
}
/**
* Does this facet have a setting with the specified value?
* @return boolean
* @since 2.3.4
*/
function facet_is( $facet, $setting_name, $setting_value ) {
if ( is_string( $facet ) ) {
$facet = $this->get_facet_by_name( $facet );
}
if ( isset( $facet[ $setting_name ] ) && $facet[ $setting_name ] == $setting_value ) {
return true;
}
return false;
}
/**
* Hash a facet value if needed
* @return string
* @since 2.1
*/
function safe_value( $value ) {
$value = remove_accents( $value );
if ( preg_match( '/[^a-z0-9_.\- ]/i', $value ) ) {
if ( ! preg_match( '/^\d{4}-(0[1-9]|1[012])-([012]\d|3[01])/', $value ) ) {
$value = md5( $value );
}
}
$value = str_replace( ' ', '-', strtolower( $value ) );
$value = preg_replace( '/[-]{2,}/', '-', $value );
$value = ( 50 < strlen( $value ) ) ? substr( $value, 0, 50 ) : $value;
return $value;
}
/**
* Properly format numbers, taking separators into account
* @return number
* @since 2.7.5
*/
function format_number( $num ) {
$sep_decimal = $this->get_setting( 'decimal_separator' );
$sep_thousands = $this->get_setting( 'thousands_separator' );
$num = str_replace( $sep_thousands, '', $num );
$num = ( ',' == $sep_decimal ) ? str_replace( ',', '.', $num ) : $num;
$num = preg_replace( '/[^0-9-.]/', '', $num );
return $num;
}
/**
* Get facet data sources
* @return array
* @since 2.2.1
*/
function get_data_sources( $context = 'default' ) {
global $wpdb;
// Cached?
if ( ! empty( $this->data_sources ) ) {
$sources = $this->data_sources;
}
else {
// Get excluded meta keys
$excluded_fields = apply_filters( 'facetwp_excluded_custom_fields', [
'_edit_last',
'_edit_lock',
] );
// Get taxonomies
$taxonomies = get_taxonomies( [], 'object' );
// Get custom fields
$meta_keys = $wpdb->get_col( "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} ORDER BY meta_key" );
$custom_fields = array_diff( $meta_keys, $excluded_fields );
$sources = [
'posts' => [
'label' => __( 'Posts', 'fwp' ),
'choices' => [
'post_type' => __( 'Post Type', 'fwp' ),
'post_date' => __( 'Post Date', 'fwp' ),
'post_modified' => __( 'Post Modified', 'fwp' ),
'post_title' => __( 'Post Title', 'fwp' ),
'post_author' => __( 'Post Author', 'fwp' )
],
'weight' => 10
],
'taxonomies' => [
'label' => __( 'Taxonomies', 'fwp' ),
'choices' => [],
'weight' => 20
],
'custom_fields' => [
'label' => __( 'Custom Fields', 'fwp' ),
'choices' => [],
'weight' => 30
]
];
foreach ( $taxonomies as $tax ) {
$sources['taxonomies']['choices'][ 'tax/' . $tax->name ] = $tax->labels->name . ' (' . $tax->name . ')';
}
foreach ( $custom_fields as $cf ) {
if ( 0 !== strpos( $cf, '_oembed_' ) ) {
$sources['custom_fields']['choices'][ 'cf/' . $cf ] = $cf;
}
}
$this->data_sources = $sources;
}
$sources = apply_filters( 'facetwp_facet_sources', $sources, $context );
uasort( $sources, [ $this, 'sort_by_weight' ] );
return $sources;
}
/**
* Sort facetwp_facet_sources by weight
* @since 2.7.5
*/
function sort_by_weight( $a, $b ) {
$a['weight'] = $a['weight'] ?? 10;
$b['weight'] = $b['weight'] ?? 10;
if ( $a['weight'] == $b['weight'] ) {
return 0;
}
return ( $a['weight'] < $b['weight'] ) ? -1 : 1;
}
/**
* Get row counts for all facets
* @since 3.3.4
*/
function get_row_counts() {
if ( isset( $this->row_counts ) ) {
return $this->row_counts;
}
global $wpdb;
$output = [];
$results = $wpdb->get_results( "SELECT facet_name, COUNT(*) AS row_count FROM {$wpdb->prefix}facetwp_index GROUP BY facet_name" );
foreach ( $results as $result ) {
$output[ $result->facet_name ] = (int) $result->row_count;
}
$this->row_counts = $output;
return $output;
}
/**
* Grab the license key
* @since 3.0.3
*/
function get_license_key() {
$license_key = defined( 'FACETWP_LICENSE_KEY' ) ? FACETWP_LICENSE_KEY : get_option( 'facetwp_license' );
$license_key = apply_filters( 'facetwp_license_key', $license_key );
return sanitize_key( trim( $license_key ) );
}
/**
* Determine whether the license is active
* @since 3.3.0
*/
function is_license_active() {
return ( 'success' == $this->get_license_meta( 'status' ) );
}
/**
* Get a license meta value
* Possible keys: status, message, expiration, payment_id, price_id
* @since 3.5.3
*/
function get_license_meta( $key = 'status' ) {
$activation = get_option( 'facetwp_activation' );
if ( ! empty( $activation ) ) {
$data = json_decode( $activation, true );
if ( isset( $data[ $key ] ) ) {
return $data[ $key ];
}
}
return false;
}
}

View File

@@ -0,0 +1,691 @@
<?php
class FacetWP_Indexer
{
/* (boolean) wp_insert_post running? */
public $is_saving = false;
/* (boolean) Whether to index a single post */
public $index_all = false;
/* (int) Number of posts to index before updating progress */
public $chunk_size = 10;
/* (string) Whether a temporary table is active */
public $table;
/* (array) Facet properties for the value being indexed */
public $facet;
/* (array) Value modifiers set via the admin UI */
public $modifiers;
function __construct() {
$this->set_table( 'auto' );
$this->run_cron();
if ( apply_filters( 'facetwp_indexer_is_enabled', true ) ) {
$this->run_hooks();
}
}
/**
* Event listeners
* @since 2.8.4
*/
function run_hooks() {
add_action( 'save_post', [ $this, 'save_post' ] );
add_action( 'delete_post', [ $this, 'delete_post' ] );
add_action( 'edited_term', [ $this, 'edit_term' ], 10, 3 );
add_action( 'delete_term', [ $this, 'delete_term' ], 10, 3 );
add_action( 'set_object_terms', [ $this, 'set_object_terms' ] );
add_action( 'facetwp_indexer_cron', [ $this, 'get_progress' ] );
add_filter( 'wp_insert_post_parent', [ $this, 'is_wp_insert_post' ] );
}
/**
* Cron task
* @since 2.8.5
*/
function run_cron() {
if ( ! wp_next_scheduled( 'facetwp_indexer_cron' ) ) {
wp_schedule_single_event( time() + 300, 'facetwp_indexer_cron' );
}
}
/**
* Update the index when posts get saved
* @since 0.1.0
*/
function save_post( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( false !== wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'auto-draft' == get_post_status( $post_id ) ) {
return;
}
$this->index( $post_id );
$this->is_saving = false;
}
/**
* Update the index when posts get deleted
* @since 0.6.0
*/
function delete_post( $post_id ) {
global $wpdb;
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
/**
* Update the index when terms get saved
* @since 0.6.0
*/
function edit_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
$term = get_term( $term_id, $taxonomy );
$slug = FWP()->helper->safe_value( $term->slug );
$matches = FWP()->helper->get_facets_by( 'source', "tax/$taxonomy" );
if ( ! empty( $matches ) ) {
$facet_names = wp_list_pluck( $matches, 'name' );
$facet_names = implode( "','", esc_sql( $facet_names ) );
$wpdb->query( $wpdb->prepare( "
UPDATE {$this->table}
SET facet_value = %s, facet_display_value = %s
WHERE facet_name IN ('$facet_names') AND term_id = %d",
$slug, $term->name, $term_id
) );
}
}
/**
* Update the index when terms get deleted
* @since 0.6.0
*/
function delete_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
$matches = FWP()->helper->get_facets_by( 'source', "tax/$taxonomy" );
if ( ! empty( $matches ) ) {
$facet_names = wp_list_pluck( $matches, 'name' );
$facet_names = implode( "','", esc_sql( $facet_names ) );
$wpdb->query( "
DELETE FROM {$this->table}
WHERE facet_name IN ('$facet_names') AND term_id = $term_id"
);
}
}
/**
* We're hijacking wp_insert_post_parent
* Prevent our set_object_terms() hook from firing within wp_insert_post
* @since 2.2.2
*/
function is_wp_insert_post( $post_parent ) {
$this->is_saving = true;
return $post_parent;
}
/**
* Support for manual taxonomy associations
* @since 0.8.0
*/
function set_object_terms( $object_id ) {
if ( ! $this->is_saving ) {
$this->index( $object_id );
}
}
/**
* Rebuild the facet index
* @param mixed $post_id The post ID (set to FALSE to re-index everything)
*/
function index( $post_id = false ) {
global $wpdb;
$this->index_all = ( false === $post_id );
// Index everything
if ( $this->index_all ) {
// Store the pre-index settings (see FacetWP_Diff)
update_option( 'facetwp_settings_last_index', get_option( 'facetwp_settings' ), 'no' );
// PHP sessions are blocking, so close if active
if ( PHP_SESSION_ACTIVE === session_status() ) {
session_write_close();
}
// Bypass the PHP timeout
ini_set( 'max_execution_time', 0 );
// Prevent multiple indexing processes
$touch = (int) $this->get_transient( 'touch' );
if ( 0 < $touch ) {
// Run only if the indexer is inactive or stalled
if ( ( time() - $touch ) < 60 ) {
exit;
}
}
else {
// Create temp table
$this->manage_temp_table( 'create' );
}
}
// Index a single post
elseif ( is_int( $post_id ) ) {
// Clear table values
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
// Exit
else {
return;
}
// Resume indexing?
$offset = (int) ( $_POST['offset'] ?? 0 );
$attempt = (int) ( $_POST['retries'] ?? 0 );
if ( 0 < $offset ) {
$post_ids = json_decode( get_option( 'facetwp_indexing' ), true );
}
else {
$post_ids = $this->get_post_ids_to_index( $post_id );
// Store post IDs
if ( $this->index_all ) {
update_option( 'facetwp_indexing', json_encode( $post_ids ) );
}
}
// Count total posts
$num_total = count( $post_ids );
// Get all facet sources
$facets = FWP()->helper->get_facets();
// Populate an array of facet value modifiers
$this->load_value_modifiers( $facets );
foreach ( $post_ids as $counter => $post_id ) {
// Advance until we reach the offset
if ( $counter < $offset ) {
continue;
}
// Update the progress bar
if ( $this->index_all ) {
if ( 0 == ( $counter % $this->chunk_size ) ) {
$num_retries = (int) $this->get_transient( 'retries' );
// Exit if newer retries exist
if ( $attempt < $num_retries ) {
exit;
}
// Exit if the indexer was cancelled
wp_cache_delete( 'facetwp_indexing_cancelled', 'options' );
if ( 'yes' === get_option( 'facetwp_indexing_cancelled', 'no' ) ) {
update_option( 'facetwp_transients', '' );
update_option( 'facetwp_indexing', '' );
$this->manage_temp_table( 'delete' );
exit;
}
$transients = [
'num_indexed' => $counter,
'num_total' => $num_total,
'retries' => $attempt,
'touch' => time(),
];
update_option( 'facetwp_transients', json_encode( $transients ) );
}
}
// If the indexer stalled, start from the last valid chunk
if ( 0 < $offset && ( $counter - $offset < $this->chunk_size ) ) {
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
$this->index_post( $post_id, $facets );
}
// Indexing complete
if ( $this->index_all ) {
update_option( 'facetwp_last_indexed', time(), 'no' );
update_option( 'facetwp_transients', '', 'no' );
update_option( 'facetwp_indexing', '', 'no' );
$this->manage_temp_table( 'replace' );
$this->manage_temp_table( 'delete' );
}
do_action( 'facetwp_indexer_complete' );
}
/**
* Get an array of post IDs to index
* @since 3.6.8
*/
function get_post_ids_to_index( $post_id = false ) {
$args = [
'post_type' => 'any',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'orderby' => 'ID',
'cache_results' => false,
'no_found_rows' => true,
];
if ( is_int( $post_id ) ) {
$args['p'] = $post_id;
$args['posts_per_page'] = 1;
}
$args = apply_filters( 'facetwp_indexer_query_args', $args );
$query = new WP_Query( $args );
return (array) $query->posts;
}
/**
* Index an individual post
* @since 3.6.8
*/
function index_post( $post_id, $facets ) {
// Force WPML to change the language
do_action( 'facetwp_indexer_post', [ 'post_id' => $post_id ] );
// Loop through all facets
foreach ( $facets as $facet ) {
// Do not index search facets
if ( 'search' == $facet['type'] ) {
continue;
}
$this->facet = $facet;
$source = $facet['source'] ?? '';
// Set default index_row() params
$defaults = [
'post_id' => $post_id,
'facet_name' => $facet['name'],
'facet_source' => $source,
'facet_value' => '',
'facet_display_value' => '',
'term_id' => 0,
'parent_id' => 0,
'depth' => 0,
'variation_id' => 0,
];
$defaults = apply_filters( 'facetwp_indexer_post_facet_defaults', $defaults, [
'facet' => $facet
] );
// Set flag for custom handling
$this->is_overridden = true;
// Bypass default indexing
$bypass = apply_filters( 'facetwp_indexer_post_facet', false, [
'defaults' => $defaults,
'facet' => $facet
] );
if ( $bypass ) {
continue;
}
$this->is_overridden = false;
// Get rows to insert
$rows = $this->get_row_data( $defaults );
foreach ( $rows as $row ) {
$this->index_row( $row );
}
}
}
/**
* Get data for a table row
* @since 2.1.1
*/
function get_row_data( $defaults ) {
$output = [];
$facet = $this->facet;
$post_id = $defaults['post_id'];
$source = $defaults['facet_source'];
if ( 'tax/' == substr( $source, 0, 4 ) ) {
$used_terms = [];
$taxonomy = substr( $source, 4 );
$term_objects = wp_get_object_terms( $post_id, $taxonomy );
if ( is_wp_error( $term_objects ) ) {
return $output;
}
// Store the term depths
$hierarchy = FWP()->helper->get_term_depths( $taxonomy );
// Only index child terms
$children = false;
if ( ! empty( $facet['parent_term'] ) ) {
$children = get_term_children( $facet['parent_term'], $taxonomy );
}
foreach ( $term_objects as $term ) {
// If "parent_term" is set, only index children
if ( false !== $children && ! in_array( $term->term_id, $children ) ) {
continue;
}
// Prevent duplicate terms
if ( isset( $used_terms[ $term->term_id ] ) ) {
continue;
}
$used_terms[ $term->term_id ] = true;
// Handle hierarchical taxonomies
$term_info = $hierarchy[ $term->term_id ];
$depth = $term_info['depth'];
// Adjust depth if parent_term is set
if ( ! empty( $facet['parent_term'] ) ) {
if ( isset( $hierarchy[ $facet['parent_term'] ] ) ) {
$anchor = (int) $hierarchy[ $facet['parent_term'] ]['depth'] + 1;
$depth = ( $depth - $anchor );
}
}
$params = $defaults;
$params['facet_value'] = $term->slug;
$params['facet_display_value'] = $term->name;
$params['term_id'] = $term->term_id;
$params['parent_id'] = $term_info['parent_id'];
$params['depth'] = $depth;
$output[] = $params;
// Automatically index implicit parents
if ( 'hierarchy' == $facet['type'] || FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' ) ) {
while ( $depth > 0 ) {
$term_id = $term_info['parent_id'];
$term_info = $hierarchy[ $term_id ];
$depth = $depth - 1;
if ( ! isset( $used_terms[ $term_id ] ) ) {
$used_terms[ $term_id ] = true;
$params = $defaults;
$params['facet_value'] = $term_info['slug'];
$params['facet_display_value'] = $term_info['name'];
$params['term_id'] = $term_id;
$params['parent_id'] = $term_info['parent_id'];
$params['depth'] = $depth;
$output[] = $params;
}
}
}
}
}
elseif ( 'cf/' == substr( $source, 0, 3 ) ) {
$source_noprefix = substr( $source, 3 );
$values = get_post_meta( $post_id, $source_noprefix, false );
foreach ( $values as $value ) {
$params = $defaults;
$params['facet_value'] = $value;
$params['facet_display_value'] = $value;
$output[] = $params;
}
}
elseif ( 'post' == substr( $source, 0, 4 ) ) {
$post = get_post( $post_id );
$value = $post->{$source};
$display_value = $value;
if ( 'post_author' == $source ) {
$user = get_user_by( 'id', $value );
$display_value = ( $user instanceof WP_User ) ? $user->display_name : $value;
}
elseif ( 'post_type' == $source ) {
$post_type = get_post_type_object( $value );
if ( isset( $post_type->labels->name ) ) {
$display_value = $post_type->labels->name;
}
}
$params = $defaults;
$params['facet_value'] = $value;
$params['facet_display_value'] = $display_value;
$output[] = $params;
}
return apply_filters( 'facetwp_indexer_row_data', $output, [
'defaults' => $defaults,
'facet' => $this->facet
] );
}
/**
* Index a facet value
* @since 0.6.0
*/
function index_row( $params ) {
// Allow for custom indexing
$params = apply_filters( 'facetwp_index_row', $params, $this );
// Allow hooks to bypass the row insertion
if ( is_array( $params ) ) {
$this->insert( $params );
}
}
/**
* Save a facet value to DB
* This can be trigged by "facetwp_index_row" to handle multiple values
* @since 1.2.5
*/
function insert( $params ) {
global $wpdb;
$value = $params['facet_value'];
$display_value = $params['facet_display_value'];
// Only accept scalar values
if ( '' === $value || ! is_scalar( $value ) ) {
return;
}
// Apply UI-based modifiers
if ( isset( $this->modifiers[ $params['facet_name'] ] ) ) {
$mod = $this->modifiers[ $params['facet_name' ] ];
$is_match = in_array( $display_value, $mod['values'] );
if ( ( 'exclude' == $mod['type'] && $is_match ) || ( 'include' == $mod['type'] && ! $is_match ) ) {
return;
}
}
$wpdb->query( $wpdb->prepare( "INSERT INTO {$this->table}
(post_id, facet_name, facet_value, facet_display_value, term_id, parent_id, depth, variation_id) VALUES (%d, %s, %s, %s, %d, %d, %d, %d)",
$params['post_id'],
$params['facet_name'],
FWP()->helper->safe_value( $value ),
$display_value,
$params['term_id'],
$params['parent_id'],
$params['depth'],
$params['variation_id']
) );
}
/**
* Get the indexing completion percentage
* @return mixed The decimal percentage, or -1
* @since 0.1.0
*/
function get_progress() {
$return = -1;
$num_indexed = (int) $this->get_transient( 'num_indexed' );
$num_total = (int) $this->get_transient( 'num_total' );
$retries = (int) $this->get_transient( 'retries' );
$touch = (int) $this->get_transient( 'touch' );
if ( 0 < $num_total ) {
// Resume a stalled indexer
if ( 60 < ( time() - $touch ) ) {
$post_data = [
'blocking' => false,
'timeout' => 0.02,
'body' => [
'action' => 'facetwp_resume_index',
'offset' => $num_indexed,
'retries' => $retries + 1,
'touch' => $touch
]
];
wp_remote_post( admin_url( 'admin-ajax.php' ), $post_data );
}
// Calculate the percent completion
if ( $num_indexed != $num_total ) {
$return = round( 100 * ( $num_indexed / $num_total ), 2 );
}
}
return $return;
}
/**
* Get indexer transient variables
* @since 1.7.8
*/
function get_transient( $name = false ) {
$transients = get_option( 'facetwp_transients' );
if ( ! empty( $transients ) ) {
$transients = json_decode( $transients, true );
if ( $name ) {
return $transients[ $name ] ?? false;
}
return $transients;
}
return false;
}
/**
* Set either the index or temp table
* @param string $table 'auto', 'index', or 'temp'
* @since 4.1.4
*/
function set_table( $table = 'auto' ) {
global $wpdb;
if ( 'auto' == $table ) {
$table = ( '' == get_option( 'facetwp_indexing', '' ) ) ? 'index' : 'temp';
}
$this->table = $wpdb->prefix . 'facetwp_' . $table;
}
/**
* Index table management
* @since 3.5
*/
function manage_temp_table( $action = 'create' ) {
global $wpdb;
$table = $wpdb->prefix . 'facetwp_index';
$temp_table = $wpdb->prefix . 'facetwp_temp';
if ( 'create' == $action ) {
$wpdb->query( "CREATE TABLE $temp_table LIKE $table" );
$this->set_table( 'temp' );
}
elseif ( 'replace' == $action ) {
$wpdb->query( "TRUNCATE TABLE $table" );
$wpdb->query( "INSERT INTO $table SELECT * FROM $temp_table" );
}
elseif ( 'delete' == $action ) {
$wpdb->query( "DROP TABLE IF EXISTS $temp_table" );
$this->set_table( 'index' );
}
}
/**
* Populate an array of facet value modifiers (defined in the admin UI)
* @since 3.5.6
*/
function load_value_modifiers( $facets ) {
$output = [];
foreach ( $facets as $facet ) {
$name = $facet['name'];
$type = empty( $facet['modifier_type'] ) ? 'off' : $facet['modifier_type'];
if ( 'include' == $type || 'exclude' == $type ) {
$temp = preg_split( '/\r\n|\r|\n/', trim( $facet['modifier_values'] ) );
$values = [];
// Compare using both original and encoded values
foreach ( $temp as $val ) {
$val = trim( $val );
$val_encoded = htmlentities( $val );
$val_decoded = html_entity_decode( $val );
$values[ $val ] = true;
$values[ $val_encoded ] = true;
$values[ $val_decoded ] = true;
}
$output[ $name ] = [ 'type' => $type, 'values' => array_keys( $values ) ];
}
}
$this->modifiers = $output;
}
}

View File

@@ -0,0 +1,157 @@
<?php
class FacetWP_Init
{
function __construct() {
add_action( 'init', [ $this, 'init' ], 20 );
add_filter( 'woocommerce_is_rest_api_request', [ $this, 'is_rest_api_request' ] );
}
/**
* Initialize classes and WP hooks
*/
function init() {
// i18n
$this->load_textdomain();
// is_plugin_active
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
$includes = [
'api/fetch',
'api/refresh',
'class-helper',
'class-ajax',
'class-request',
'class-renderer',
'class-diff',
'class-indexer',
'class-display',
'class-builder',
'class-overrides',
'class-settings',
'class-upgrade',
'functions'
];
foreach ( $includes as $inc ) {
include ( FACETWP_DIR . "/includes/$inc.php" );
}
new FacetWP_Upgrade();
new FacetWP_Overrides();
FWP()->api = new FacetWP_API_Fetch();
FWP()->helper = new FacetWP_Helper();
FWP()->facet = new FacetWP_Renderer();
FWP()->settings = new FacetWP_Settings();
FWP()->diff = new FacetWP_Diff();
FWP()->indexer = new FacetWP_Indexer();
FWP()->display = new FacetWP_Display();
FWP()->builder = new FacetWP_Builder();
FWP()->request = new FacetWP_Request();
FWP()->ajax = new FacetWP_Ajax();
// integrations
include( FACETWP_DIR . '/includes/integrations/searchwp/searchwp.php' );
include( FACETWP_DIR . '/includes/integrations/woocommerce/woocommerce.php' );
include( FACETWP_DIR . '/includes/integrations/edd/edd.php' );
include( FACETWP_DIR . '/includes/integrations/acf/acf.php' );
include( FACETWP_DIR . '/includes/integrations/wp-cli/wp-cli.php' );
include( FACETWP_DIR . '/includes/integrations/wp-rocket/wp-rocket.php' );
// update checks
include( FACETWP_DIR . '/includes/class-updater.php' );
// hooks
add_action( 'admin_menu', [ $this, 'admin_menu' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ], 10, 2 );
add_filter( 'plugin_action_links_facetwp/index.php', [ $this, 'plugin_action_links' ] );
do_action( 'facetwp_init' );
}
/**
* i18n support
*/
function load_textdomain() {
// admin-facing
load_plugin_textdomain( 'fwp' );
// front-facing
load_plugin_textdomain( 'fwp-front', false, basename( FACETWP_DIR ) . '/languages' );
}
/**
* Register the FacetWP settings page
*/
function admin_menu() {
add_options_page( 'FacetWP', 'FacetWP', 'manage_options', 'facetwp', [ $this, 'settings_page' ] );
}
/**
* Enqueue jQuery
*/
function front_scripts() {
if ( 'yes' == FWP()->helper->get_setting( 'load_jquery', 'yes' ) ) {
wp_enqueue_script( 'jquery' );
}
}
/**
* Route to the correct edit screen
*/
function settings_page() {
include( FACETWP_DIR . '/templates/page-settings.php' );
}
/**
* Prevent WP from redirecting FWP pager to /page/X
*/
function redirect_canonical( $redirect_url, $requested_url ) {
if ( false !== strpos( $redirect_url, FWP()->helper->get_setting( 'prefix' ) . 'paged' ) ) {
return false;
}
return $redirect_url;
}
/**
* Add "Settings" link to plugin listing page
*/
function plugin_action_links( $links ) {
$settings_link = admin_url( 'options-general.php?page=facetwp' );
$settings_link = '<a href=" ' . $settings_link . '">' . __( 'Settings', 'fwp' ) . '</a>';
array_unshift( $links, $settings_link );
return $links;
}
/**
* WooCommerce 3.6+ doesn't load its frontend includes for REST API requests
* We need to force-load these includes for FacetWP refreshes
* See includes() within class-woocommerce.php
*
* This code isn't within /integrations/woocommerce/ because it runs *before* init
*
* @since 3.3.10
*/
function is_rest_api_request( $request ) {
if ( false !== strpos( $_SERVER['REQUEST_URI'], 'facetwp' ) ) {
return false;
}
return $request;
}
}
$this->init = new FacetWP_Init();

View File

@@ -0,0 +1,103 @@
<?php
class FacetWP_Overrides
{
public $raw;
function __construct() {
add_filter( 'facetwp_index_row', [ $this, 'index_row' ], 5, 2 );
add_filter( 'facetwp_index_row', [ $this, 'format_numbers' ], 15, 2 );
add_filter( 'facetwp_is_main_query', [ $this, 'ignore_post_types' ], 10, 2 );
}
/**
* Indexer modifications
*/
function index_row( $params, $class ) {
if ( $class->is_overridden ) {
return $params;
}
$facet = FWP()->helper->get_facet_by_name( $params['facet_name'] );
// Store raw numbers to format later
if ( in_array( $facet['type'], [ 'number_range', 'slider' ] ) ) {
$this->raw = [
'value' => $params['facet_value'],
'label' => $params['facet_display_value']
];
}
// Support "Other data source" values
if ( ! empty( $facet['source_other'] ) ) {
$other_params = $params;
$other_params['facet_source'] = $facet['source_other'];
$rows = $class->get_row_data( $other_params );
$params['facet_display_value'] = $rows[0]['facet_display_value'];
}
return $params;
}
/**
* Make sure that numbers are properly formatted
*/
function format_numbers( $params, $class ) {
if ( empty( $this->raw ) ) {
return $params;
}
$value = $params['facet_value'];
$label = $params['facet_display_value'];
// Only format if un-altered
if ( $this->raw['value'] === $value && $this->raw['label'] === $label ) {
$params['facet_value'] = FWP()->helper->format_number( $this->raw['value'] );
$params['facet_display_value'] = FWP()->helper->format_number( $this->raw['label'] );
}
$this->raw = null;
return $params;
}
/**
* Ignore certain post types
*/
function ignore_post_types( $is_main_query, $query ) {
$blacklist = [
'acf-field',
'acf-field-group',
'advanced_ads',
'carts',
'cookielawinfo',
'edd_wish_list',
'ms_relationship',
'nav_menu_item',
'wc_user_membership',
'wp_block',
'wp_global_styles',
'wp_navigation',
'wp_template',
'wp_template_part'
];
$post_type = $query->get( 'post_type' );
if ( is_string( $post_type ) && in_array( $post_type, $blacklist ) ) {
$is_main_query = false;
}
// Ignore the "WP GDPR Compliance" plugin
if ( '[wpgdprc_access_request_form]' == $query->get( 's' ) ) {
$is_main_query = false;
}
return $is_main_query;
}
}

View File

@@ -0,0 +1,662 @@
<?php
class FacetWP_Renderer
{
/* (array) Data for the current facets */
public $facets;
/* (array) Data for the current template */
public $template;
/* (array) WP_Query arguments */
public $query_args;
/* (array) Data used to build the pager */
public $pager_args;
/* (string) MySQL WHERE clause passed to each facet */
public $where_clause = '';
/* (array) AJAX parameters passed in */
public $ajax_params;
/* (array) HTTP parameters from the original page (URI, GET) */
public $http_params;
/* (boolean) Is search active? */
public $is_search = false;
/* (boolean) Are we preloading? */
public $is_preload = false;
/* (array) Cache preloaded facet values */
public $preloaded_values;
/* (array) The final WP_Query object */
public $query;
function __construct() {
$this->facet_types = FWP()->helper->facet_types;
}
/**
* Generate the facet output
* @param array $params An array of arrays (see the FacetWP->refresh() method)
* @return array
*/
function render( $params ) {
$output = [
'facets' => [],
'template' => '',
'settings' => [],
];
// Hook params
$params = apply_filters( 'facetwp_render_params', $params );
// First ajax refresh?
$first_load = (bool) $params['first_load'];
$is_bfcache = (bool) $params['is_bfcache'];
// Initial pageload?
$this->is_preload = isset( $params['is_preload'] );
// Set the AJAX and HTTP params
$this->ajax_params = $params;
$this->http_params = $params['http_params'];
// Validate facets
$this->facets = [];
foreach ( $params['facets'] as $f ) {
$name = $f['facet_name'];
$facet = FWP()->helper->get_facet_by_name( $name );
if ( $facet ) {
// Default to "OR" mode
$facet['operator'] = $facet['operator'] ?? 'or';
// Support the "facetwp_preload_url_vars" hook
if ( $first_load && empty( $f['selected_values'] ) && ! empty( $this->http_params['url_vars'][ $name ] ) ) {
$f['selected_values'] = $this->http_params['url_vars'][ $name ];
}
// Support commas within search / autocomplete facets
if ( 'search' == $facet['type'] || 'autocomplete' == $facet['type'] ) {
$f['selected_values'] = implode( ',', (array) $f['selected_values'] );
}
$facet['selected_values'] = FWP()->helper->sanitize( $f['selected_values'] );
$this->facets[ $name ] = $facet;
}
}
// Get the template from $helper->settings
if ( 'wp' == $params['template'] ) {
$this->template = [ 'name' => 'wp' ];
$query_args = FWP()->request->query_vars ?? [];
}
else {
$this->template = FWP()->helper->get_template_by_name( $params['template'] );
$query_args = $this->get_query_args();
}
// Detect search string
if ( ! empty( $query_args['s'] ) ) {
$this->is_search = true;
}
// Run the query once (prevent duplicate queries when preloading)
if ( empty( $this->query_args ) ) {
// Support "post__in"
if ( empty( $query_args['post__in'] ) ) {
$query_args['post__in'] = [];
}
// Get the template "query" field
$query_args = apply_filters( 'facetwp_query_args', $query_args, $this );
// Pagination
$query_args['paged'] = empty( $params['paged'] ) ? 1 : (int) $params['paged'];
// Preserve SQL_CALC_FOUND_ROWS
unset( $query_args['no_found_rows'] );
// Narrow posts based on facet selections
$post_ids = $this->get_filtered_post_ids( $query_args );
// Update the SQL query
if ( ! empty( $post_ids ) ) {
if ( FWP()->is_filtered ) {
$query_args['post__in'] = $post_ids;
}
$this->where_clause = ' AND post_id IN (' . implode( ',', $post_ids ) . ')';
}
// Set the default limit
if ( empty( $query_args['posts_per_page'] ) ) {
$query_args['posts_per_page'] = (int) get_option( 'posts_per_page' );
}
// Adhere to the "per page" box
$per_page = isset( $params['extras']['per_page'] ) ? $params['extras']['per_page'] : '';
if ( ! empty( $per_page ) && 'default' != $per_page ) {
$query_args['posts_per_page'] = (int) $per_page;
}
$this->query_args = apply_filters( 'facetwp_filtered_query_args', $query_args, $this );
// Run the WP_Query
$this->query = new WP_Query( $this->query_args );
}
// Debug
if ( 'on' == FWP()->helper->get_setting( 'debug_mode', 'off' ) ) {
$output['settings']['debug'] = $this->get_debug_info();
}
// Generate the template HTML
// For performance gains, skip the template on pageload
if ( 'wp' != $this->template['name'] ) {
if ( ! $first_load || $is_bfcache || apply_filters( 'facetwp_template_force_load', false ) ) {
$output['template'] = $this->get_template_html();
}
}
// Don't render these facets
$frozen_facets = $params['frozen_facets'];
// Calculate pager args
$pager_args = [
'page' => (int) $this->query_args['paged'],
'per_page' => (int) $this->query_args['posts_per_page'],
'total_rows' => (int) $this->query->found_posts,
'total_pages' => 1,
];
if ( 0 < $pager_args['per_page'] ) {
$pager_args['total_pages'] = ceil( $pager_args['total_rows'] / $pager_args['per_page'] );
}
$pager_args = apply_filters( 'facetwp_pager_args', $pager_args, $this );
$this->pager_args = $pager_args;
// Stick the pager args into the JSON response
$output['settings']['pager'] = $pager_args;
// Display the pagination HTML
if ( isset( $params['extras']['pager'] ) ) {
$output['pager'] = $this->paginate( $pager_args );
}
// Display the "per page" HTML
if ( isset( $params['extras']['per_page'] ) ) {
$output['per_page'] = $this->get_per_page_box();
}
// Display the counts HTML
if ( isset( $params['extras']['counts'] ) ) {
$output['counts'] = $this->get_result_count( $pager_args );
}
// Not paging
if ( 0 == $params['soft_refresh'] ) {
$output['settings']['num_choices'] = [];
}
// Get facet data
foreach ( $this->facets as $facet_name => $the_facet ) {
$facet_type = $the_facet['type'];
$ui_type = empty( $the_facet['ui_type'] ) ? $facet_type : $the_facet['ui_type'];
// Invalid facet type
if ( ! isset( $this->facet_types[ $facet_type ] ) ) {
continue;
}
// Skip facets when paging
if ( 0 < $params['soft_refresh'] && 'pager' != $facet_type ) {
continue;
}
// Get facet labels
if ( 0 == $params['soft_refresh'] ) {
$output['settings']['labels'][ $facet_name ] = facetwp_i18n( $the_facet['label'] );
}
// Load all facets on back / forward button press (first_load = true)
if ( ! $first_load ) {
// Skip frozen facets
if ( isset( $frozen_facets[ $facet_name ] ) ) {
continue;
}
}
$args = [
'facet' => $the_facet,
'where_clause' => $this->where_clause,
'selected_values' => $the_facet['selected_values'],
];
// Load facet values if needed
if ( method_exists( $this->facet_types[ $facet_type ], 'load_values' ) ) {
// Grab preloaded values if available
if ( isset( $this->preloaded_values[ $facet_name ] ) ) {
$args['values'] = $this->preloaded_values[ $facet_name ];
}
else {
$args['values'] = $this->facet_types[ $facet_type ]->load_values( $args );
if ( $this->is_preload ) {
$this->preloaded_values[ $facet_name ] = $args['values'];
}
}
}
// Filter the render args
$args = apply_filters( 'facetwp_facet_render_args', $args );
// Return the number of available choices
if ( isset( $args['values'] ) ) {
$num_choices = 0;
$is_ghost = FWP()->helper->facet_is( $the_facet, 'ghosts', 'yes' );
foreach ( $args['values'] as $choice ) {
if ( isset( $choice['counter'] ) && ( 0 < $choice['counter'] || $is_ghost ) ) {
$num_choices++;
}
}
$output['settings']['num_choices'][ $facet_name ] = $num_choices;
}
// Generate the facet HTML
$html = $this->facet_types[ $ui_type ]->render( $args );
$output['facets'][ $facet_name ] = apply_filters( 'facetwp_facet_html', $html, $args );
// Return any JS settings
if ( method_exists( $this->facet_types[ $ui_type ], 'settings_js' ) ) {
$output['settings'][ $facet_name ] = $this->facet_types[ $ui_type ]->settings_js( $args );
}
// Grab num_choices for slider facets
if ( 'slider' == $the_facet['type'] ) {
$min = $output['settings'][ $facet_name ]['range']['min'];
$max = $output['settings'][ $facet_name ]['range']['max'];
$output['settings']['num_choices'][ $facet_name ] = ( $min == $max ) ? 0 : 1;
}
}
return apply_filters( 'facetwp_render_output', $output, $params );
}
/**
* Get WP_Query arguments by executing the template "query" field
* @return null
*/
function get_query_args() {
$defaults = [];
// Allow templates to piggyback archives
if ( apply_filters( 'facetwp_template_use_archive', false ) ) {
$main_query = $GLOBALS['wp_the_query'];
// Initial pageload
if ( $main_query->is_archive || $main_query->is_search ) {
if ( $main_query->is_category ) {
$defaults['cat'] = $main_query->get( 'cat' );
}
elseif ( $main_query->is_tag ) {
$defaults['tag_id'] = $main_query->get( 'tag_id' );
}
elseif ( $main_query->is_tax ) {
$defaults['taxonomy'] = $main_query->get( 'taxonomy' );
$defaults['term'] = $main_query->get( 'term' );
}
elseif ( $main_query->is_search ) {
$defaults['s'] = $main_query->get( 's' );
}
$this->archive_args = $defaults;
}
// Subsequent ajax requests
elseif ( ! empty( $this->http_params['archive_args'] ) ) {
foreach ( $this->http_params['archive_args'] as $key => $val ) {
if ( in_array( $key, [ 'cat', 'tag_id', 'taxonomy', 'term', 's' ] ) ) {
$defaults[ $key ] = $val;
}
}
}
}
// Use the query builder
if ( isset( $this->template['modes'] ) && 'visual' == $this->template['modes']['query'] ) {
$query_args = FWP()->builder->parse_query_obj( $this->template['query_obj'] );
}
else {
// remove UTF-8 non-breaking spaces
$query_args = preg_replace( "/\xC2\xA0/", ' ', $this->template['query'] );
$query_args = (array) eval( '?>' . $query_args );
}
// Merge the two arrays
return array_merge( $defaults, $query_args );
}
/**
* Get ALL post IDs for the matching query
* @return array An array of post IDs
*/
function get_filtered_post_ids( $query_args = [] ) {
if ( empty( $query_args ) ) {
$query_args = $this->query_args;
}
// Only get relevant post IDs
$args = array_merge( $query_args, [
'paged' => 1,
'posts_per_page' => -1,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'cache_results' => false,
'no_found_rows' => true,
'nopaging' => true, // prevent "offset" issues
'facetwp' => false,
'fields' => 'ids',
] );
$query = new WP_Query( $args );
// Allow hooks to modify the default post IDs
$post_ids = apply_filters( 'facetwp_pre_filtered_post_ids', $query->posts, $this );
// Store the unfiltered post IDs
FWP()->unfiltered_post_ids = $post_ids;
foreach ( $this->facets as $facet_name => $the_facet ) {
$facet_type = $the_facet['type'];
// Stop looping
if ( empty( $post_ids ) ) {
break;
}
$matches = [];
$selected_values = $the_facet['selected_values'];
if ( empty( $selected_values ) ) {
continue;
}
// Handle each facet
if ( isset( $this->facet_types[ $facet_type ] ) ) {
$hook_params = [
'facet' => $the_facet,
'selected_values' => $selected_values,
];
// Hook to support custom filter_posts() handler
$matches = apply_filters( 'facetwp_facet_filter_posts', false, $hook_params );
if ( false === $matches ) {
$matches = $this->facet_types[ $facet_type ]->filter_posts( $hook_params );
}
}
// Skip this facet
if ( 'continue' == $matches ) {
continue;
}
// Force array
$matches = (array) $matches;
// Store post IDs per facet (needed for "OR" mode)
FWP()->or_values[ $facet_name ] = $matches;
if ( 'search' == $facet_type ) {
$this->is_search = true;
}
// For search facets, loop through $matches to set order
// For other facets, loop through $post_ids to preserve the existing order
$needles = ( 'search' == $facet_type ) ? $matches : $post_ids;
$haystack = ( 'search' == $facet_type ) ? $post_ids : $matches;
$haystack = array_flip( $haystack );
$intersected_ids = [];
foreach ( $needles as $post_id ) {
if ( isset( $haystack[ $post_id ] ) ) {
$intersected_ids[] = $post_id;
}
}
$post_ids = $intersected_ids;
}
$post_ids = apply_filters( 'facetwp_filtered_post_ids', array_values( $post_ids ), $this );
// Store the filtered post IDs
FWP()->filtered_post_ids = $post_ids;
// Set a flag for whether filtering is applied
FWP()->is_filtered = ( FWP()->filtered_post_ids !== FWP()->unfiltered_post_ids );
// Return a zero array if no matches
return empty( $post_ids ) ? [ 0 ] : $post_ids;
}
/**
* Run the template display code
* @return string (HTML)
*/
function get_template_html() {
global $post, $wp_query;
$output = apply_filters( 'facetwp_template_html', false, $this );
if ( false === $output ) {
ob_start();
// Preserve globals
$temp_post = is_object( $post ) ? clone $post : $post;
$temp_wp_query = is_object( $wp_query ) ? clone $wp_query : $wp_query;
$query = $this->query;
$wp_query = $query; // Make $query->blah() optional
if ( isset( $this->template['modes'] ) && 'visual' == $this->template['modes']['display'] ) {
echo FWP()->builder->render_layout( $this->template['layout'] );
}
else {
// Remove UTF-8 non-breaking spaces
$display_code = $this->template['template'];
$display_code = preg_replace( "/\xC2\xA0/", ' ', $display_code );
eval( '?>' . $display_code );
}
// Reset globals
$post = $temp_post;
$wp_query = $temp_wp_query;
// Store buffered output
$output = ob_get_clean();
}
$output = preg_replace( "/\xC2\xA0/", ' ', $output );
return $output;
}
/**
* Result count (1-10 of 234)
* @param array $params An array with "page", "per_page", and "total_rows"
* @return string
*/
function get_result_count( $params = [] ) {
$text_of = __( 'of', 'fwp-front' );
$page = (int) $params['page'];
$per_page = (int) $params['per_page'];
$total_rows = (int) $params['total_rows'];
if ( $per_page < $total_rows ) {
$lower = ( 1 + ( ( $page - 1 ) * $per_page ) );
$upper = ( $page * $per_page );
$upper = ( $total_rows < $upper ) ? $total_rows : $upper;
$output = "$lower-$upper $text_of $total_rows";
}
else {
$lower = ( 0 < $total_rows ) ? 1 : 0;
$upper = $total_rows;
$output = $total_rows;
}
return apply_filters( 'facetwp_result_count', $output, [
'lower' => $lower,
'upper' => $upper,
'total' => $total_rows,
] );
}
/**
* Pagination
* @param array $params An array with "page", "per_page", and "total_rows"
* @return string
*/
function paginate( $params = [] ) {
$pager_class = FWP()->helper->facet_types['pager'];
$pager_class->pager_args = $params;
$output = $pager_class->render_numbers([
'inner_size' => 2,
'dots_label' => '…',
'prev_label' => '&lt;&lt;',
'next_label' => '&gt;&gt;',
]);
return apply_filters( 'facetwp_pager_html', $output, $params );
}
/**
* "Per Page" dropdown box
* @return string
*/
function get_per_page_box() {
$pager_class = FWP()->helper->facet_types['pager'];
$pager_class->pager_args = $this->pager_args;
$options = apply_filters( 'facetwp_per_page_options', [ 10, 25, 50, 100 ] );
$output = $pager_class->render_per_page([
'default_label' => __( 'Per page', 'fwp-front' ),
'per_page_options' => implode( ',', $options )
]);
return apply_filters( 'facetwp_per_page_html', $output, [
'options' => $options
] );
}
/**
* Get debug info for the browser console
* @since 3.5.7
*/
function get_debug_info() {
$last_indexed = get_option( 'facetwp_last_indexed' );
$last_indexed = $last_indexed ? human_time_diff( $last_indexed ) : 'never';
$debug = [
'query_args' => $this->query_args,
'sql' => $this->query->request,
'facets' => $this->facets,
'template' => $this->template,
'settings' => FWP()->helper->settings['settings'],
'last_indexed' => $last_indexed,
'row_counts' => FWP()->helper->get_row_counts(),
'hooks_used' => $this->get_hooks_used()
];
// Reduce debug payload
if ( ! empty( $this->query_args['post__in'] ) ) {
$debug['query_args']['post__in_count'] = count( $this->query_args['post__in'] );
$debug['query_args']['post__in'] = array_slice( $this->query_args['post__in'], 0, 10 );
$debug['sql'] = preg_replace_callback( '/posts.ID IN \((.*?)\)/s', function( $matches ) {
$count = substr_count( $matches[1], ',' ) + 1;
return ( $count <= 10 ) ? $matches[0] : "posts.ID IN (<$count IDs>)";
}, $debug['sql'] );
}
return $debug;
}
/**
* Display the location of relevant hooks (for Debug Mode)
* @since 3.5.7
*/
function get_hooks_used() {
$relevant_hooks = [];
foreach ( $GLOBALS['wp_filter'] as $tag => $hook_data ) {
if ( 0 === strpos( $tag, 'facetwp' ) || 'pre_get_posts' == $tag ) {
foreach ( $hook_data->callbacks as $callbacks ) {
foreach ( $callbacks as $cb ) {
if ( is_string( $cb['function'] ) && false !== strpos( $cb['function'], '::' ) ) {
$cb['function'] = explode( '::', $cb['function'] );
}
if ( is_array( $cb['function'] ) ) {
$class = is_object( $cb['function'][0] ) ? get_class( $cb['function'][0] ) : $cb['function'][0];
$ref = new ReflectionMethod( $class, $cb['function'][1] );
}
elseif ( is_object( $cb['function'] ) ) {
if ( is_a( $cb['function'], 'Closure' ) ) {
$ref = new ReflectionFunction( $cb['function'] );
}
else {
$class = get_class( $cb['function'] );
$ref = new ReflectionMethod( $class, '__invoke' );
}
}
else {
$ref = new ReflectionFunction( $cb['function'] );
}
$filename = str_replace( ABSPATH, '', $ref->getFileName() );
// ignore built-in hooks
if ( false === strpos( $filename, 'plugins/facetwp' ) ) {
if ( false !== strpos( $filename, 'wp-content' ) ) {
$relevant_hooks[ $tag ][] = $filename . ':' . $ref->getStartLine();
}
}
}
}
}
}
return $relevant_hooks;
}
}

View File

@@ -0,0 +1,379 @@
<?php
class FacetWP_Request
{
/* (array) FacetWP-related GET variables */
public $url_vars = [];
/* (mixed) The main query vars */
public $query_vars = null;
/* (boolean) FWP template shortcode? */
public $is_shortcode = false;
/* (boolean) Is a FacetWP refresh? */
public $is_refresh = false;
/* (boolean) Initial load? */
public $is_preload = false;
function __construct() {
$this->process_json();
$this->intercept_request();
}
/**
* application/json requires processing the raw PHP input stream
*/
function process_json() {
$json = file_get_contents( 'php://input' );
if ( 0 === strpos( $json, '{' ) ) {
$post_data = json_decode( $json, true );
$action = $post_data['action'] ?? '';
if ( is_string( $action ) && 0 === strpos( $action, 'facetwp' ) ) {
$_POST = $post_data;
}
}
}
/**
* If AJAX and the template is "wp", return the buffered HTML
* Otherwise, store the GET variables for later use
*/
function intercept_request() {
$action = isset( $_POST['action'] ) ? sanitize_key( $_POST['action'] ) : '';
$valid_actions = [
'facetwp_refresh',
'facetwp_autocomplete_load'
];
$this->is_refresh = ( 'facetwp_refresh' == $action );
$this->is_preload = ! in_array( $action, $valid_actions );
$prefix = FWP()->helper->get_setting( 'prefix' );
$is_css_tpl = isset( $_POST['data']['template'] ) && 'wp' == $_POST['data']['template'];
// Disable the admin bar to prevent JSON issues
if ( $this->is_refresh ) {
add_filter( 'show_admin_bar', '__return_false' );
}
// Pageload
if ( $this->is_preload ) {
$features = [ 'paged', 'per_page', 'sort' ];
$valid_names = wp_list_pluck( FWP()->helper->get_facets(), 'name' );
$valid_names = array_merge( $valid_names, $features );
// Store GET variables
foreach ( $valid_names as $name ) {
if ( isset( $_GET[ $prefix . $name ] ) && '' !== $_GET[ $prefix . $name ] ) {
$new_val = stripslashes_deep( $_GET[ $prefix . $name ] );
$new_val = in_array( $name, $features ) ? $new_val : explode( ',', $new_val );
$this->url_vars[ $name ] = $new_val;
}
}
$this->url_vars = apply_filters( 'facetwp_preload_url_vars', $this->url_vars );
}
// Populate $_GET
else {
$data = stripslashes_deep( $_POST['data'] );
if ( ! empty( $data['http_params']['get'] ) ) {
foreach ( $data['http_params']['get'] as $key => $val ) {
if ( ! isset( $_GET[ $key ] ) ) {
$_GET[ $key ] = $val;
}
}
}
}
if ( $this->is_preload || $is_css_tpl ) {
add_filter( 'posts_pre_query', [ $this, 'maybe_abort_query' ], 10, 2 );
add_action( 'pre_get_posts', [ $this, 'sacrificial_lamb' ], 998 );
add_action( 'pre_get_posts', [ $this, 'update_query_vars' ], 999 );
}
if ( ! $this->is_preload && $is_css_tpl && 'facetwp_autocomplete_load' != $action ) {
add_action( 'shutdown', [ $this, 'inject_template' ], 0 );
ob_start();
}
}
/**
* FacetWP runs the archive query before WP gets the chance.
* This hook prevents the query from running twice, by letting us inject the
* first query's posts (and counts) into the "main" query.
*/
function maybe_abort_query( $posts, $query ) {
$do_abort = apply_filters( 'facetwp_archive_abort_query', true, $query );
$has_query_run = ( ! empty( FWP()->facet->query ) );
if ( $do_abort && $has_query_run && isset( $this->query_vars ) ) {
// New var; any changes to $query will cause is_main_query() to return false
$query_vars = $query->query_vars;
// If paged = 0, set to 1 or the compare will fail
if ( empty( $query_vars['paged'] ) ) {
$query_vars['paged'] = 1;
}
// Only intercept the identical query
if ( $query_vars === $this->query_vars ) {
$posts = FWP()->facet->query->posts;
$query->found_posts = FWP()->facet->query->found_posts;
$query->max_num_pages = FWP()->facet->query->max_num_pages;
}
}
return $posts;
}
/**
* Fixes https://core.trac.wordpress.org/ticket/40393
*/
function sacrificial_lamb( $query ) {
}
/**
* Force FacetWP to use the default WP query
*/
function update_query_vars( $query ) {
if ( isset( $this->query_vars ) // Ran already
|| $this->is_shortcode // Skip shortcode template
|| ( is_admin() && ! wp_doing_ajax() ) // Skip admin
|| ( wp_doing_ajax() && ! $this->is_refresh ) // Skip other ajax
|| ! $this->is_main_query( $query ) // Not the main query
) {
return;
}
// Set the flag
$query->set( 'facetwp', true );
// If "s" is an empty string and no post_type is set, WP sets
// post_type = "any". We want to prevent this except on the search page.
if ( '' == $query->get( 's' ) && ! isset( $_GET['s'] ) ) {
$query->set( 's', null );
}
// Set the initial query vars, needed for render()
$this->query_vars = $query->query_vars;
// Notify
do_action( 'facetwp_found_main_query' );
// Generate the FWP output
$data = ( $this->is_preload ) ? $this->process_preload_data() : $this->process_post_data();
$this->output = FWP()->facet->render( $data );
// Set the updated query vars, needed for maybe_abort_query()
$this->query_vars = FWP()->facet->query->query_vars;
// Set the updated query vars
$force_query = apply_filters( 'facetwp_preload_force_query', false, $query );
if ( ! $this->is_preload || ! empty( $this->url_vars ) || $force_query ) {
$query->query_vars = FWP()->facet->query_args;
}
if ( 'product_query' == $query->get( 'wc_query' ) ) {
wc_set_loop_prop( 'total', FWP()->facet->pager_args['total_rows'] );
wc_set_loop_prop( 'per_page', FWP()->facet->pager_args['per_page'] );
wc_set_loop_prop( 'total_pages', FWP()->facet->pager_args['total_pages'] );
wc_set_loop_prop( 'current_page', FWP()->facet->pager_args['page'] );
}
}
/**
* Is this the main query?
*/
function is_main_query( $query ) {
if ( 'yes' == FWP()->helper->get_setting( 'strict_query_detection', 'no' ) ) {
$is_main_query = ( $query->is_main_query() );
}
else {
$is_main_query = ( $query->is_main_query() || $query->is_archive );
}
$is_main_query = ( $query->is_singular || $query->is_feed ) ? false : $is_main_query;
$is_main_query = ( $query->get( 'suppress_filters', false ) ) ? false : $is_main_query; // skip get_posts()
$is_main_query = ( '' !== $query->get( 'facetwp' ) ) ? (bool) $query->get( 'facetwp' ) : $is_main_query; // flag
return apply_filters( 'facetwp_is_main_query', $is_main_query, $query );
}
/**
* Process the AJAX $_POST data
* This gets passed into FWP()->facet->render()
*/
function process_post_data() {
$data = stripslashes_deep( $_POST['data'] );
$facets = $data['facets'];
$extras = $data['extras'] ?? [];
$frozen_facets = $data['frozen_facets'] ?? [];
$params = [
'facets' => [],
'template' => $data['template'],
'frozen_facets' => $frozen_facets,
'http_params' => $data['http_params'],
'extras' => $extras,
'soft_refresh' => (int) $data['soft_refresh'],
'is_bfcache' => (int) $data['is_bfcache'],
'first_load' => (int) $data['first_load'], // skip the template?
'paged' => (int) $data['paged'],
];
foreach ( $facets as $facet_name => $selected_values ) {
$params['facets'][] = [
'facet_name' => $facet_name,
'selected_values' => $selected_values,
];
}
return $params;
}
/**
* On initial pageload, preload the data
*
* This gets called twice; once in the template shortcode (to grab only the template)
* and again in FWP()->display->front_scripts() to grab everything else.
*
* Two calls are needed for timing purposes; the template shortcode often renders
* before some or all of the other FacetWP-related shortcodes.
*/
function process_preload_data( $template_name = false, $overrides = [] ) {
if ( false === $template_name ) {
$template_name = $this->template_name ?? 'wp';
}
$this->template_name = $template_name;
// Is this a template shortcode?
$this->is_shortcode = ( 'wp' != $template_name );
$params = [
'facets' => [],
'template' => $template_name,
'http_params' => [
'get' => $_GET,
'uri' => FWP()->helper->get_uri(),
'url_vars' => $this->url_vars,
],
'frozen_facets' => [],
'soft_refresh' => 1, // skip the facets
'is_preload' => 1,
'is_bfcache' => 0,
'first_load' => 0, // load the template
'extras' => [],
'paged' => 1,
];
// Support "/page/X/" on preload
if ( ! empty( $this->query_vars['paged'] ) ) {
$params['paged'] = (int) $this->query_vars['paged'];
}
foreach ( $this->url_vars as $key => $val ) {
if ( 'paged' == $key ) {
$params['paged'] = $val;
}
elseif ( 'per_page' == $key || 'sort' == $key ) {
$params['extras'][ $key ] = $val;
}
else {
$params['facets'][] = [
'facet_name' => $key,
'selected_values' => $val,
];
}
}
// Override the defaults
$params = array_merge( $params, $overrides );
return $params;
}
/**
* This gets called from FWP()->display->front_scripts(), when we finally
* know which shortcodes are on the page.
*
* Since we already got the template HTML on the first process_preload_data() call,
* this time we're grabbing everything but the template.
*
* The return value of this method gets passed into the 2nd argument of
* process_preload_data().
*/
function process_preload_overrides( $items ) {
$overrides = [];
$url_vars = FWP()->request->url_vars;
foreach ( $items['facets'] as $name ) {
$overrides['facets'][] = [
'facet_name' => $name,
'selected_values' => $url_vars[ $name ] ?? [],
];
}
if ( isset( $items['extras']['counts'] ) ) {
$overrides['extras']['counts'] = true;
}
if ( isset( $items['extras']['pager'] ) ) {
$overrides['extras']['pager'] = true;
}
if ( isset( $items['extras']['per_page'] ) ) {
$overrides['extras']['per_page'] = $url_vars['per_page'] ?? 'default';
}
if ( isset( $items['extras']['sort'] ) ) {
$overrides['extras']['sort'] = $url_vars['sort'] ?? 'default';
}
$overrides['soft_refresh'] = 0; // load the facets
$overrides['first_load'] = 1; // skip the template
return $overrides;
}
/**
* Inject the page HTML into the JSON response
* We'll cherry-pick the content from the HTML using front.js
*/
function inject_template() {
$html = ob_get_clean();
// Throw an error
if ( empty( $this->output['settings'] ) ) {
$html = __( 'FacetWP was unable to auto-detect the post listing', 'fwp' );
}
// Grab the <body> contents
else {
preg_match( "/<body(.*?)>(.*?)<\/body>/s", $html, $matches );
if ( ! empty( $matches ) ) {
$html = trim( $matches[2] );
}
}
$this->output['template'] = $html;
do_action( 'facetwp_inject_template', $this->output );
wp_send_json( $this->output );
}
}

View File

@@ -0,0 +1,646 @@
<?php
class FacetWP_Settings
{
/**
* Get the field settings array
* @since 3.0.0
*/
function get_registered_settings() {
$defaults = [
'general' => [
'label' => __( 'General', 'fwp' ),
'fields' => [
'license_key' => [
'label' => __( 'License key', 'fwp' ),
'html' => $this->get_setting_html( 'license_key' )
],
'gmaps_api_key' => [
'label' => __( 'Google Maps API key', 'fwp' ),
'html' => $this->get_setting_html( 'gmaps_api_key' )
],
'separators' => [
'label' => __( 'Separators', 'fwp' ),
'notes' => 'Enter the thousands and decimal separators, respectively',
'html' => $this->get_setting_html( 'separators' )
],
'prefix' => [
'label' => __( 'URL prefix', 'fwp' ),
'html' => $this->get_setting_html( 'prefix', 'dropdown', [
'choices' => [ 'fwp_' => 'fwp_', '_' => '_' ]
])
],
'load_jquery' => [
'label' => __( 'Load jQuery', 'fwp' ),
'notes' => 'FacetWP no longer requires jQuery, but enable if needed',
'html' => $this->get_setting_html( 'load_jquery', 'toggle', [
'true_value' => 'yes',
'false_value' => 'no'
])
],
'load_a11y' => [
'label' => __( 'Load a11y support', 'fwp' ),
'notes' => 'Improved accessibility for users with disabilities',
'html' => $this->get_setting_html( 'load_a11y', 'toggle', [
'true_value' => 'yes',
'false_value' => 'no'
])
],
'strict_query_detection' => [
'label' => __( 'Strict query detection', 'fwp' ),
'notes' => 'Enable if FacetWP auto-detects the wrong archive query',
'html' => $this->get_setting_html( 'strict_query_detection', 'toggle', [
'true_value' => 'yes',
'false_value' => 'no'
])
],
'debug_mode' => [
'label' => __( 'Debug mode', 'fwp' ),
'notes' => 'After enabling, type "FWP.settings.debug" into the browser console on your front-end facet page',
'html' => $this->get_setting_html( 'debug_mode', 'toggle', [
'true_value' => 'on',
'false_value' => 'off'
])
]
]
],
'woocommerce' => [
'label' => __( 'WooCommerce', 'fwp' ),
'fields' => [
'wc_enable_variations' => [
'label' => __( 'Support product variations?', 'fwp' ),
'notes' => __( 'Enable if your store uses variable products.', 'fwp' ),
'html' => $this->get_setting_html( 'wc_enable_variations', 'toggle' )
],
'wc_index_all' => [
'label' => __( 'Index out-of-stock products?', 'fwp' ),
'notes' => __( 'Show facet choices for out-of-stock products?', 'fwp' ),
'html' => $this->get_setting_html( 'wc_index_all', 'toggle' )
]
]
],
'backup' => [
'label' => __( 'Import / Export', 'fwp' ),
'fields' => [
'export' => [
'label' => __( 'Export', 'fwp' ),
'html' => $this->get_setting_html( 'export' )
],
'import' => [
'label' => __( 'Import', 'fwp' ),
'html' => $this->get_setting_html( 'import' )
]
]
]
];
if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
unset( $defaults['woocommerce'] );
}
if ( '_' == FWP()->helper->settings['settings']['prefix'] ) {
unset( $defaults['general']['fields']['prefix'] );
}
return apply_filters( 'facetwp_settings_admin', $defaults, $this );
}
/**
* All facet admin fields
* @since 3.9
*/
function get_registered_facet_fields() {
$settings = [
'label_any' => [
'label' => __( 'Default label', 'fwp' ),
'notes' => 'Customize the "Any" label',
'default' => __( 'Any', 'fwp' )
],
'placeholder' => [
'label' => __( 'Placeholder text', 'fwp' )
],
'parent_term' => [
'label' => __( 'Parent term', 'fwp' ),
'notes' => 'To show only child terms, enter the parent <a href="https://facetwp.com/how-to-find-a-wordpress-terms-id/" target="_blank">term ID</a>. Otherwise, leave blank.',
'show' => "facet.source.substr(0, 3) == 'tax'"
],
'hierarchical' => [
'type' => 'toggle',
'label' => __( 'Hierarchical', 'fwp' ),
'notes' => 'Is this a hierarchical taxonomy?',
'show' => "facet.source.substr(0, 3) == 'tax'"
],
'show_expanded' => [
'type' => 'toggle',
'label' => __( 'Show expanded', 'fwp' ),
'notes' => 'Should child terms be visible by default?',
'show' => "facet.hierarchical == 'yes'"
],
'multiple' => [
'type' => 'toggle',
'label' => __( 'Multi-select', 'fwp' ),
'notes' => 'Allow multiple selections?'
],
'ghosts' => [
'type' => 'alias',
'items' => [
'ghosts' => [
'type' => 'toggle',
'label' => __( 'Show ghosts', 'fwp' ),
'notes' => 'Show choices that would return zero results?'
],
'preserve_ghosts' => [
'type' => 'toggle',
'label' => __( 'Preserve ghost order', 'fwp' ),
'notes' => 'Keep ghost choices in the same order?',
'show' => "facet.ghosts == 'yes'"
]
]
],
'modifiers' => [
'type' => 'alias',
'items' => [
'modifier_type' => [
'type' => 'select',
'label' => __( 'Value modifiers', 'fwp' ),
'notes' => 'Include or exclude certain values?',
'choices' => [
'off' => __( 'Off', 'fwp' ),
'exclude' => __( 'Exclude these values', 'fwp' ),
'include' => __( 'Show only these values', 'fwp' )
]
],
'modifier_values' => [
'type' => 'textarea',
'label' => '',
'placeholder' => 'Add one value per line',
'show' => "facet.modifier_type != 'off'"
]
]
],
'operator' => [
'type' => 'select',
'label' => __( 'Facet logic', 'fwp' ),
'notes' => 'How should multiple selections affect the results?',
'choices' => [
'and' => __( 'AND (match all)', 'fwp' ),
'or' => __( 'OR (match any)', 'fwp' )
]
],
'orderby' => [
'type' => 'select',
'label' => __( 'Sort by', 'fwp' ),
'choices' => [
'count' => __( 'Highest count', 'fwp' ),
'display_value' => __( 'Display value', 'fwp' ),
'raw_value' => __( 'Raw value', 'fwp' ),
'term_order' => __( 'Term order', 'fwp' )
]
],
'count' => [
'label' => __( 'Count', 'fwp' ),
'notes' => 'The maximum number of choices to show (-1 for no limit)',
'default' => 10
],
'soft_limit' => [
'label' => __( 'Soft limit', 'fwp' ),
'notes' => 'Show a toggle link after this many choices',
'default' => 5,
'show' => "facet.hierarchical != 'yes'"
],
'source_other' => [
'label' => __( 'Other data source', 'fwp' ),
'notes' => 'Use a separate value for the upper limit?',
'html' => '<data-sources :facet="facet" setting-name="source_other"></data-sources>'
],
'compare_type' => [
'type' => 'select',
'label' => __( 'Compare type', 'fwp' ),
'notes' => "<strong>Basic</strong> - entered range surrounds the post's range<br /><strong>Enclose</strong> - entered range is fully inside the post's range<br /><strong>Intersect</strong> - entered range overlaps the post's range<br /><br />When in doubt, choose <strong>Basic</strong>",
'choices' => [
'' => __( 'Basic', 'fwp' ),
'enclose' => __( 'Enclose', 'fwp' ),
'intersect' => __( 'Intersect', 'fwp' )
]
],
'ui_type' => [
'label' => __( 'UI type', 'fwp' ),
'html' => '<ui-type :facet="facet"></ui-type>'
],
'reset_text' => [
'label' => __( 'Reset text', 'fwp' ),
'default' => 'Reset'
]
];
foreach ( FWP()->helper->facet_types as $name => $obj ) {
if ( method_exists( $obj, 'register_fields' ) ) {
$settings = array_merge( $settings, $obj->register_fields() );
}
}
return $settings;
}
/**
* Return HTML for a single facet field (supports aliases)
* @since 3.9
*/
function get_facet_field_html( $name ) {
ob_start();
$fields = FWP()->settings->get_registered_facet_fields();
if ( isset( $fields[ $name ] ) ) {
$field = $fields[ $name ];
if ( isset( $field['type'] ) && 'alias' == $field['type'] ) {
foreach ( $field['items'] as $k => $v ) {
$v['name'] = $k;
$this->render_facet_field( $v );
}
}
else {
$field['name'] = $name;
$this->render_facet_field( $field );
}
}
return ob_get_clean();
}
/**
* Render a facet field
* @since 3.9
*/
function render_facet_field( $field ) {
$name = str_replace( '_', '-', $field['name'] );
$type = $field['type'] ?? 'text';
$placeholder = $field['placeholder'] ?? '';
$show = isset( $field['show'] ) ? ' v-show="' . $field['show'] . '"' : '';
$default = isset( $field['default'] ) ? ' value="' . $field['default'] . '"' : '';
$label = empty( $field['label'] ) ? '' : $field['label'];
if ( isset( $field['notes'] ) ) {
$label = '<div class="facetwp-tooltip">' . $label . '<div class="facetwp-tooltip-content">' . $field['notes'] . '</div></div>';
}
ob_start();
if ( isset( $field['html'] ) ) {
echo $field['html'];
}
elseif ( 'text' == $type ) {
?>
<input type="text" class="facet-<?php echo $name; ?>" placeholder="<?php echo $placeholder; ?>"<?php echo $default; ?> />
<?php
}
elseif ( 'textarea' == $type ) {
?>
<textarea class="facet-<?php echo $name; ?>" placeholder="<?php echo $placeholder; ?>"></textarea>
<?php
}
elseif ( 'toggle' == $type ) {
?>
<label class="facetwp-switch">
<input type="checkbox" class="facet-<?php echo $name; ?>" true-value="yes" false-value="no" />
<span class="facetwp-slider"></span>
</label>
<?php
}
elseif ( 'select' == $type ) {
?>
<select class="facet-<?php echo $name; ?>">
<?php foreach ( $field['choices'] as $k => $v ) : ?>
<option value="<?php echo $k; ?>"><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
<?php
}
$html = ob_get_clean();
?>
<div class="facetwp-row"<?php echo $show; ?>>
<div><?php echo $label; ?></div>
<div><?php echo $html; ?></div>
</div>
<?php
}
/**
* Return HTML for a setting field
* @since 3.0.0
*/
function get_setting_html( $setting_name, $field_type = 'text', $atts = [] ) {
ob_start();
if ( 'license_key' == $setting_name ) : ?>
<input type="text" class="facetwp-license" style="width:360px" value="<?php echo FWP()->helper->get_license_key(); ?>"<?php echo defined( 'FACETWP_LICENSE_KEY' ) ? ' disabled' : ''; ?> />
<div @click="activate" class="btn-normal"><?php _e( 'Activate', 'fwp' ); ?></div>
<div class="facetwp-activation-status field-notes"><?php echo $this->get_activation_status(); ?></div>
<?php elseif ( 'gmaps_api_key' == $setting_name ) : ?>
<input type="text" v-model="app.settings.gmaps_api_key" style="width:360px" />
<a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank"><?php _e( 'Get an API key', 'fwp' ); ?></a>
<?php elseif ( 'separators' == $setting_name ) : ?>
Thousands:
<input type="text" v-model="app.settings.thousands_separator" style="width:30px" /> &nbsp;
Decimal:
<input type="text" v-model="app.settings.decimal_separator" style="width:30px" />
<?php elseif ( 'export' == $setting_name ) : ?>
<select class="export-items" multiple="multiple">
<?php foreach ( $this->get_export_choices() as $val => $label ) : ?>
<option value="<?php echo $val; ?>"><?php echo $label; ?></option>
<?php endforeach; ?>
</select>
<div class="btn-normal export-submit">
<?php _e( 'Export', 'fwp' ); ?>
</div>
<?php elseif ( 'import' == $setting_name ) : ?>
<div><textarea class="import-code" placeholder="<?php _e( 'Paste the import code here', 'fwp' ); ?>"></textarea></div>
<div><input type="checkbox" class="import-overwrite" /> <?php _e( 'Overwrite existing items?', 'fwp' ); ?></div>
<div style="margin-top:5px">
<div class="btn-normal import-submit"><?php _e( 'Import', 'fwp' ); ?></div>
</div>
<?php elseif ( 'dropdown' == $field_type ) : ?>
<select class="facetwp-setting" v-model="app.settings.<?php echo $setting_name; ?>">
<?php foreach ( $atts['choices'] as $val => $label ) : ?>
<option value="<?php echo $val; ?>"><?php echo $label; ?></option>
<?php endforeach; ?>
</select>
<?php elseif ( 'toggle' == $field_type ) : ?>
<?php
$true_value = $atts['true_value'] ?? 'yes';
$false_value = $atts['false_value'] ?? 'no';
?>
<label class="facetwp-switch">
<input
type="checkbox"
v-model="app.settings.<?php echo $setting_name; ?>"
true-value="<?php echo $true_value; ?>"
false-value="<?php echo $false_value; ?>"
/>
<span class="facetwp-slider"></span>
</label>
<?php endif;
return ob_get_clean();
}
/**
* Get an array of all facets and templates
* @since 3.0.0
*/
function get_export_choices() {
$export = [];
$settings = FWP()->helper->settings;
foreach ( $settings['facets'] as $facet ) {
$export['facet-' . $facet['name']] = 'Facet - ' . $facet['label'];
}
foreach ( $settings['templates'] as $template ) {
$export['template-' . $template['name']] = 'Template - '. $template['label'];
}
return $export;
}
/**
* Get the activation status
* @since 3.0.0
*/
function get_activation_status() {
$message = __( 'Not yet activated', 'fwp' );
$status = FWP()->helper->get_license_meta( 'status' );
if ( false !== $status ) {
if ( 'success' == $status ) {
$expiration = FWP()->helper->get_license_meta( 'expiration' );
$expiration = date( 'M j, Y', strtotime( $expiration ) );
$message = __( 'Valid until', 'fwp' ) . ' ' . $expiration;
}
else {
$message = FWP()->helper->get_license_meta( 'message' );
}
}
return $message;
}
/**
* Load i18n admin strings
* @since 3.2.0
*/
function get_i18n_strings() {
return [
'Number of grid columns' => __( 'Number of grid columns', 'fwp' ),
'Spacing between results' => __( 'Spacing between results', 'fwp' ),
'No results text' => __( 'No results text', 'fwp' ),
'Text style' => __( 'Text style', 'fwp' ),
'Text color' => __( 'Text color', 'fwp' ),
'Font size' => __( 'Font size', 'fwp' ),
'Background color' => __( 'Background color', 'fwp' ),
'Border' => __( 'Border', 'fwp' ),
'Border style' => __( 'Border style', 'fwp' ),
'None' => __( 'None', 'fwp' ),
'Solid' => __( 'Solid', 'fwp' ),
'Dashed' => __( 'Dashed', 'fwp' ),
'Dotted' => __( 'Dotted', 'fwp' ),
'Double' => __( 'Double', 'fwp' ),
'Border color' => __( 'Border color', 'fwp' ),
'Border width' => __( 'Border width', 'fwp' ),
'Button text' => __( 'Button text', 'fwp' ),
'Button text color' => __( 'Button text color', 'fwp' ),
'Button padding' => __( 'Button padding', 'fwp' ),
'Separator' => __( 'Separator', 'fwp' ),
'Custom CSS' => __( 'Custom CSS', 'fwp' ),
'Column widths' => __( 'Column widths', 'fwp' ),
'Content' => __( 'Content', 'fwp' ),
'Image size' => __( 'Image size', 'fwp' ),
'Author field' => __( 'Author field', 'fwp' ),
'Display name' => __( 'Display name', 'fwp' ),
'User login' => __( 'User login', 'fwp' ),
'User ID' => __( 'User ID', 'fwp' ),
'Field type' => __( 'Field type', 'fwp' ),
'Text' => __( 'Text', 'fwp' ),
'Date' => __( 'Date', 'fwp' ),
'Number' => __( 'Number', 'fwp' ),
'Date format' => __( 'Date format', 'fwp' ),
'Input format' => __( 'Input format', 'fwp' ),
'Number format' => __( 'Number format', 'fwp' ),
'Link' => __( 'Link', 'fwp' ),
'Link type' => __( 'Link type', 'fwp' ),
'Post URL' => __( 'Post URL', 'fwp' ),
'Custom URL' => __( 'Custom URL', 'fwp' ),
'Open in new tab?' => __( 'Open in new tab?', 'fwp' ),
'Prefix' => __( 'Prefix', 'fwp' ),
'Suffix' => __( 'Suffix', 'fwp' ),
'Hide item?' => __( 'Hide item?', 'fwp' ),
'Padding' => __( 'Padding', 'fwp' ),
'CSS class' => __( 'CSS class', 'fwp' ),
'Button Border' => __( 'Button border', 'fwp' ),
'Term URL' => __( 'Term URL', 'fwp' ),
'Fetch' => __( 'Fetch', 'fwp' ),
'All post types' => __( 'All post types', 'fwp' ),
'and show' => __( 'and show', 'fwp' ),
'per page' => __( 'per page', 'fwp' ),
'Sort by' => __( 'Sort by', 'fwp' ),
'Posts' => __( 'Posts', 'fwp' ),
'Post Title' => __( 'Post Title', 'fwp' ),
'Post Name' => __( 'Post Name', 'fwp' ),
'Post Type' => __( 'Post Type', 'fwp' ),
'Post Date' => __( 'Post Date', 'fwp' ),
'Post Modified' => __( 'Post Modified', 'fwp' ),
'Comment Count' => __( 'Comment Count', 'fwp' ),
'Menu Order' => __( 'Menu Order', 'fwp' ),
'Custom Fields' => __( 'Custom Fields', 'fwp' ),
'Narrow results by' => __( 'Narrow results by', 'fwp' ),
'Hit Enter' => __( 'Hit Enter', 'fwp' ),
'Add query sort' => __( 'Add query sort', 'fwp' ),
'Add query filter' => __( 'Add query filter', 'fwp' ),
'Clear' => __( 'Clear', 'fwp' ),
'Enter term slugs' => __( 'Enter term slugs', 'fwp' ),
'Enter values' => __( 'Enter values', 'fwp' ),
'Layout' => __( 'Layout', 'fwp' ),
'Content' => __( 'Content', 'fwp' ),
'Style' => __( 'Style', 'fwp' ),
'Row' => __( 'Row', 'fwp' ),
'Column' => __( 'Column', 'fwp' ),
'Start typing' => __( 'Start typing', 'fwp' ),
'Label' => __( 'Label', 'fwp' ),
'Unique name' => __( 'Unique name', 'fwp' ),
'Facet type' => __( 'Facet type', 'fwp' ),
'Copy shortcode' => __( 'Copy shortcode', 'fwp' ),
'Data source' => __( 'Data source', 'fwp' ),
'Switch to advanced mode' => __( 'Switch to advanced mode', 'fwp' ),
'Switch to visual mode' => __( 'Switch to visual mode', 'fwp' ),
'Display' => __( 'Display', 'fwp' ),
'Query' => __( 'Query', 'fwp' ),
'Help' => __( 'Help', 'fwp' ),
'Display Code' => __( 'Display Code', 'fwp' ),
'Query Arguments' => __( 'Query Arguments', 'fwp' ),
'Saving' => __( 'Saving', 'fwp' ),
'Indexing' => __( 'Indexing', 'fwp' ),
'The index table is empty' => __( 'The index table is empty', 'fwp' ),
'Indexing complete' => __( 'Indexing complete', 'fwp' ),
'Looking' => __( 'Looking', 'fwp' ),
'Purging' => __( 'Purging', 'fwp' ),
'Copied!' => __( 'Copied!', 'fwp' ),
'Press CTRL+C to copy' => __( 'Press CTRL+C to copy', 'fwp' ),
'Activating' => __( 'Activating', 'fwp' ),
'Re-index' => __( 'Re-index', 'fwp' ),
'Stop indexer' => __( 'Stop indexer', 'fwp' ),
'Loading' => __( 'Loading', 'fwp' ),
'Importing' => __( 'Importing', 'fwp' ),
'Convert to query args' => __( 'Convert to query args', 'fwp' )
];
}
/**
* Get available image sizes
* @since 3.2.7
*/
function get_image_sizes() {
global $_wp_additional_image_sizes;
$sizes = [];
$default_sizes = [ 'thumbnail', 'medium', 'medium_large', 'large', 'full' ];
foreach ( get_intermediate_image_sizes() as $size ) {
if ( in_array( $size, $default_sizes ) ) {
$sizes[ $size ]['width'] = (int) get_option( "{$size}_size_w" );
$sizes[ $size ]['height'] = (int) get_option( "{$size}_size_h" );
}
elseif ( isset( $_wp_additional_image_sizes[ $size ] ) ) {
$sizes[ $size ] = $_wp_additional_image_sizes[ $size ];
}
}
return $sizes;
}
/**
* Return an array of formatted image sizes
* @since 3.2.7
*/
function get_image_size_labels() {
$labels = [];
$sizes = $this->get_image_sizes();
foreach ( $sizes as $size => $data ) {
$height = ( 0 === $data['height'] ) ? 'w' : 'x' . $data['height'];
$label = $size . ' (' . $data['width'] . $height . ')';
$labels[ $size ] = $label;
}
$labels['full'] = __( 'full', 'fwp' );
return $labels;
}
/**
* Create SVG images (based on Font Awesome)
* @license https://fontawesome.com/license/free CC BY 4.0
* @since 3.6.5
*/
function get_svg() {
$output = [];
$icons = [
// [viewBox width, viewBox height, icon width, svg data]
"align-center" => [448, 512, 14, "M432 160H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0 256H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM108.1 96h231.81A12.09 12.09 0 0 0 352 83.9V44.09A12.09 12.09 0 0 0 339.91 32H108.1A12.09 12.09 0 0 0 96 44.09V83.9A12.1 12.1 0 0 0 108.1 96zm231.81 256A12.09 12.09 0 0 0 352 339.9v-39.81A12.09 12.09 0 0 0 339.91 288H108.1A12.09 12.09 0 0 0 96 300.09v39.81a12.1 12.1 0 0 0 12.1 12.1z"],
"align-left" => [448, 512, 14, "M12.83 352h262.34A12.82 12.82 0 0 0 288 339.17v-38.34A12.82 12.82 0 0 0 275.17 288H12.83A12.82 12.82 0 0 0 0 300.83v38.34A12.82 12.82 0 0 0 12.83 352zm0-256h262.34A12.82 12.82 0 0 0 288 83.17V44.83A12.82 12.82 0 0 0 275.17 32H12.83A12.82 12.82 0 0 0 0 44.83v38.34A12.82 12.82 0 0 0 12.83 96zM432 160H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0 256H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"],
"align-right" => [448, 512, 14, "M16 224h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm416 192H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm3.17-384H172.83A12.82 12.82 0 0 0 160 44.83v38.34A12.82 12.82 0 0 0 172.83 96h262.34A12.82 12.82 0 0 0 448 83.17V44.83A12.82 12.82 0 0 0 435.17 32zm0 256H172.83A12.82 12.82 0 0 0 160 300.83v38.34A12.82 12.82 0 0 0 172.83 352h262.34A12.82 12.82 0 0 0 448 339.17v-38.34A12.82 12.82 0 0 0 435.17 288z"],
"arrow-circle-up" => [512, 512, 16, "M8 256C8 119 119 8 256 8s248 111 248 248-111 248-248 248S8 393 8 256zm143.6 28.9l72.4-75.5V392c0 13.3 10.7 24 24 24h16c13.3 0 24-10.7 24-24V209.4l72.4 75.5c9.3 9.7 24.8 9.9 34.3.4l10.9-11c9.4-9.4 9.4-24.6 0-33.9L273 107.7c-9.4-9.4-24.6-9.4-33.9 0L106.3 240.4c-9.4 9.4-9.4 24.6 0 33.9l10.9 11c9.6 9.5 25.1 9.3 34.4-.4z"],
"bold" => [384, 512, 12, "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z"],
"caret-down" => [320, 512, 10, "M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"],
"cog" => [512, 512, 16, "M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"],
"columns" => [512, 512, 16, "M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM224 416H64V160h160v256zm224 0H288V160h160v256z"],
"eye-slash" => [640, 512, 20, "M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"],
"italic" => [320, 512, 10, "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z"],
"lock" => [448, 512, 14, "M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z"],
"lock-open" => [576, 512, 18, "M423.5 0C339.5.3 272 69.5 272 153.5V224H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48h-48v-71.1c0-39.6 31.7-72.5 71.3-72.9 40-.4 72.7 32.1 72.7 72v80c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24v-80C576 68 507.5-.3 423.5 0z"],
"minus-circle" => [512, 512, 16, "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zM124 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H124z"],
"plus" => [448, 512, 14, "M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"],
"plus-circle" => [512, 512, 16, "M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"],
"times" => [352, 512, 11, "M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"]
];
foreach ( $icons as $name => $attr ) {
$svg = '<svg aria-hidden="true" focusable="false" class="svg-inline--fa fa-{name} fa-w-{faw}" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}"><path fill="currentColor" d="{d}"></path></svg>';
$search = [ '{name}', '{w}', '{h}', '{faw}', '{d}' ];
$replace = [ $name, $attr[0], $attr[1], $attr[2], $attr[3] ];
$output[ $name ] = str_replace( $search, $replace, $svg );
}
return $output;
}
}

View File

@@ -0,0 +1,183 @@
<?php
class FacetWP_Updater
{
function __construct() {
add_filter( 'plugins_api', [ $this, 'plugins_api' ], 10, 3 );
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_update' ] );
add_action( 'in_plugin_update_message-' . FACETWP_BASENAME, [ $this, 'in_plugin_update_message' ], 10, 2 );
}
/**
* Get info for FacetWP and its add-ons
*/
function get_plugins_to_check() {
$output = [];
$plugins = get_plugins();
if ( is_multisite() ) {
$active_plugins = get_site_option( 'active_sitewide_plugins', [] );
$active_plugins = array_flip( $active_plugins ); // [plugin] => path
}
else {
$active_plugins = get_option( 'active_plugins', [] );
}
foreach ( $active_plugins as $plugin_path ) {
if ( is_multisite() ) {
$plugin_path = str_replace( WP_PLUGIN_DIR, '', $plugin_path );
}
if ( isset( $plugins[ $plugin_path ] ) ) {
$info = $plugins[ $plugin_path ];
$slug = trim( dirname( $plugin_path ), '/' );
// only intercept FacetWP and its add-ons
$is_valid = in_array( $slug, [ 'facetwp', 'user-post-type' ] );
$is_valid = ( 0 === strpos( $slug, 'facetwp-' ) ) ? true : $is_valid;
if ( $is_valid ) {
$output[ $slug ] = [
'name' => $info['Name'],
'version' => $info['Version'],
'description' => $info['Description'],
'plugin_path' => $plugin_path,
];
}
}
}
return $output;
}
/**
* Handle the "View Details" popup
*
* $args->slug = "facetwp-flyout"
* plugin_path = "facetwp-flyout/facetwp-flyout.php"
*/
function plugins_api( $result, $action, $args ) {
if ( 'plugin_information' == $action ) {
$slug = $args->slug;
$to_check = $this->get_plugins_to_check();
$response = get_option( 'facetwp_updater_response', '' );
$response = json_decode( $response, true );
if ( isset( $to_check[ $slug ] ) && isset( $response[ $slug ] ) ) {
$local_data = $to_check[ $slug ];
$remote_data = $response[ $slug ];
return (object) [
'name' => $local_data['name'],
'slug' => $local_data['plugin_path'],
'version' => $remote_data['version'],
'last_updated' => $remote_data['last_updated'],
'download_link' => $remote_data['package'],
'sections' => [
'description' => $local_data['description'],
'changelog' => $remote_data['sections']['changelog']
],
'homepage' => 'https://facetwp.com/',
'rating' => 100,
'num_ratings' => 1
];
}
}
return $result;
}
/**
* Grab (and cache) plugin update data
*/
function check_update( $transient ) {
if ( empty( $transient->checked ) ) {
return $transient;
}
$now = strtotime( 'now' );
$response = get_option( 'facetwp_updater_response', '' );
$ts = (int) get_option( 'facetwp_updater_last_checked' );
$plugins = $this->get_plugins_to_check();
if ( empty( $response ) || $ts + 14400 < $now ) {
$request = wp_remote_post( 'https://api.facetwp.com', [
'body' => [
'action' => 'check_plugins',
'slugs' => array_keys( $plugins ),
'license' => FWP()->helper->get_license_key(),
'host' => FWP()->helper->get_http_host(),
'wp_v' => get_bloginfo( 'version' ),
'fwp_v' => FACETWP_VERSION,
'php_v' => phpversion(),
]
] );
if ( ! is_wp_error( $request ) || 200 == wp_remote_retrieve_response_code( $request ) ) {
$body = json_decode( $request['body'], true );
$activation = json_encode( $body['activation'] );
$response = json_encode( $body['slugs'] );
}
update_option( 'facetwp_activation', $activation );
update_option( 'facetwp_updater_response', $response, 'no' );
update_option( 'facetwp_updater_last_checked', $now, 'no' );
}
if ( ! empty( $response ) ) {
$response = json_decode( $response, true );
foreach ( $response as $slug => $info ) {
if ( isset( $plugins[ $slug ] ) ) {
$plugin_path = $plugins[ $slug ]['plugin_path'];
$response_obj = (object) [
'slug' => $slug,
'plugin' => $plugin_path,
'new_version' => $info['version'],
'package' => $info['package'],
'requires' => $info['requires'],
'tested' => $info['tested']
];
if ( version_compare( $plugins[ $slug ]['version'], $info['version'], '<' ) ) {
$transient->response[ $plugin_path ] = $response_obj;
}
else {
$transient->no_update[ $plugin_path ] = $response_obj;
}
}
}
}
return $transient;
}
/**
* Display a license reminder on the plugin list screen
*/
function in_plugin_update_message( $plugin_data, $response ) {
if ( ! FWP()->helper->is_license_active() ) {
$price_id = (int) FWP()->helper->get_license_meta( 'price_id' );
$license = FWP()->helper->get_license_key();
if ( 0 < $price_id ) {
echo '<br />' . sprintf(
__( 'Please <a href="%s" target="_blank">renew your license</a> for automatic updates.', 'fwp' ),
esc_url( "https://facetwp.com/checkout/?edd_action=add_to_cart&download_id=24&edd_options[price_id]=$price_id&discount=$license" )
);
}
else {
echo '<br />' . __( 'Please activate your FacetWP license for automatic updates.', 'fwp' );
}
}
}
}
new FacetWP_Updater();

View File

@@ -0,0 +1,106 @@
<?php
class FacetWP_Upgrade
{
function __construct() {
$this->version = FACETWP_VERSION;
$this->last_version = get_option( 'facetwp_version' );
if ( version_compare( $this->last_version, $this->version, '<' ) ) {
if ( version_compare( $this->last_version, '0.1.0', '<' ) ) {
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
$this->clean_install();
}
else {
$this->run_upgrade();
}
update_option( 'facetwp_version', $this->version );
}
}
private function clean_install() {
global $wpdb;
$int = apply_filters( 'facetwp_use_bigint', false ) ? 'BIGINT' : 'INT';
$charset_collate = $wpdb->get_charset_collate();
$sql = "
CREATE TABLE IF NOT EXISTS {$wpdb->prefix}facetwp_index (
id BIGINT unsigned not null auto_increment,
post_id $int unsigned,
facet_name VARCHAR(50),
facet_value VARCHAR(50),
facet_display_value VARCHAR(200),
term_id $int unsigned default '0',
parent_id $int unsigned default '0',
depth INT unsigned default '0',
variation_id $int unsigned default '0',
PRIMARY KEY (id),
INDEX post_id_idx (post_id),
INDEX facet_name_idx (facet_name),
INDEX facet_name_value_idx (facet_name, facet_value)
) $charset_collate";
dbDelta( $sql );
// Add default settings
$settings = file_get_contents( FACETWP_DIR . '/assets/js/src/sample.json' );
add_option( 'facetwp_settings', $settings );
}
private function run_upgrade() {
global $wpdb;
$table = sanitize_key( $wpdb->prefix . 'facetwp_index' );
if ( version_compare( $this->last_version, '3.1.0', '<' ) ) {
$wpdb->query( "ALTER TABLE $table MODIFY facet_name VARCHAR(50)" );
$wpdb->query( "ALTER TABLE $table MODIFY facet_value VARCHAR(50)" );
$wpdb->query( "ALTER TABLE $table MODIFY facet_display_value VARCHAR(200)" );
$wpdb->query( "CREATE INDEX facet_name_value_idx ON $table (facet_name, facet_value)" );
}
if ( version_compare( $this->last_version, '3.3.2', '<' ) ) {
$wpdb->query( "CREATE INDEX post_id_idx ON $table (post_id)" );
$wpdb->query( "DROP INDEX facet_source_idx ON $table" );
$wpdb->query( "ALTER TABLE $table DROP COLUMN facet_source" );
}
if ( version_compare( $this->last_version, '3.3.3', '<' ) ) {
if ( function_exists( 'SWP' ) ) {
$engines = array_keys( SWP()->settings['engines'] );
$settings = get_option( 'facetwp_settings' );
$settings = json_decode( $settings, true );
foreach ( $settings['facets'] as $key => $facet ) {
if ( 'search' == $facet['type'] ) {
if ( in_array( $facet['search_engine'], $engines ) ) {
$settings['facets'][ $key ]['search_engine'] = 'swp_' . $facet['search_engine'];
}
}
}
update_option( 'facetwp_settings', json_encode( $settings ) );
}
}
if ( version_compare( $this->last_version, '3.5.3', '<' ) ) {
update_option( 'facetwp_updater_response', '', 'no' );
update_option( 'facetwp_updater_last_checked', '', 'no' );
}
if ( version_compare( $this->last_version, '3.5.4', '<' ) ) {
$settings = get_option( 'facetwp_settings' );
$settings = json_decode( $settings, true );
if ( ! isset( $settings['settings']['prefix'] ) ) {
$settings['settings']['prefix'] = 'fwp_';
update_option( 'facetwp_settings', json_encode( $settings ) );
}
}
}
}

View File

@@ -0,0 +1,163 @@
<?php
class FacetWP_Facet_Autocomplete extends FacetWP_Facet
{
public $is_buffering = false;
function __construct() {
$this->label = __( 'Autocomplete', 'fwp' );
$this->fields = [ 'placeholder' ];
// ajax
add_action( 'facetwp_autocomplete_load', [ $this, 'ajax_load' ] );
// css-based template
add_action( 'facetwp_init', [ $this, 'maybe_buffer_output' ] );
add_action( 'facetwp_found_main_query', [ $this, 'template_handler' ] );
// result limit
$this->limit = (int) apply_filters( 'facetwp_facet_autocomplete_limit', 10 );
}
/**
* For page templates with a custom WP_Query, we need to prevent the
* page header from being output with the autocomplete JSON
*/
function maybe_buffer_output() {
if ( isset( $_POST['action'] ) && 'facetwp_autocomplete_load' == $_POST['action'] ) {
$this->is_buffering = true;
ob_start();
}
}
/**
* For CSS-based templates, the "facetwp_autocomplete_load" action isn't fired
* so we need to manually check the action
*/
function template_handler() {
if ( isset( $_POST['action'] ) && 'facetwp_autocomplete_load' == $_POST['action'] ) {
if ( $this->is_buffering ) {
while ( ob_get_level() ) {
ob_end_clean();
}
}
$this->ajax_load();
}
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$value = (array) $params['selected_values'];
$value = empty( $value ) ? '' : stripslashes( $value[0] );
$placeholder = empty( $facet['placeholder'] ) ? __( 'Start typing', 'fwp-front' ) : $facet['placeholder'];
$placeholder = facetwp_i18n( $placeholder );
$output .= '<input type="text" class="facetwp-autocomplete" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $placeholder ) . '" autocomplete="off" />';
$output .= '<input type="button" class="facetwp-autocomplete-update" value="' . __( 'Go', 'fwp-front' ) . '" />';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$selected_values = is_array( $selected_values ) ? $selected_values[0] : $selected_values;
$selected_values = stripslashes( $selected_values );
if ( empty( $selected_values ) ) {
return 'continue';
}
$sql = "
SELECT DISTINCT post_id FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = %s AND facet_display_value LIKE %s";
$sql = $wpdb->prepare( $sql, $facet['name'], '%' . $selected_values . '%' );
return facetwp_sql( $sql, $facet );
}
/**
* Output any front-end scripts
*/
function front_scripts() {
FWP()->display->json['no_results'] = __( 'No results', 'fwp-front' );
FWP()->display->assets['fComplete.js'] = FACETWP_URL . '/assets/vendor/fComplete/fComplete.js';
FWP()->display->assets['fComplete.css'] = FACETWP_URL . '/assets/vendor/fComplete/fComplete.css';
}
/**
* Load facet values via AJAX
*/
function ajax_load() {
global $wpdb;
// optimizations
$_POST['data']['soft_refresh'] = 1;
$_POST['data']['extras'] = [];
$query = stripslashes( $_POST['query'] );
$query = FWP()->helper->sanitize( $wpdb->esc_like( $query ) );
$facet_name = FWP()->helper->sanitize( $_POST['facet_name'] );
$output = [];
// simulate a refresh
FWP()->facet->render(
FWP()->request->process_post_data()
);
// then grab the matching post IDs
$where_clause = $this->get_where_clause( [ 'name' => $facet_name ] );
if ( ! empty( $query ) && ! empty( $facet_name ) ) {
$sql = "
SELECT DISTINCT facet_display_value
FROM {$wpdb->prefix}facetwp_index
WHERE
facet_name = '$facet_name' AND
facet_display_value LIKE '%$query%'
$where_clause
ORDER BY facet_display_value ASC
LIMIT $this->limit";
$results = $wpdb->get_results( $sql );
foreach ( $results as $result ) {
$output[] = [
'value' => $result->facet_display_value,
'label' => $result->facet_display_value,
];
}
}
wp_send_json( $output );
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
return [
'loadingText' => __( 'Loading', 'fwp-front' ) . '...',
'minCharsText' => __( 'Enter {n} or more characters', 'fwp-front' ),
'noResultsText' => __( 'No results', 'fwp-front' ),
'maxResults' => $this->limit
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
class FacetWP_Facet
{
/**
* Grab the orderby, as needed by several facet types
* @since 3.0.4
*/
function get_orderby( $facet ) {
$key = $facet['orderby'];
// Count (default)
$orderby = 'counter DESC, f.facet_display_value ASC';
// Display value
if ( 'display_value' == $key ) {
$orderby = 'f.facet_display_value ASC';
}
// Raw value
elseif ( 'raw_value' == $key ) {
$orderby = 'f.facet_value ASC';
}
// Term order
elseif ( 'term_order' == $key && 'tax' == substr( $facet['source'], 0, 3 ) ) {
$term_ids = get_terms( [
'taxonomy' => str_replace( 'tax/', '', $facet['source'] ),
'term_order' => true, // Custom flag
'fields' => 'ids',
] );
if ( ! empty( $term_ids ) && ! is_wp_error( $term_ids ) ) {
$term_ids = implode( ',', $term_ids );
$orderby = "FIELD(f.term_id, $term_ids)";
}
}
// Sort by depth
if ( FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' ) ) {
$orderby = "f.depth, $orderby";
}
return $orderby;
}
/**
* Grab the limit, and support -1
* @since 3.5.4
*/
function get_limit( $facet, $default = 10 ) {
$count = $facet['count'];
if ( '-1' == $count ) {
return 1000;
}
elseif ( ctype_digit( $count ) ) {
return $count;
}
return $default;
}
/**
* Adjust the $where_clause for facets in "OR" mode
*
* FWP()->or_values contains EVERY facet and their matching post IDs
* FWP()->unfiltered_post_ids contains original post IDs
*
* @since 3.2.0
*/
function get_where_clause( $facet ) {
// Ignore the current facet's selections
if ( isset( FWP()->or_values ) && ( 1 < count( FWP()->or_values ) || ! isset( FWP()->or_values[ $facet['name'] ] ) ) ) {
$post_ids = [];
$or_values = FWP()->or_values; // Preserve original
unset( $or_values[ $facet['name'] ] );
$counter = 0;
foreach ( $or_values as $name => $vals ) {
$post_ids = ( 0 == $counter ) ? $vals : array_intersect( $post_ids, $vals );
$counter++;
}
$post_ids = array_intersect( $post_ids, FWP()->unfiltered_post_ids );
}
else {
$post_ids = FWP()->unfiltered_post_ids;
}
$post_ids = empty( $post_ids ) ? [ 0 ] : $post_ids;
return ' AND post_id IN (' . implode( ',', $post_ids ) . ')';
}
/**
* Render some commonly used admin settings
* @since 3.5.6
* @deprecated 3.9
*/
function render_setting( $name ) {
echo FWP()->settings->get_facet_field_html( $name );
}
}

View File

@@ -0,0 +1,258 @@
<?php
class FacetWP_Facet_Checkboxes extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Checkboxes', 'fwp' );
$this->fields = [ 'parent_term', 'modifiers', 'hierarchical', 'show_expanded',
'ghosts', 'operator', 'orderby', 'count', 'soft_limit' ];
}
/**
* Load the available choices
*/
function load_values( $params ) {
global $wpdb;
$facet = $params['facet'];
$from_clause = $wpdb->prefix . 'facetwp_index f';
$where_clause = $params['where_clause'];
// Orderby
$orderby = $this->get_orderby( $facet );
// Limit
$limit = $this->get_limit( $facet );
// Use "OR" mode when necessary
$is_single = FWP()->helper->facet_is( $facet, 'multiple', 'no' );
$using_or = FWP()->helper->facet_is( $facet, 'operator', 'or' );
// Facet in "OR" mode
if ( $is_single || $using_or ) {
$where_clause = $this->get_where_clause( $facet );
}
$orderby = apply_filters( 'facetwp_facet_orderby', $orderby, $facet );
$from_clause = apply_filters( 'facetwp_facet_from', $from_clause, $facet );
$where_clause = apply_filters( 'facetwp_facet_where', $where_clause, $facet );
$sql = "
SELECT f.facet_value, f.facet_display_value, f.term_id, f.parent_id, f.depth, COUNT(DISTINCT f.post_id) AS counter
FROM $from_clause
WHERE f.facet_name = '{$facet['name']}' $where_clause
GROUP BY f.facet_value
ORDER BY $orderby
LIMIT $limit";
$output = $wpdb->get_results( $sql, ARRAY_A );
// Show "ghost" facet choices
// For performance gains, only run if facets are in use
$show_ghosts = FWP()->helper->facet_is( $facet, 'ghosts', 'yes' );
if ( $show_ghosts && FWP()->is_filtered ) {
$raw_post_ids = implode( ',', FWP()->unfiltered_post_ids );
$sql = "
SELECT f.facet_value, f.facet_display_value, f.term_id, f.parent_id, f.depth, 0 AS counter
FROM $from_clause
WHERE f.facet_name = '{$facet['name']}' AND post_id IN ($raw_post_ids)
GROUP BY f.facet_value
ORDER BY $orderby
LIMIT $limit";
$ghost_output = $wpdb->get_results( $sql, ARRAY_A );
$tmp = [];
$preserve_ghosts = FWP()->helper->facet_is( $facet, 'preserve_ghosts', 'yes' );
$orderby_count = FWP()->helper->facet_is( $facet, 'orderby', 'count' );
// Keep the facet placement intact
if ( $preserve_ghosts && ! $orderby_count ) {
foreach ( $ghost_output as $row ) {
$tmp[ $row['facet_value'] . ' ' ] = $row;
}
foreach ( $output as $row ) {
$tmp[ $row['facet_value'] . ' ' ] = $row;
}
$output = $tmp;
}
else {
// Make the array key equal to the facet_value (for easy lookup)
foreach ( $output as $row ) {
$tmp[ $row['facet_value'] . ' ' ] = $row; // Force a string array key
}
$output = $tmp;
foreach ( $ghost_output as $row ) {
$facet_value = $row['facet_value'];
if ( ! isset( $output[ "$facet_value " ] ) ) {
$output[ "$facet_value " ] = $row;
}
}
}
$output = array_splice( $output, 0, $limit );
$output = array_values( $output );
}
return $output;
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$facet = $params['facet'];
if ( FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' ) ) {
return $this->render_hierarchy( $params );
}
$output = '';
$values = (array) $params['values'];
$soft_limit = empty( $facet['soft_limit'] ) ? 0 : (int) $facet['soft_limit'];
$key = 0;
foreach ( $values as $key => $row ) {
if ( 0 < $soft_limit && $key == $soft_limit ) {
$output .= '<div class="facetwp-overflow facetwp-hidden">';
}
$output .= $this->render_choice( $row, $params );
}
if ( 0 < $soft_limit && $soft_limit <= $key ) {
$output .= '</div>';
$output .= '<a class="facetwp-toggle">' . __( 'See {num} more', 'fwp-front' ) . '</a>';
$output .= '<a class="facetwp-toggle facetwp-hidden">' . __( 'See less', 'fwp-front' ) . '</a>';
}
return $output;
}
/**
* Generate the facet HTML (hierarchical taxonomies)
*/
function render_hierarchy( $params ) {
$output = '';
$facet = $params['facet'];
$values = FWP()->helper->sort_taxonomy_values( $params['values'], $facet['orderby'] );
$init_depth = -1;
$last_depth = -1;
foreach ( $values as $row ) {
$depth = (int) $row['depth'];
if ( -1 == $last_depth ) {
$init_depth = $depth;
}
elseif ( $depth > $last_depth ) {
$output .= '<div class="facetwp-depth">';
}
elseif ( $depth < $last_depth ) {
for ( $i = $last_depth; $i > $depth; $i-- ) {
$output .= '</div>';
}
}
$output .= $this->render_choice( $row, $params );
$last_depth = $depth;
}
for ( $i = $last_depth; $i > $init_depth; $i-- ) {
$output .= '</div>';
}
return $output;
}
/**
* Render a single facet choice
*/
function render_choice( $row, $params ) {
$label = esc_html( $row['facet_display_value'] );
$output = '';
$selected_values = (array) $params['selected_values'];
$selected = in_array( $row['facet_value'], $selected_values ) ? ' checked' : '';
$selected .= ( '' != $row['counter'] && 0 == $row['counter'] && '' == $selected ) ? ' disabled' : '';
$output .= '<div class="facetwp-checkbox' . $selected . '" data-value="' . esc_attr( $row['facet_value'] ) . '">';
$output .= '<span class="facetwp-display-value">';
$output .= apply_filters( 'facetwp_facet_display_value', $label, [
'selected' => ( '' !== $selected ),
'facet' => $params['facet'],
'row' => $row
]);
$output .= '</span>';
$output .= '<span class="facetwp-counter">(' . $row['counter'] . ')</span>';
$output .= '</div>';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$output = [];
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$sql = $wpdb->prepare( "SELECT DISTINCT post_id
FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = %s",
$facet['name']
);
// Match ALL values
if ( 'and' == $facet['operator'] ) {
foreach ( $selected_values as $key => $value ) {
$results = facetwp_sql( $sql . " AND facet_value IN ('$value')", $facet );
$output = ( $key > 0 ) ? array_intersect( $output, $results ) : $results;
if ( empty( $output ) ) {
break;
}
}
}
// Match ANY value
else {
$selected_values = implode( "','", $selected_values );
$output = facetwp_sql( $sql . " AND facet_value IN ('$selected_values')", $facet );
}
return $output;
}
/**
* Output any front-end scripts
*/
function front_scripts() {
FWP()->display->json['expand'] = '[+]';
FWP()->display->json['collapse'] = '[-]';
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
$expand = empty( $params['facet']['show_expanded'] ) ? 'no' : $params['facet']['show_expanded'];
return [ 'show_expanded' => $expand ];
}
}

View File

@@ -0,0 +1,292 @@
<?php
class FacetWP_Facet_Date_Range extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Date Range', 'fwp' );
$this->fields = [ 'source_other', 'compare_type', 'date_fields', 'date_format' ];
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$value = $params['selected_values'];
$value = empty( $value ) ? [ '', '' ] : $value;
$fields = empty( $params['facet']['fields'] ) ? 'both' : $params['facet']['fields'];
$text_date = facetwp_i18n( __( 'Date', 'fwp-front' ) );
$text_start_date = facetwp_i18n( __( 'Start Date', 'fwp-front' ) );
$text_end_date = facetwp_i18n( __( 'End Date', 'fwp-front' ) );
if ( 'exact' == $fields ) {
$output .= '<input type="text" class="facetwp-date facetwp-date-min" value="' . esc_attr( $value[0] ) . '" placeholder="' . esc_attr( $text_date ) . '" />';
}
if ( 'both' == $fields || 'start_date' == $fields ) {
$output .= '<input type="text" class="facetwp-date facetwp-date-min" value="' . esc_attr( $value[0] ) . '" placeholder="' . esc_attr( $text_start_date ) . '" />';
}
if ( 'both' == $fields || 'end_date' == $fields ) {
$output .= '<input type="text" class="facetwp-date facetwp-date-max" value="' . esc_attr( $value[1] ) . '" placeholder="' . esc_attr( $text_end_date ) . '" />';
}
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$facet = $params['facet'];
$values = $params['selected_values'];
$where = '';
$min = empty( $values[0] ) ? false : $values[0];
$max = empty( $values[1] ) ? false : $values[1];
$fields = $facet['fields'] ?? 'both';
$compare_type = empty( $facet['compare_type'] ) ? 'basic' : $facet['compare_type'];
$is_dual = ! empty( $facet['source_other'] );
if ( $is_dual && 'basic' != $compare_type ) {
if ( 'exact' == $fields ) {
$max = $min;
}
$min = ( false !== $min ) ? $min : '0000-00-00';
$max = ( false !== $max ) ? $max : '3000-12-31';
/**
* Enclose compare
* The post's range must surround the user-defined range
*/
if ( 'enclose' == $compare_type ) {
$where .= " AND LEFT(facet_value, 10) <= '$min'";
$where .= " AND LEFT(facet_display_value, 10) >= '$max'";
}
/**
* Intersect compare
* @link http://stackoverflow.com/a/325964
*/
if ( 'intersect' == $compare_type ) {
$where .= " AND LEFT(facet_value, 10) <= '$max'";
$where .= " AND LEFT(facet_display_value, 10) >= '$min'";
}
}
/**
* Basic compare
* The user-defined range must surround the post's range
*/
else {
if ( 'exact' == $fields ) {
$max = $min;
}
if ( false !== $min ) {
$where .= " AND LEFT(facet_value, 10) >= '$min'";
}
if ( false !== $max ) {
$where .= " AND LEFT(facet_display_value, 10) <= '$max'";
}
}
$sql = "
SELECT DISTINCT post_id FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}' $where";
return facetwp_sql( $sql, $facet );
}
/**
* Output any front-end scripts
*/
function front_scripts() {
FWP()->display->assets['fDate.css'] = FACETWP_URL . '/assets/vendor/fDate/fDate.css';
FWP()->display->assets['fDate.js'] = FACETWP_URL . '/assets/vendor/fDate/fDate.min.js';
}
function register_fields() {
return [
'date_fields' => [
'type' => 'alias',
'items' => [
'fields' => [
'type' => 'select',
'label' => __( 'Fields to show', 'fwp' ),
'choices' => [
'both' => __( 'Start + End Dates', 'fwp' ),
'exact' => __( 'Exact Date', 'fwp' ),
'start_date' => __( 'Start Date', 'fwp' ),
'end_date' => __( 'End Date', 'fwp' )
]
]
]
],
'date_format' => [
'type' => 'alias',
'items' => [
'format' => [
'label' => __( 'Display format', 'fwp' ),
'notes' => 'See available <a href="https://facetwp.com/help-center/facets/facet-types/date-range/#tokens" target="_blank">formatting tokens</a>',
'placeholder' => 'Y-m-d'
]
]
]
];
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
global $wpdb;
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$fields = empty( $facet['fields'] ) ? 'both' : $facet['fields'];
$format = empty( $facet['format'] ) ? '' : $facet['format'];
// Use "OR" mode by excluding the facet's own selection
$where_clause = $this->get_where_clause( $facet );
$sql = "
SELECT MIN(facet_value) AS `minDate`, MAX(facet_display_value) AS `maxDate` FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}' AND facet_display_value != '' $where_clause";
$row = $wpdb->get_row( $sql );
$min = substr( $row->minDate, 0, 10 );
$max = substr( $row->maxDate, 0, 10 );
if ( 'both' == $fields ) {
$min_upper = ! empty( $selected_values[1] ) ? $selected_values[1] : $max;
$max_lower = ! empty( $selected_values[0] ) ? $selected_values[0] : $min;
$range = [
'min' => [
'minDate' => $min,
'maxDate' => $min_upper
],
'max' => [
'minDate' => $max_lower,
'maxDate' => $max
]
];
}
else {
$range = [
'minDate' => $min,
'maxDate' => $max
];
}
return [
'locale' => $this->get_i18n_labels(),
'format' => $format,
'fields' => $fields,
'range' => $range
];
}
function get_i18n_labels() {
$locale = get_locale();
$locale = empty( $locale ) ? 'en' : substr( $locale, 0, 2 );
$locales = [
'ca' => [
'weekdays_short' => ['Dg', 'Dl', 'Dt', 'Dc', 'Dj', 'Dv', 'Ds'],
'months_short' => ['Gen', 'Febr', 'Març', 'Abr', 'Maig', 'Juny', 'Jul', 'Ag', 'Set', 'Oct', 'Nov', 'Des'],
'months' => ['Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'],
'firstDayOfWeek' => 1
],
'da' => [
'weekdays_short' => ['søn', 'man', 'tir', 'ons', 'tors', 'fre', 'lør'],
'months_short' => ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
'months' => ['januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'december'],
'firstDayOfWeek' => 1
],
'de' => [
'weekdays_short' => ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
'months_short' => ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
'months' => ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
'firstDayOfWeek' => 1
],
'es' => [
'weekdays_short' => ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'],
'months_short' => ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
'months' => ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
'firstDayOfWeek' => 1
],
'fr' => [
'weekdays_short' => ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam'],
'months_short' => ['janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', 'sept', 'oct', 'nov', 'déc'],
'months' => ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
'firstDayOfWeek' => 1
],
'it' => [
'weekdays_short' => ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
'months_short' => ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
'months' => ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
'firstDayOfWeek' => 1
],
'nb' => [
'weekdays_short' => ['Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'],
'months_short' => ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
'months' => ['Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Desember'],
'firstDayOfWeek' => 1
],
'nl' => [
'weekdays_short' => ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
'months_short' => ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sept', 'okt', 'nov', 'dec'],
'months' => ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
'firstDayOfWeek' => 1
],
'pl' => [
'weekdays_short' => ['Nd', 'Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So'],
'months_short' => ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
'months' => ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
'firstDayOfWeek' => 1
],
'pt' => [
'weekdays_short' => ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'],
'months_short' => ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
'months' => ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
'firstDayOfWeek' => 0
],
'ro' => [
'weekdays_short' => ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'],
'months_short' => ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Noi', 'Dec'],
'months' => ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'],
'firstDayOfWeek' => 1
],
'ru' => [
'weekdays_short' => ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
'months_short' => ['Янв', 'Фев', 'Март', 'Апр', 'Май', 'Июнь', 'Июль', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
'months' => ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
'firstDayOfWeek' => 1
],
'sv' => [
'weekdays_short' => ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör'],
'months_short' => ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
'months' => ['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'],
'firstDayOfWeek' => 1
]
];
if ( isset( $locales[ $locale ] ) ) {
$locales[ $locale ]['clearText'] = __( 'Clear', 'fwp-front' );
return $locales[ $locale ];
}
return '';
}
}

View File

@@ -0,0 +1,73 @@
<?php
class FacetWP_Facet_Dropdown extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Dropdown', 'fwp' );
$this->fields = [ 'label_any', 'parent_term', 'modifiers', 'hierarchical', 'orderby', 'count' ];
}
/**
* Load the available choices
*/
function load_values( $params ) {
return FWP()->helper->facet_types['checkboxes']->load_values( $params );
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$values = (array) $params['values'];
$selected_values = (array) $params['selected_values'];
$is_hierarchical = FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' );
if ( $is_hierarchical ) {
$values = FWP()->helper->sort_taxonomy_values( $params['values'], $facet['orderby'] );
}
$label_any = empty( $facet['label_any'] ) ? __( 'Any', 'fwp-front' ) : $facet['label_any'];
$label_any = facetwp_i18n( $label_any );
$output .= '<select class="facetwp-dropdown">';
$output .= '<option value="">' . esc_attr( $label_any ) . '</option>';
foreach ( $values as $row ) {
$selected = in_array( $row['facet_value'], $selected_values ) ? ' selected' : '';
$indent = $is_hierarchical ? str_repeat( '&nbsp;&nbsp;', (int) $row['depth'] ) : '';
// Determine whether to show counts
$label = esc_attr( $row['facet_display_value'] );
$label = apply_filters( 'facetwp_facet_display_value', $label, [
'selected' => ( '' !== $selected ),
'facet' => $facet,
'row' => $row
]);
$show_counts = apply_filters( 'facetwp_facet_dropdown_show_counts', true, [ 'facet' => $facet ] );
if ( $show_counts ) {
$label .= ' (' . $row['counter'] . ')';
}
$output .= '<option value="' . esc_attr( $row['facet_value'] ) . '"' . $selected . '>' . $indent . $label . '</option>';
}
$output .= '</select>';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
return FWP()->helper->facet_types['checkboxes']->filter_posts( $params );
}
}

View File

@@ -0,0 +1,100 @@
<?php
class FacetWP_Facet_fSelect extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'fSelect', 'fwp' );
$this->fields = [ 'label_any', 'parent_term', 'modifiers', 'hierarchical', 'multiple',
'ghosts', 'operator', 'orderby', 'count' ];
}
/**
* Load the available choices
*/
function load_values( $params ) {
return FWP()->helper->facet_types['checkboxes']->load_values( $params );
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$values = (array) $params['values'];
$selected_values = (array) $params['selected_values'];
$is_hierarchical = FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' );
if ( $is_hierarchical ) {
$values = FWP()->helper->sort_taxonomy_values( $params['values'], $facet['orderby'] );
}
$multiple = FWP()->helper->facet_is( $facet, 'multiple', 'yes' ) ? ' multiple="multiple"' : '';
$label_any = empty( $facet['label_any'] ) ? __( 'Any', 'fwp-front' ) : $facet['label_any'];
$label_any = facetwp_i18n( $label_any );
$output .= '<select class="facetwp-dropdown"' . $multiple . '>';
$output .= '<option value="">' . esc_html( $label_any ) . '</option>';
foreach ( $values as $row ) {
$selected = in_array( $row['facet_value'], $selected_values, true ) ? ' selected' : '';
$disabled = ( 0 == $row['counter'] && '' == $selected ) ? ' disabled' : '';
$label = esc_html( $row['facet_display_value'] );
$label = apply_filters( 'facetwp_facet_display_value', $label, [
'selected' => ( '' !== $selected ),
'facet' => $facet,
'row' => $row
]);
$show_counts = apply_filters( 'facetwp_facet_dropdown_show_counts', true, [ 'facet' => $facet ] );
$counter = $show_counts ? $row['counter'] : '';
$depth = $is_hierarchical ? $row['depth'] : 0;
$output .= '<option value="' . esc_attr( $row['facet_value'] ) . '" data-counter="' . $counter . '" class="d' . $depth . '"' . $selected . $disabled . '>' . $label . '</option>';
}
$output .= '</select>';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
return FWP()->helper->facet_types['checkboxes']->filter_posts( $params );
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
$facet = $params['facet'];
$label_any = empty( $facet['label_any'] ) ? __( 'Any', 'fwp-front' ) : $facet['label_any'];
$label_any = facetwp_i18n( $label_any );
return [
'placeholder' => $label_any,
'overflowText' => __( '{n} selected', 'fwp-front' ),
'searchText' => __( 'Search', 'fwp-front' ),
'noResultsText' => __( 'No results found', 'fwp-front' ),
'operator' => $facet['operator']
];
}
/**
* Output any front-end scripts
*/
function front_scripts() {
FWP()->display->assets['fSelect.css'] = FACETWP_URL . '/assets/vendor/fSelect/fSelect.css';
FWP()->display->assets['fSelect.js'] = FACETWP_URL . '/assets/vendor/fSelect/fSelect.js';
}
}

View File

@@ -0,0 +1,179 @@
<?php
class FacetWP_Facet_Hierarchy extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Hierarchy', 'fwp' );
$this->fields = [ 'label_any', 'modifiers', 'orderby', 'count' ];
}
/**
* Load the available choices
*/
function load_values( $params ) {
global $wpdb;
$facet = $params['facet'];
$from_clause = $wpdb->prefix . 'facetwp_index f';
$where_clause = $params['where_clause'];
$selected_values = (array) $params['selected_values'];
$facet_parent_id = 0;
$output = [];
$label_any = empty( $facet['label_any'] ) ? __( 'Any', 'fwp-front' ) : $facet['label_any'];
$label_any = facetwp_i18n( $label_any );
// Orderby
$orderby = $this->get_orderby( $facet );
// Determine the parent_id and depth
if ( ! empty( $selected_values[0] ) ) {
// Get term ID from slug
$sql = "
SELECT t.term_id
FROM {$wpdb->terms} t
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id AND tt.taxonomy = %s
WHERE t.slug = %s
LIMIT 1";
$value = $selected_values[0];
$taxonomy = str_replace( 'tax/', '', $facet['source'] );
$facet_parent_id = (int) $wpdb->get_var( $wpdb->prepare( $sql, $taxonomy, $value ) );
// Invalid term
if ( $facet_parent_id < 1 ) {
return [];
}
// Create term lookup array
$depths = FWP()->helper->get_term_depths( $taxonomy );
$max_depth = (int) $depths[ $facet_parent_id ]['depth'];
$last_parent_id = $facet_parent_id;
// Loop backwards
for ( $i = 0; $i <= $max_depth; $i++ ) {
$output[] = [
'facet_value' => $depths[ $last_parent_id ]['slug'],
'facet_display_value' => $depths[ $last_parent_id ]['name'],
'depth' => $depths[ $last_parent_id ]['depth'] + 1,
'counter' => 1, // FWP.settings.num_choices
];
$last_parent_id = (int) $depths[ $last_parent_id ]['parent_id'];
}
$output[] = [
'facet_value' => '',
'facet_display_value' => $label_any,
'depth' => 0,
'counter' => 1,
];
// Reverse it
$output = array_reverse( $output );
}
// Update the WHERE clause
$where_clause .= " AND parent_id = '$facet_parent_id'";
$orderby = apply_filters( 'facetwp_facet_orderby', $orderby, $facet );
$from_clause = apply_filters( 'facetwp_facet_from', $from_clause, $facet );
$where_clause = apply_filters( 'facetwp_facet_where', $where_clause, $facet );
$sql = "
SELECT f.facet_value, f.facet_display_value, COUNT(DISTINCT f.post_id) AS counter
FROM $from_clause
WHERE f.facet_name = '{$facet['name']}' $where_clause
GROUP BY f.facet_value
ORDER BY $orderby";
$results = $wpdb->get_results( $sql, ARRAY_A );
$new_depth = empty( $output ) ? 0 : $output[ count( $output ) - 1 ]['depth'] + 1;
foreach ( $results as $result ) {
$result['depth'] = $new_depth;
$result['is_choice'] = true;
$output[] = $result;
}
return $output;
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$facet = $params['facet'];
$values = (array) $params['values'];
$selected_values = (array) $params['selected_values'];
$output = '';
$num_visible = $this->get_limit( $facet );
$num = 0;
if ( ! empty( $values ) ) {
foreach ( $values as $row ) {
$last_depth = $last_depth ?? $row['depth'];
$selected = ( ! empty( $selected_values ) && $row['facet_value'] == $selected_values[0] );
$label = esc_html( $row['facet_display_value'] );
$label = apply_filters( 'facetwp_facet_display_value', $label, [
'selected' => $selected,
'facet' => $facet,
'row' => $row
]);
if ( $row['depth'] > $last_depth ) {
$output .= '<div class="facetwp-depth">';
}
if ( $num == $num_visible ) {
$output .= '<div class="facetwp-overflow facetwp-hidden">';
}
if ( ! $selected ) {
if ( isset( $row['is_choice'] ) ) {
$label .= ' <span class="facetwp-counter">(' . $row['counter'] . ')</span>';
}
else {
$arrow = apply_filters( 'facetwp_facet_hierarchy_arrow', '&#8249; ' );
$label = $arrow . $label;
}
}
$output .= '<div class="facetwp-link' . ( $selected ? ' checked' : '' ) . '" data-value="' . esc_attr( $row['facet_value'] ) . '">' . $label . '</div>';
if ( isset( $row['is_choice'] ) ) {
$num++;
}
$last_depth = $row['depth'];
}
if ( $num_visible < $num ) {
$output .= '</div>';
$output .= '<a class="facetwp-toggle">' . __( 'See more', 'fwp-front' ) . '</a>';
$output .= '<a class="facetwp-toggle facetwp-hidden">' . __( 'See less', 'fwp-front' ) . '</a>';
}
for ( $i = 0; $i <= $last_depth; $i++ ) {
$output .= '</div>';
}
}
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
return FWP()->helper->facet_types['checkboxes']->filter_posts( $params );
}
}

View File

@@ -0,0 +1,137 @@
<?php
class FacetWP_Facet_Number_Range extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Number Range', 'fwp' );
$this->fields = [ 'source_other', 'compare_type', 'number_fields' ];
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$value = $params['selected_values'];
$value = empty( $value ) ? [ '', '', ] : $value;
$fields = empty( $params['facet']['fields'] ) ? 'both' : $params['facet']['fields'];
if ( 'exact' == $fields ) {
$output .= '<input type="text" class="facetwp-number facetwp-number-min" value="' . esc_attr( $value[0] ) . '" placeholder="' . __( 'Number', 'fwp-front' ) . '" />';
}
if ( 'both' == $fields || 'min' == $fields ) {
$output .= '<input type="text" class="facetwp-number facetwp-number-min" value="' . esc_attr( $value[0] ) . '" placeholder="' . __( 'Min', 'fwp-front' ) . '" />';
}
if ( 'both' == $fields || 'max' == $fields ) {
$output .= '<input type="text" class="facetwp-number facetwp-number-max" value="' . esc_attr( $value[1] ) . '" placeholder="' . __( 'Max', 'fwp-front' ) . '" />';
}
$output .= '<input type="button" class="facetwp-submit" value="' . __( 'Go', 'fwp-front' ) . '" />';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$facet = $params['facet'];
$values = $params['selected_values'];
$where = '';
$min = ( '' == $values[0] ) ? false : $values[0];
$max = ( '' == $values[1] ) ? false : $values[1];
$fields = $facet['fields'] ?? 'both';
$compare_type = empty( $facet['compare_type'] ) ? 'basic' : $facet['compare_type'];
$is_dual = ! empty( $facet['source_other'] );
if ( $is_dual && 'basic' != $compare_type ) {
if ( 'exact' == $fields ) {
$max = $min;
}
$min = ( false !== $min ) ? $min : -999999999999;
$max = ( false !== $max ) ? $max : 999999999999;
/**
* Enclose compare
* The post's range must surround the user-defined range
*/
if ( 'enclose' == $compare_type ) {
$where .= " AND (facet_value + 0) <= '$min'";
$where .= " AND (facet_display_value + 0) >= '$max'";
}
/**
* Intersect compare
* @link http://stackoverflow.com/a/325964
*/
if ( 'intersect' == $compare_type ) {
$where .= " AND (facet_value + 0) <= '$max'";
$where .= " AND (facet_display_value + 0) >= '$min'";
}
}
/**
* Basic compare
* The user-defined range must surround the post's range
*/
else {
if ( 'exact' == $fields ) {
$max = $min;
}
if ( false !== $min ) {
$where .= " AND (facet_value + 0) >= '$min'";
}
if ( false !== $max ) {
$where .= " AND (facet_display_value + 0) <= '$max'";
}
}
$sql = "
SELECT DISTINCT post_id FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}' $where";
return facetwp_sql( $sql, $facet );
}
function register_fields() {
return [
'number_fields' => [
'type' => 'alias',
'items' => [
'fields' => [
'type' => 'select',
'label' => __( 'Fields to show', 'fwp' ),
'choices' => [
'both' => __( 'Min + Max', 'fwp' ),
'exact' => __( 'Exact', 'fwp' ),
'min' => __( 'Min', 'fwp' ),
'max' => __( 'Max', 'fwp' )
]
]
]
]
];
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
$facet = $params['facet'];
$fields = empty( $facet['fields'] ) ? 'both' : $facet['fields'];
return [
'fields' => $fields
];
}
}

View File

@@ -0,0 +1,277 @@
<?php
class FacetWP_Facet_Pager extends FacetWP_Facet
{
public $pager_args;
function __construct() {
$this->label = __( 'Pager', 'fwp' );
$this->fields = [ 'pager_type', 'inner_size', 'dots_label', 'prev_label', 'next_label',
'count_text_plural', 'count_text_singular', 'count_text_none', 'load_more_text',
'loading_text', 'default_label', 'per_page_options' ];
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$facet = $params['facet'];
$pager_type = $facet['pager_type'];
$this->pager_args = FWP()->facet->pager_args;
$method = 'render_' . $pager_type;
if ( method_exists( $this, $method ) ) {
return $this->$method( $facet );
}
}
function render_numbers( $facet ) {
$inner_size = (int) $facet['inner_size'];
$dots_label = facetwp_i18n( $facet['dots_label'] );
$prev_label = facetwp_i18n( $facet['prev_label'] );
$next_label = facetwp_i18n( $facet['next_label'] );
$output = '';
$page = (int) $this->pager_args['page'];
$total_pages = (int) $this->pager_args['total_pages'];
$inner_first = max( $page - $inner_size, 2 );
$inner_last = min( $page + $inner_size, $total_pages - 1 );
if ( 1 < $total_pages ) {
// Prev button
if ( 1 < $page && '' != $prev_label ) {
$output .= $this->render_page( $page - 1, $prev_label, 'prev' );
}
// First page
$output .= $this->render_page( 1, false, 'first' );
// Dots
if ( 2 < $inner_first && '' != $dots_label ) {
$output .= $this->render_page( '', $dots_label, 'dots' );
}
for ( $i = $inner_first; $i <= $inner_last; $i++ ) {
$output .= $this->render_page( $i );
}
// Dots
if ( $inner_last < $total_pages - 1 && '' != $dots_label ) {
$output .= $this->render_page( '', $dots_label, 'dots' );
}
// Last page
$output .= $this->render_page( $total_pages, false, 'last' );
// Next button
if ( $page < $total_pages && '' != $next_label ) {
$output .= $this->render_page( $page + 1, $next_label, 'next' );
}
}
return '<div class="facetwp-pager">' . $output . '</div>';
}
function render_page( $page, $label = false, $extra_class = false ) {
$label = ( false === $label ) ? $page : $label;
$class = 'facetwp-page';
if ( ! empty( $extra_class ) ) {
$class .= ' ' . $extra_class;
}
if ( $page == $this->pager_args['page'] ) {
$class .= ' active';
}
$data = empty( $page ) ? '' : ' data-page="' . $page . '"';
$html = '<a class="' . $class . '"' . $data . '>' . $label . '</a>';
return apply_filters( 'facetwp_facet_pager_link', $html, [
'page' => $page,
'label' => $label,
'extra_class' => $extra_class
]);
}
function render_counts( $facet ) {
$text_singular = facetwp_i18n( $facet['count_text_singular'] );
$text_plural = facetwp_i18n( $facet['count_text_plural'] );
$text_none = facetwp_i18n( $facet['count_text_none'] );
$page = $this->pager_args['page'];
$per_page = $this->pager_args['per_page'];
$total_rows = $this->pager_args['total_rows'];
$total_pages = $this->pager_args['total_pages'];
if ( -1 == $per_page ) {
$per_page = $total_rows;
}
if ( 1 < $total_rows ) {
$lower = ( 1 + ( ( $page - 1 ) * $per_page ) );
$upper = ( $page * $per_page );
$upper = ( $total_rows < $upper ) ? $total_rows : $upper;
// If a load_more pager is in use, force $lower = 1
if ( FWP()->helper->facet_setting_exists( 'pager_type', 'load_more' ) ) {
$lower = 1;
}
$output = $text_plural;
$output = str_replace( '[lower]', $lower, $output );
$output = str_replace( '[upper]', $upper, $output );
$output = str_replace( '[total]', $total_rows, $output );
$output = str_replace( '[page]', $page, $output );
$output = str_replace( '[per_page]', $per_page, $output );
$output = str_replace( '[total_pages]', $total_pages, $output );
}
else {
$output = ( 0 < $total_rows ) ? $text_singular : $text_none;
}
return $output;
}
function render_load_more( $facet ) {
$text = facetwp_i18n( $facet['load_more_text'] );
$loading_text = facetwp_i18n( $facet['loading_text'] );
return '<button class="facetwp-load-more" data-loading="' . esc_attr( $loading_text ) . '">' . esc_attr( $text ) . '</button>';
}
function render_per_page( $facet ) {
$default = facetwp_i18n( $facet['default_label'] );
$options = explode( ',', $facet['per_page_options'] );
$options = array_map( 'trim', $options );
$output = '';
if ( ! empty( $default ) ) {
$output .= '<option value="">' . esc_attr( $default ) . '</option>';
}
$per_page = $this->pager_args['per_page'];
$var_exists = isset( FWP()->request->url_vars['per_page'] );
foreach ( $options as $option ) {
$val = $label = $option;
// Support "All" option
if ( ! ctype_digit( $val ) ) {
$val = -1;
$label = facetwp_i18n( $label );
}
$selected = ( $var_exists && $val == $per_page ) ? ' selected' : '';
$output .= '<option value="' . $val . '"' . $selected . '>' . esc_attr( $label ) . '</option>';
}
return '<select class="facetwp-per-page-select">' . $output . '</select>';
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
return 'continue';
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
$facet = $params['facet'];
return [
'pager_type' => $facet['pager_type']
];
}
function register_fields() {
return [
'pager_type' => [
'type' => 'select',
'label' => __( 'Pager type', 'fwp' ),
'choices' => [
'numbers' => __( 'Page numbers', 'fwp' ),
'counts' => __( 'Result counts', 'fwp' ),
'load_more' => __( 'Load more', 'fwp' ),
'per_page' => __( 'Per page', 'fwp' )
]
],
'inner_size' => [
'label' => __( 'Inner size', 'fwp' ),
'notes' => 'Number of pages to show on each side of the current page',
'default' => 2,
'show' => "facet.pager_type == 'numbers'"
],
'dots_label' => [
'label' => __( 'Dots label', 'fwp' ),
'notes' => 'The filler between the inner and outer pages',
'default' => '…',
'show' => "facet.pager_type == 'numbers'"
],
'prev_label' => [
'label' => __( 'Prev button label', 'fwp' ),
'notes' => 'Leave blank to hide',
'default' => '« Prev',
'show' => "facet.pager_type == 'numbers'"
],
'next_label' => [
'label' => __( 'Next button label', 'fwp' ),
'notes' => 'Leave blank to hide',
'default' => 'Next »',
'show' => "facet.pager_type == 'numbers'"
],
'count_text_plural' => [
'label' => __( 'Count text (plural)', 'fwp' ),
'notes' => 'Available tags: [lower], [upper], [total], [page], [per_page], [total_pages]',
'default' => '[lower] - [upper] of [total] results',
'show' => "facet.pager_type == 'counts'"
],
'count_text_singular' => [
'label' => __( 'Count text (singular)', 'fwp' ),
'default' => '1 result',
'show' => "facet.pager_type == 'counts'"
],
'count_text_none' => [
'label' => __( 'Count text (no results)', 'fwp' ),
'default' => 'No results',
'show' => "facet.pager_type == 'counts'"
],
'load_more_text' => [
'label' => __( 'Load more text', 'fwp' ),
'default' => 'Load more',
'show' => "facet.pager_type == 'load_more'"
],
'loading_text' => [
'label' => __( 'Loading text', 'fwp' ),
'default' => 'Loading...',
'show' => "facet.pager_type == 'load_more'"
],
'default_label' => [
'label' => __( 'Default label', 'fwp' ),
'default' => 'Per page',
'show' => "facet.pager_type == 'per_page'"
],
'per_page_options' => [
'label' => __( 'Per page options', 'fwp' ),
'notes' => 'A comma-separated list of choices. Optionally add a non-numeric choice to be used as a "Show all" option.',
'default' => '10, 25, 50, 100',
'show' => "facet.pager_type == 'per_page'"
]
];
}
}

View File

@@ -0,0 +1,354 @@
<?php
class FacetWP_Facet_Proximity_Core extends FacetWP_Facet
{
/* (array) Associative array containing post_id => distance */
public $distance = [];
/* (array) Associative array containing post_id => [lat, lng] */
public $post_latlng = [];
function __construct() {
$this->label = __( 'Proximity', 'fwp' );
$this->fields = [ 'longitude', 'unit', 'radius_ui', 'radius_options', 'radius_min', 'radius_max', 'radius_default' ];
add_filter( 'facetwp_index_row', [ $this, 'index_latlng' ], 1, 2 );
add_filter( 'facetwp_sort_options', [ $this, 'sort_options' ], 1, 2 );
add_filter( 'facetwp_filtered_post_ids', [ $this, 'sort_by_distance' ], 10, 2 );
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$value = $params['selected_values'];
$unit = empty( $facet['unit'] ) ? 'mi' : $facet['unit'];
$lat = empty( $value[0] ) ? '' : $value[0];
$lng = empty( $value[1] ) ? '' : $value[1];
$chosen_radius = empty( $value[2] ) ? '' : (float) $value[2];
$location_name = empty( $value[3] ) ? '' : urldecode( $value[3] );
$radius_options = [ 10, 25, 50, 100, 250 ];
// Grab the radius UI
$radius_ui = empty( $facet['radius_ui'] ) ? 'dropdown' : $facet['radius_ui'];
// Grab radius options from the UI
if ( ! empty( $facet['radius_options'] ) ) {
$radius_options = explode( ',', preg_replace( '/\s+/', '', $facet['radius_options'] ) );
}
// Grab default radius from the UI
if ( empty( $chosen_radius ) && ! empty( $facet['radius_default'] ) ) {
$chosen_radius = (float) $facet['radius_default'];
}
// Support dynamic radius
if ( ! empty( $chosen_radius ) && 0 < $chosen_radius ) {
if ( ! in_array( $chosen_radius, $radius_options ) ) {
$radius_options[] = $chosen_radius;
}
}
$radius_options = apply_filters( 'facetwp_proximity_radius_options', $radius_options );
ob_start();
?>
<span class="facetwp-input-wrap">
<i class="facetwp-icon locate-me"></i>
<input type="text" class="facetwp-location" value="<?php echo esc_attr( $location_name ); ?>" placeholder="<?php _e( 'Enter location', 'fwp-front' ); ?>" autocomplete="off" />
<div class="location-results facetwp-hidden"></div>
</span>
<?php if ( 'dropdown' == $radius_ui ) : ?>
<select class="facetwp-radius facetwp-radius-dropdown">
<?php foreach ( $radius_options as $radius ) : ?>
<?php $selected = ( $chosen_radius == $radius ) ? ' selected' : ''; ?>
<option value="<?php echo $radius; ?>"<?php echo $selected; ?>><?php echo "$radius $unit"; ?></option>
<?php endforeach; ?>
</select>
<?php elseif ( 'slider' == $radius_ui ) : ?>
<div class="facetwp-radius-wrap">
<input class="facetwp-radius facetwp-radius-slider" type="range"
min="<?php echo $facet['radius_min']; ?>"
max="<?php echo $facet['radius_max']; ?>"
value="<?php echo $chosen_radius; ?>"
/>
<div class="facetwp-radius-label">
<span class="facetwp-radius-dist"><?php echo $chosen_radius; ?></span>
<span class="facetwp-radius-unit"><?php echo $facet['unit']; ?></span>
</div>
</div>
<?php elseif ( 'none' == $radius_ui ) : ?>
<input class="facetwp-radius facetwp-hidden" value="<?php echo $chosen_radius; ?>" />
<?php endif; ?>
<input type="hidden" class="facetwp-lat" value="<?php echo esc_attr( $lat ); ?>" />
<input type="hidden" class="facetwp-lng" value="<?php echo esc_attr( $lng ); ?>" />
<?php
return ob_get_clean();
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$unit = empty( $facet['unit'] ) ? 'mi' : $facet['unit'];
$earth_radius = ( 'mi' == $unit ) ? 3959 : 6371;
if ( empty( $selected_values ) || empty( $selected_values[0] ) ) {
return 'continue';
}
$lat1 = (float) $selected_values[0];
$lng1 = (float) $selected_values[1];
$radius = (float) $selected_values[2];
$rad = M_PI / 180;
$sql = "
SELECT DISTINCT post_id, facet_value AS `lat`, facet_display_value AS `lng`
FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}'";
$results = $wpdb->get_results( $sql );
foreach ( $results as $row ) {
$lat2 = (float) $row->lat;
$lng2 = (float) $row->lng;
if ( ( $lat1 == $lat2 ) && ( $lng1 == $lng2 ) ) {
$dist = 0;
}
else {
$calc = sin( $lat1 * $rad ) * sin( $lat2 * $rad ) +
cos( $lat1 * $rad ) * cos( $lat2 * $rad ) *
cos( $lng2 * $rad - $lng1 * $rad );
// acos() must be between -1 and 1
$dist = acos( max( -1, min( 1, $calc ) ) ) * $earth_radius;
}
if ( $dist <= $radius ) {
$existing = $this->distance[ $row->post_id ] ?? -1;
if ( -1 == $existing || $dist < $existing ) {
$this->distance[ $row->post_id ] = $dist;
if ( apply_filters( 'facetwp_proximity_store_latlng', false ) ) {
$this->post_latlng[ $row->post_id ] = [ $lat2, $lng2 ];
}
}
}
}
asort( $this->distance, SORT_NUMERIC );
return array_keys( $this->distance );
}
/**
* Output front-end scripts
*/
function front_scripts() {
if ( apply_filters( 'facetwp_proximity_load_js', true ) ) {
// hard-coded
$api_key = defined( 'GMAPS_API_KEY' ) ? GMAPS_API_KEY : '';
// admin ui
$tmp_key = FWP()->helper->get_setting( 'gmaps_api_key' );
$api_key = empty( $tmp_key ) ? $api_key : $tmp_key;
// hook
$api_key = apply_filters( 'facetwp_gmaps_api_key', $api_key );
FWP()->display->assets['gmaps'] = '//maps.googleapis.com/maps/api/js?libraries=places&key=' . trim( $api_key ) . '&callback=Function.prototype';
}
// Pass extra options into Places Autocomplete
$options = apply_filters( 'facetwp_proximity_autocomplete_options', [] );
FWP()->display->json['proximity']['autocomplete_options'] = $options;
FWP()->display->json['proximity']['clearText'] = __( 'Clear location', 'fwp-front' );
FWP()->display->json['proximity']['queryDelay'] = 250;
FWP()->display->json['proximity']['minLength'] = 3;
}
function register_fields() {
return [
'longitude' => [
'type' => 'alias',
'items' => [
'source_other' => [
'label' => __( 'Longitude', 'fwp' ),
'notes' => '(Optional) use a separate longitude field',
'html' => '<data-sources :facet="facet" setting-name="source_other"></data-sources>'
]
]
],
'unit' => [
'type' => 'select',
'label' => __( 'Unit of measurement', 'fwp' ),
'choices' => [
'mi' => __( 'Miles', 'fwp' ),
'km' => __( 'Kilometers', 'fwp' )
]
],
'radius_ui' => [
'type' => 'select',
'label' => __( 'Radius UI', 'fwp' ),
'choices' => [
'dropdown' => __( 'Dropdown', 'fwp' ),
'slider' => __( 'Slider', 'fwp' ),
'none' => __( 'None', 'fwp' )
]
],
'radius_options' => [
'label' => __( 'Radius options', 'fwp' ),
'notes' => 'A comma-separated list of radius choices',
'default' => '10, 25, 50, 100, 250',
'show' => "facet.radius_ui == 'dropdown'"
],
'radius_min' => [
'label' => __( 'Range (min)', 'fwp' ),
'default' => 1,
'show' => "facet.radius_ui == 'slider'"
],
'radius_max' => [
'label' => __( 'Range (max)', 'fwp' ),
'default' => 50,
'show' => "facet.radius_ui == 'slider'"
],
'radius_default' => [
'label' => __( 'Default radius', 'fwp' ),
'default' => 25
]
];
}
/**
* Index the coordinates
* We expect a comma-separated "latitude, longitude"
*/
function index_latlng( $params, $class ) {
$facet = FWP()->helper->get_facet_by_name( $params['facet_name'] );
if ( false !== $facet && 'proximity' == $facet['type'] ) {
$latlng = $params['facet_value'];
// Only handle "lat, lng" strings
if ( ! empty( $latlng ) && is_string( $latlng ) ) {
$latlng = preg_replace( '/[^0-9.,-]/', '', $latlng );
if ( ! empty( $facet['source_other'] ) ) {
$other_params = $params;
$other_params['facet_source'] = $facet['source_other'];
$rows = $class->get_row_data( $other_params );
if ( ! empty( $rows ) && false === strpos( $latlng, ',' ) ) {
$lng = $rows[0]['facet_display_value'];
$lng = preg_replace( '/[^0-9.,-]/', '', $lng );
$latlng .= ',' . $lng;
}
}
if ( preg_match( "/^([\d.-]+),([\d.-]+)$/", $latlng ) ) {
$latlng = explode( ',', $latlng );
$params['facet_value'] = $latlng[0];
$params['facet_display_value'] = $latlng[1];
}
}
}
return $params;
}
/**
* Add "Distance" to the sort box
*/
function sort_options( $options, $params ) {
if ( FWP()->helper->facet_setting_exists( 'type', 'proximity' ) ) {
$options['distance'] = [
'label' => __( 'Distance', 'fwp-front' ),
'query_args' => [
'orderby' => 'post__in',
'order' => 'ASC',
],
];
}
return $options;
}
/**
* Sort the final (filtered) post IDs by distance
*/
function sort_by_distance( $post_ids, $class ) {
$distance = FWP()->helper->facet_types['proximity']->distance;
if ( ! empty( $distance ) ) {
$ordered_posts = array_keys( $distance );
$filtered_posts = array_flip( $post_ids );
$intersected_ids = [];
foreach ( $ordered_posts as $p ) {
if ( isset( $filtered_posts[ $p ] ) ) {
$intersected_ids[] = $p;
}
}
$post_ids = $intersected_ids;
}
return $post_ids;
}
}
/**
* Get a post's distance
*/
function facetwp_get_distance( $post_id = false ) {
global $post;
// Get the post ID
$post_id = ( false === $post_id ) ? $post->ID : $post_id;
// Get the proximity class
$facet_type = FWP()->helper->facet_types['proximity'];
// Get the distance
$distance = $facet_type->distance[ $post_id ] ?? -1;
if ( -1 < $distance ) {
return apply_filters( 'facetwp_proximity_distance_output', $distance );
}
return false;
}

View File

@@ -0,0 +1,64 @@
<?php
class FacetWP_Facet_Radio_Core extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Radio', 'fwp' );
$this->fields = [ 'label_any', 'parent_term', 'modifiers', 'ghosts', 'orderby', 'count' ];
}
/**
* Load the available choices
*/
function load_values( $params ) {
$params['facet']['operator'] = 'or';
return FWP()->helper->facet_types['checkboxes']->load_values( $params );
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$values = (array) $params['values'];
$selected_values = (array) $params['selected_values'];
$label_any = empty( $facet['label_any'] ) ? false : facetwp_i18n( $facet['label_any'] );
if ( $label_any ) {
$selected = empty( $selected_values ) ? ' checked' : '';
$output .= '<div class="facetwp-radio' . $selected . '" data-value="">' . esc_attr( $label_any ) . '</div>';
}
foreach ( $values as $row ) {
$label = esc_html( $row['facet_display_value'] );
$selected = in_array( $row['facet_value'], $selected_values ) ? ' checked' : '';
$selected .= ( 0 == $row['counter'] && '' == $selected ) ? ' disabled' : '';
$output .= '<div class="facetwp-radio' . $selected . '" data-value="' . esc_attr( $row['facet_value'] ) . '">';
$output .= '<span class="facetwp-display-value">';
$output .= apply_filters( 'facetwp_facet_display_value', $label, [
'selected' => ( '' !== $selected ),
'facet' => $facet,
'row' => $row
]);
$output .= '</span>';
$output .= '<span class="facetwp-counter">(' . $row['counter'] . ')</span>';
$output .= '</div>';
}
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
$params['facet']['operator'] = 'or';
return FWP()->helper->facet_types['checkboxes']->filter_posts( $params );
}
}

View File

@@ -0,0 +1,114 @@
<?php
class FacetWP_Facet_Rating extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Star Rating', 'fwp' );
$this->fields = [];
}
/**
* Load the available choices
*/
function load_values( $params ) {
global $wpdb;
$facet = $params['facet'];
$from_clause = $wpdb->prefix . 'facetwp_index f';
// Facet in "OR" mode
$where_clause = $this->get_where_clause( $facet );
$output = [
1 => [ 'counter' => 0 ],
2 => [ 'counter' => 0 ],
3 => [ 'counter' => 0 ],
4 => [ 'counter' => 0 ],
5 => [ 'counter' => 0 ]
];
$sql = "
SELECT COUNT(*) AS `count`, FLOOR(f.facet_value) AS `rating`
FROM $from_clause
WHERE f.facet_name = '{$facet['name']}' AND FLOOR(f.facet_value) >= 1 $where_clause
GROUP BY rating";
$results = $wpdb->get_results( $sql );
foreach ( $results as $result ) {
$output[ $result->rating ]['counter'] = $result->count;
}
$total = 0;
// The lower rating should include higher rating counts
for ( $i = 5; $i > 0; $i-- ) {
$output[ $i ]['counter'] += $total;
$total = $output[ $i ]['counter'];
}
return $output;
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$values = (array) $params['values'];
$selected_values = (array) $params['selected_values'];
$num_stars = 0;
foreach ( $values as $val ) {
if ( 0 < $val['counter'] ) {
$num_stars++;
}
}
if ( 0 < $num_stars ) {
$output .= '<span class="facetwp-stars">';
for ( $i = $num_stars; $i >= 1; $i-- ) {
$class = in_array( $i, $selected_values ) ? ' selected' : '';
$output .= '<span class="facetwp-star' . $class . '" data-value="' . $i . '" data-counter="' . $values[ $i ]['counter'] . '">&#9733;</span>';
}
$output .= '</span>';
$output .= ' <span class="facetwp-star-label"></span>';
$output .= ' <span class="facetwp-counter"></span>';
}
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
global $wpdb;
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$selected_values = is_array( $selected_values ) ? $selected_values[0] : $selected_values;
$sql = "
SELECT DISTINCT post_id FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}' AND facet_value >= '$selected_values'";
return $wpdb->get_col( $sql );
}
/**
* Output front-end scripts
*/
function front_scripts() {
FWP()->display->json['rating']['& up'] = __( '& up', 'fwp-front' );
FWP()->display->json['rating']['Undo'] = __( 'Undo', 'fwp-front' );
}
}

View File

@@ -0,0 +1,85 @@
<?php
class FacetWP_Facet_Reset extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Reset', 'fwp' );
$this->fields = [ 'reset_ui', 'reset_text', 'reset_mode', 'reset_facets', 'auto_hide' ];
}
function render( $params ) {
$facet = $params['facet'];
$reset_ui = $facet['reset_ui'];
$reset_text = empty( $facet['reset_text'] ) ? __( 'Reset', 'fwp-front' ) : $facet['reset_text'];
$reset_text = facetwp_i18n( $reset_text );
$classes = [ 'facetwp-reset' ];
$attrs = '';
if ( ! FWP()->helper->facet_is( $facet, 'reset_mode', 'off' ) ) {
if ( ! empty( $facet['reset_facets'] ) ) {
$vals = implode( ',', $facet['reset_facets'] );
$attrs = ' data-mode="{mode}" data-values="{vals}"';
$attrs = str_replace( '{mode}', $facet['reset_mode'], $attrs );
$attrs = str_replace( '{vals}', esc_attr( $vals ), $attrs );
}
}
if ( FWP()->helper->facet_is( $facet, 'auto_hide', 'yes' ) ) {
$classes[] = 'facetwp-hide-empty';
}
if ( 'button' == $reset_ui ) {
$output = '<button class="{classes}"{attrs}>{label}</button>';
}
else {
$output = '<a class="{classes}" href="javascript:;"{attrs}>{label}</a>';
}
$output = str_replace( '{classes}', implode( ' ', $classes ), $output );
$output = str_replace( '{label}', esc_attr( $reset_text ), $output );
$output = str_replace( '{attrs}', $attrs, $output );
return $output;
}
function filter_posts( $params ) {
return 'continue';
}
function register_fields() {
return [
'reset_ui' => [
'type' => 'select',
'label' => __( 'Reset UI', 'fwp' ),
'choices' => [
'button' => __( 'Button', 'fwp' ),
'link' => __( 'Link', 'fwp' )
]
],
'reset_mode' => [
'type' => 'select',
'label' => __( 'Include / exclude', 'fwp' ),
'notes' => 'Include or exclude certain facets?',
'choices' => [
'off' => __( 'Reset everything', 'fwp' ),
'include' => __( 'Reset only these facets', 'fwp' ),
'exclude' => __( 'Reset all except these facets', 'fwp' )
]
],
'reset_facets' => [
'label' => '',
'html' => '<facet-names :facet="facet" setting="reset_facets"></facet-names>',
'show' => "facet.reset_mode != 'off'"
],
'auto_hide' => [
'type' => 'toggle',
'label' => __( 'Auto-hide', 'fwp' ),
'notes' => 'Hide when no facets have selected values'
]
];
}
}

View File

@@ -0,0 +1,89 @@
<?php
class FacetWP_Facet_Search extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Search', 'fwp' );
$this->fields = [ 'search_engine', 'placeholder', 'auto_refresh' ];
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$output = '';
$facet = $params['facet'];
$value = (array) $params['selected_values'];
$value = empty( $value ) ? '' : stripslashes( $value[0] );
$placeholder = empty( $facet['placeholder'] ) ? __( 'Enter keywords', 'fwp-front' ) : $facet['placeholder'];
$placeholder = facetwp_i18n( $placeholder );
$output .= '<span class="facetwp-input-wrap">';
$output .= '<i class="facetwp-icon"></i>';
$output .= '<input type="text" class="facetwp-search" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $placeholder ) . '" autocomplete="off" />';
$output .= '</span>';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$selected_values = is_array( $selected_values ) ? $selected_values[0] : $selected_values;
if ( empty( $selected_values ) ) {
return 'continue';
}
// Default WP search
$search_args = [
's' => $selected_values,
'posts_per_page' => 200,
'fields' => 'ids',
];
$search_args = apply_filters( 'facetwp_search_query_args', $search_args, $params );
$query = new WP_Query( $search_args );
return (array) $query->posts;
}
function register_fields() {
$engines = apply_filters( 'facetwp_facet_search_engines', [] );
$choices = [ '' => __( 'WP Default', 'fwp' ) ];
foreach ( $engines as $key => $label ) {
$choices[ $key ] = $label;
}
return [
'search_engine' => [
'type' => 'select',
'label' => __( 'Search engine', 'fwp' ),
'choices' => $choices
],
'auto_refresh' => [
'type' => 'toggle',
'label' => __( 'Auto refresh', 'fwp' ),
'notes' => 'Automatically refresh the results while typing?'
]
];
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
$auto_refresh = empty( $params['facet']['auto_refresh'] ) ? 'no' : $params['facet']['auto_refresh'];
return [ 'auto_refresh' => $auto_refresh ];
}
}

View File

@@ -0,0 +1,160 @@
<?php
class FacetWP_Facet_Slider extends FacetWP_Facet
{
function __construct() {
$this->label = __( 'Slider', 'fwp' );
$this->fields = [ 'source_other', 'compare_type', 'prefix', 'suffix',
'reset_text', 'slider_format', 'step' ];
add_filter( 'facetwp_render_output', [ $this, 'maybe_prevent_facet_html' ], 10, 2 );
}
/**
* Generate the facet HTML
*/
function render( $params ) {
$facet = $params['facet'];
$reset_text = __( 'Reset', 'fwp-front' );
if ( ! empty( $facet['reset_text'] ) ) {
$reset_text = facetwp_i18n( $facet['reset_text'] );
}
$output = '<div class="facetwp-slider-wrap">';
$output .= '<div class="facetwp-slider"></div>';
$output .= '</div>';
$output .= '<span class="facetwp-slider-label"></span>';
$output .= '<div><input type="button" class="facetwp-slider-reset" value="' . esc_attr( $reset_text ) . '" /></div>';
return $output;
}
/**
* Filter the query based on selected values
*/
function filter_posts( $params ) {
return FWP()->helper->facet_types['number_range']->filter_posts( $params );
}
/**
* (Front-end) Attach settings to the AJAX response
*/
function settings_js( $params ) {
global $wpdb;
$facet = $params['facet'];
$where_clause = $this->get_where_clause( $facet );
$selected_values = $params['selected_values'];
// Set default slider values
$defaults = [
'format' => '',
'prefix' => '',
'suffix' => '',
'step' => 1,
];
$facet = array_merge( $defaults, $facet );
$sql = "
SELECT MIN(facet_value + 0) AS `min`, MAX(facet_display_value + 0) AS `max` FROM {$wpdb->prefix}facetwp_index
WHERE facet_name = '{$facet['name']}' AND facet_display_value != '' $where_clause";
$row = $wpdb->get_row( $sql );
$range_min = (float) $row->min;
$range_max = (float) $row->max;
$selected_min = (float) ( $selected_values[0] ?? $range_min );
$selected_max = (float) ( $selected_values[1] ?? $range_max );
return [
'range' => [ // outer (bar)
'min' => min( $range_min, $selected_min ),
'max' => max( $range_max, $selected_max )
],
'decimal_separator' => FWP()->helper->get_setting( 'decimal_separator' ),
'thousands_separator' => FWP()->helper->get_setting( 'thousands_separator' ),
'start' => [ $selected_min, $selected_max ], // inner (handles)
'format' => $facet['format'],
'prefix' => facetwp_i18n( $facet['prefix'] ),
'suffix' => facetwp_i18n( $facet['suffix'] ),
'step' => $facet['step']
];
}
/**
* Prevent the slider HTML from refreshing when active
* @since 3.8.11
*/
function maybe_prevent_facet_html( $output, $params ) {
if ( ! empty( $output['facets'] && 0 === $params['first_load' ] ) ) {
foreach ( FWP()->facet->facets as $name => $facet ) {
if ( 'slider' == $facet['type'] && ! empty( $facet['selected_values'] ) ) {
unset( $output['facets'][ $name ] );
}
}
}
return $output;
}
/**
* Output any front-end scripts
*/
function front_scripts() {
FWP()->display->assets['nouislider.css'] = FACETWP_URL . '/assets/vendor/noUiSlider/nouislider.css';
FWP()->display->assets['nouislider.js'] = FACETWP_URL . '/assets/vendor/noUiSlider/nouislider.min.js';
FWP()->display->assets['nummy.js'] = FACETWP_URL . '/assets/vendor/nummy/nummy.min.js';
}
function register_fields() {
$thousands = FWP()->helper->get_setting( 'thousands_separator' );
$decimal = FWP()->helper->get_setting( 'decimal_separator' );
$choices = [];
if ( '' != $thousands ) {
$choices['0,0'] = "5{$thousands}280";
$choices['0,0.0'] = "5{$thousands}280{$decimal}4";
$choices['0,0.00'] = "5{$thousands}280{$decimal}42";
}
$choices['0'] = '5280';
$choices['0.0'] = "5280{$decimal}4";
$choices['0.00'] = "5280{$decimal}42";
$choices['0a'] = '5k';
$choices['0.0a'] = "5{$decimal}3k";
$choices['0.00a'] = "5{$decimal}28k";
return [
'prefix' => [
'label' => __( 'Prefix', 'fwp' ),
'notes' => 'Text that appears before each slider value',
],
'suffix' => [
'label' => __( 'Suffix', 'fwp' ),
'notes' => 'Text that appears after each slider value',
],
'slider_format' => [
'type' => 'alias',
'items' => [
'format' => [
'type' => 'select',
'label' => __( 'Format', 'fwp' ),
'notes' => 'If the number separators are wrong, change the [Separators] setting in the Settings tab, then save and reload the page',
'choices' => $choices
]
]
],
'step' => [
'label' => __( 'Step', 'fwp' ),
'notes' => 'The amount of increase between intervals',
'default' => 1
]
];
}
}

View File

@@ -0,0 +1,226 @@
<?php
class FacetWP_Facet_Sort extends FacetWP_Facet
{
public $sort_options = [];
function __construct() {
$this->label = __( 'Sort', 'fwp' );
$this->fields = [ 'sort_default_label', 'sort_options' ];
add_filter( 'facetwp_filtered_query_args', [ $this, 'apply_sort' ], 1, 2 );
add_filter( 'facetwp_render_output', [ $this, 'render_sort_feature' ], 1, 2 );
}
/**
* Render the sort facet
*/
function render( $params ) {
$facet = $this->parse_sort_facet( $params['facet'] );
$selected_values = (array) $params['selected_values'];
$label = facetwp_i18n( $facet['default_label'] );
$output = '<option value="">' . esc_attr( $label ) . '</option>';
foreach ( $facet['sort_options'] as $key => $choice ) {
$label = facetwp_i18n( $choice['label'] );
$selected = in_array( $key, $selected_values ) ? ' selected' : '';
$output .= '<option value="' . esc_attr( $key ) . '"' . $selected . '>' . esc_attr( $label ) . '</option>';
}
return '<select>' . $output . '</select>';
}
/**
* Sort facets don't narrow results
*/
function filter_posts( $params ) {
return 'continue';
}
/**
* Register admin settings
*/
function register_fields() {
return [
'sort_default_label' => [
'type' => 'alias',
'items' => [
'default_label' => [
'label' => __( 'Default label', 'fwp' ),
'notes' => 'The sort box placeholder text',
'default' => __( 'Sort by', 'fwp' )
]
]
],
'sort_options' => [
'label' => __( 'Sort options', 'fwp' ),
'notes' => 'Define the choices that appear in the sort box',
'html' => '<sort-options :facet="facet"></sort-options><input type="hidden" class="facet-sort-options" value="[]" />'
]
];
}
/**
* Convert a sort facet's sort options into WP_Query arguments
* @since 4.0.8
*/
function parse_sort_facet( $facet ) {
$sort_options = [];
foreach ( $facet['sort_options'] as $row ) {
$parsed = FWP()->builder->parse_query_obj([ 'orderby' => $row['orderby'] ]);
$sort_options[ $row['name'] ] = [
'label' => $row['label'],
'query_args' => array_intersect_key( $parsed, [
'meta_query' => true,
'orderby' => true
])
];
}
$sort_options = apply_filters( 'facetwp_facet_sort_options', $sort_options, [
'facet' => $facet,
'template_name' => FWP()->facet->template['name']
]);
$facet['sort_options'] = $sort_options;
return $facet;
}
/**
* Handle both sort facets and the (old) sort feature
* @since 4.0.6
*/
function apply_sort( $query_args, $class ) {
foreach ( $class->facets as $facet ) {
if ( 'sort' == $facet['type'] ) {
$sort_facet = $this->parse_sort_facet( $facet );
break;
}
}
// Support the (old) sort feature
$sort_value = 'default';
$this->sort_options = $this->get_sort_options();
if ( ! empty( $class->ajax_params['extras']['sort'] ) ) {
$sort_value = $class->ajax_params['extras']['sort'];
if ( ! empty( $this->sort_options[ $sort_value ] ) ) {
$args = $this->sort_options[ $sort_value ]['query_args'];
$query_args = array_merge( $query_args, $args );
}
}
// Preserve relevancy sort
$use_relevancy = apply_filters( 'facetwp_use_search_relevancy', true, $class );
$is_default_sort = ( 'default' == $sort_value && empty( $class->http_params['get']['orderby'] ) );
if ( $class->is_search && $use_relevancy && $is_default_sort && FWP()->is_filtered ) {
$query_args['orderby'] = 'post__in';
}
// Support the (new) sort facet
if ( ! empty( $sort_facet['selected_values'] ) ) {
$chosen = $sort_facet['selected_values'][0];
$sort_options = $sort_facet['sort_options'];
if ( isset( $sort_options[ $chosen ] ) ) {
$qa = $sort_options[ $chosen ]['query_args'];
if ( isset( $qa['meta_query'] ) ) {
$meta_query = $query_args['meta_query'] ?? [];
$query_args['meta_query'] = array_merge( $meta_query, $qa['meta_query'] );
}
$query_args['orderby'] = $qa['orderby'];
}
}
return $query_args;
}
/**
* Generate choices for the (old) sort feature
* @since 4.0.6
*/
function get_sort_options() {
$options = [
'default' => [
'label' => __( 'Sort by', 'fwp-front' ),
'query_args' => []
],
'title_asc' => [
'label' => __( 'Title (A-Z)', 'fwp-front' ),
'query_args' => [
'orderby' => 'title',
'order' => 'ASC',
]
],
'title_desc' => [
'label' => __( 'Title (Z-A)', 'fwp-front' ),
'query_args' => [
'orderby' => 'title',
'order' => 'DESC',
]
],
'date_desc' => [
'label' => __( 'Date (Newest)', 'fwp-front' ),
'query_args' => [
'orderby' => 'date',
'order' => 'DESC',
]
],
'date_asc' => [
'label' => __( 'Date (Oldest)', 'fwp-front' ),
'query_args' => [
'orderby' => 'date',
'order' => 'ASC',
]
]
];
return apply_filters( 'facetwp_sort_options', $options, [
'template_name' => FWP()->facet->template['name'],
] );
}
/**
* Render the (old) sort feature
* @since 4.0.6
*/
function render_sort_feature( $output, $params ) {
$has_sort = isset( $params['extras']['sort'] );
$has_choices = isset( $this->sort_options );
if ( 0 == $params['soft_refresh'] && $has_sort && $has_choices ) {
$html = '';
foreach ( $this->sort_options as $key => $atts ) {
$html .= '<option value="' . $key . '">' . $atts['label'] . '</option>';
}
$html = '<select class="facetwp-sort-select">' . $html . '</select>';
$output['sort'] = apply_filters( 'facetwp_sort_html', $html, [
'sort_options' => $this->sort_options,
'template_name' => FWP()->facet->template['name'],
]);
}
return $output;
}
}

View File

@@ -0,0 +1,43 @@
<?php
// DO NOT MODIFY THIS FILE!
// Use your theme's functions.php instead
/**
* An alternate to using do_shortcode()
*
* facetwp_display( 'pager' );
* facetwp_display( 'template', 'cars' );
* facetwp_display( 'template', 'cars', [ 'static' => true ] );
*
* @since 1.7.5
*/
function facetwp_display() {
$args = array_replace( [ 'pager', true, [] ], func_get_args() );
$atts = (array) $args[2];
$atts[ $args[0] ] = $args[1];
return FWP()->display->shortcode( $atts );
}
/**
* Allow for translation of dynamic strings
* @since 2.1
*/
function facetwp_i18n( $string ) {
return apply_filters( 'facetwp_i18n', $string );
}
/**
* Support SQL modifications
* @since 2.7
*/
function facetwp_sql( $sql, $facet ) {
global $wpdb;
$sql = apply_filters( 'facetwp_wpdb_sql', $sql, $facet );
return apply_filters( 'facetwp_wpdb_get_col', $wpdb->get_col( $sql ), $sql, $facet );
}

View File

@@ -0,0 +1,517 @@
<?php
class FacetWP_Integration_ACF
{
public $fields = [];
public $parent_type_lookup = [];
public $repeater_row;
function __construct() {
add_filter( 'facetwp_facet_sources', [ $this, 'facet_sources' ] );
add_filter( 'facetwp_facet_orderby', [ $this, 'facet_orderby' ], 10, 2 );
add_filter( 'facetwp_indexer_query_args', [ $this, 'lookup_acf_fields' ] );
add_filter( 'facetwp_indexer_post_facet', [ $this, 'index_acf_values' ], 1, 2 );
add_filter( 'facetwp_acf_display_value', [ $this, 'index_source_other' ], 1, 2 );
add_filter( 'facetwp_builder_item_value', [ $this, 'layout_builder_values' ], 999, 2 );
}
/**
* Add ACF fields to the Data Sources dropdown
*/
function facet_sources( $sources ) {
$fields = $this->get_fields();
$choices = [];
foreach ( $fields as $field ) {
$field_id = $field['hierarchy'];
$field_name = $field['name'];
$field_label = '[' . $field['group_title'] . '] ' . $field['parents'] . $field['label'];
$choices[ "acf/$field_id" ] = $field_label;
// remove "hidden" ACF fields
unset( $sources['custom_fields']['choices'][ "cf/_$field_name" ] );
}
if ( ! empty( $choices ) ) {
$sources['acf'] = [
'label' => 'ACF',
'choices' => $choices,
'weight' => 5
];
}
return $sources;
}
/**
* If the facet "Sort by" value is "Term order", then preserve
* the custom order of certain ACF fields (checkboxes, radio, etc.)
*/
function facet_orderby( $orderby, $facet ) {
if ( isset( $facet['source'] ) && isset( $facet['orderby'] ) ) {
if ( 0 === strpos( $facet['source'], 'acf/' ) && 'term_order' == $facet['orderby'] ) {
$source_parts = explode( '/', $facet['source'] );
$field_id = array_pop( $source_parts );
$field_object = get_field_object( $field_id );
if ( ! empty( $field_object['choices'] ) ) {
$choices = $field_object['choices'];
$choices = implode( "','", esc_sql( $choices ) );
$orderby = "FIELD(f.facet_display_value, '$choices')";
}
}
}
return $orderby;
}
/**
* Index ACF field data
*/
function index_acf_values( $return, $params ) {
$defaults = $params['defaults'];
$facet = $params['facet'];
if ( isset( $facet['source'] ) && 'acf/' == substr( $facet['source'], 0, 4 ) ) {
$hierarchy = explode( '/', substr( $facet['source'], 4 ) );
// support "User Post Type" plugin
$object_id = apply_filters( 'facetwp_acf_object_id', $defaults['post_id'] );
// get values (for sub-fields, use the parent repeater)
$value = get_field( $hierarchy[0], $object_id, false );
// handle repeater values
if ( 1 < count( $hierarchy ) ) {
$parent_field_key = array_shift( $hierarchy );
$value = $this->process_field_value( $value, $hierarchy, $parent_field_key );
// get the sub-field properties
$sub_field = get_field_object( $hierarchy[0], $object_id, false, false );
foreach ( $value as $key => $val ) {
$this->repeater_row = $key;
$rows = $this->get_values_to_index( $val, $sub_field, $defaults );
$this->index_field_values( $rows );
}
}
else {
// get the field properties
$field = get_field_object( $hierarchy[0], $object_id, false, false );
// index values
$rows = $this->get_values_to_index( $value, $field, $defaults );
$this->index_field_values( $rows );
}
return true;
}
return $return;
}
/**
* Hijack the "facetwp_indexer_query_args" hook to lookup the fields once
*/
function lookup_acf_fields( $args ) {
$this->get_fields();
return $args;
}
/**
* Grab all ACF fields
*/
function get_fields() {
add_action( 'pre_get_posts', [ $this, 'disable_wpml' ] );
$field_groups = acf_get_field_groups();
remove_action( 'pre_get_posts', [ $this, 'disable_wpml' ] );
foreach ( $field_groups as $field_group ) {
$fields = acf_get_fields( $field_group );
if ( ! empty( $fields ) ) {
$this->flatten_fields( $fields, $field_group );
}
}
return $this->fields;
}
/**
* We need to get field groups in ALL languages
*/
function disable_wpml( $query ) {
$query->set( 'suppress_filters', true );
$query->set( 'lang', '' );
}
/**
* Extract field values from the repeater array
*/
function process_field_value( $value, $hierarchy, $parent_field_key ) {
$temp_val = [];
// prevent PHP8 fatal error on invalid lookup field
$parent_field_type = $this->parent_type_lookup[ $parent_field_key ] ?? 'none';
if ( ! is_array( $value ) || 'none' == $parent_field_type ) {
return $temp_val;
}
// reduce the hierarchy array
$field_key = array_shift( $hierarchy );
// group
if ( 'group' == $parent_field_type ) {
if ( 0 == count( $hierarchy ) ) {
$temp_val[] = $value[ $field_key ];
}
else {
return $this->process_field_value( $value[ $field_key ], $hierarchy, $field_key );
}
}
// repeater
else {
if ( 0 == count( $hierarchy ) ) {
foreach ( $value as $val ) {
$temp_val[] = $val[ $field_key ];
}
}
else {
foreach ( $value as $outer ) {
if ( isset( $outer[ $field_key ] ) ) {
foreach ( $outer[ $field_key ] as $inner ) {
$temp_val[] = $inner;
}
}
}
return $this->process_field_value( $temp_val, $hierarchy, $field_key );
}
}
return $temp_val;
}
/**
* Get an array of $params arrays
* Useful for indexing and grabbing values for the Layout Builder
* @since 3.4.0
*/
function get_values_to_index( $value, $field, $params ) {
$value = maybe_unserialize( $value );
$type = $field['type'];
$output = [];
// checkboxes
if ( 'checkbox' == $type || 'select' == $type || 'radio' == $type ) {
if ( false !== $value ) {
foreach ( (array) $value as $val ) {
$display_value = isset( $field['choices'][ $val ] ) ?
$field['choices'][ $val ] :
$val;
$params['facet_value'] = $val;
$params['facet_display_value'] = $display_value;
$output[] = $params;
}
}
}
// relationship
elseif ( 'relationship' == $type || 'post_object' == $type || 'page_link' == $type ) {
if ( false !== $value ) {
foreach ( (array) $value as $val ) {
// does the post exist?
if ( false !== get_post_type( $val ) ) {
$params['facet_value'] = $val;
$params['facet_display_value'] = get_the_title( $val );
$output[] = $params;
}
}
}
}
// user
elseif ( 'user' == $type ) {
if ( false !== $value ) {
foreach ( (array) $value as $val ) {
$user = get_user_by( 'id', $val );
// does the user exist?
if ( false !== $user ) {
$params['facet_value'] = $val;
$params['facet_display_value'] = $user->display_name;
$output[] = $params;
}
}
}
}
// taxonomy
elseif ( 'taxonomy' == $type ) {
if ( ! empty( $value ) ) {
foreach ( (array) $value as $val ) {
global $wpdb;
$term_id = (int) $val;
$term = $wpdb->get_row( "SELECT name, slug FROM {$wpdb->terms} WHERE term_id = '$term_id' LIMIT 1" );
// does the term exist?
if ( null !== $term ) {
$params['facet_value'] = $term->slug;
$params['facet_display_value'] = $term->name;
$params['term_id'] = $term_id;
$output[] = $params;
}
}
}
}
// date_picker
elseif ( 'date_picker' == $type ) {
$formatted = $this->format_date( $value );
$params['facet_value'] = $formatted;
$params['facet_display_value'] = apply_filters( 'facetwp_acf_display_value', $formatted, $params );
$output[] = $params;
}
// true_false
elseif ( 'true_false' == $type ) {
$display_value = ( 0 < (int) $value ) ? __( 'Yes', 'fwp-front' ) : __( 'No', 'fwp-front' );
$params['facet_value'] = $value;
$params['facet_display_value'] = $display_value;
$output[] = $params;
}
// google_map
elseif ( 'google_map' == $type ) {
if ( isset( $value['lat'] ) && isset( $value['lng'] ) ) {
$params['facet_value'] = $value['lat'];
$params['facet_display_value'] = $value['lng'];
$params['place_details'] = $value;
$output[] = $params;
}
}
// text
else {
$params['facet_value'] = $value;
$params['facet_display_value'] = apply_filters( 'facetwp_acf_display_value', $value, $params );
$output[] = $params;
}
return $output;
}
/**
* Index values
*/
function index_field_values( $rows ) {
foreach ( $rows as $params ) {
FWP()->indexer->index_row( $params );
}
}
/**
* Handle "source_other" setting
*/
function index_source_other( $value, $params ) {
if ( ! empty( $params['facet_name'] ) ) {
$facet = FWP()->helper->get_facet_by_name( $params['facet_name'] );
if ( ! empty( $facet['source_other'] ) ) {
$hierarchy = explode( '/', substr( $facet['source_other'], 4 ) );
// support "User Post Type" plugin
$object_id = apply_filters( 'facetwp_acf_object_id', $params['post_id'] );
// get the value
$value = get_field( $hierarchy[0], $object_id, false );
// handle repeater values
if ( 1 < count( $hierarchy ) ) {
$parent_field_key = array_shift( $hierarchy );
$value = $this->process_field_value( $value, $hierarchy, $parent_field_key );
$value = $value[ $this->repeater_row ];
}
}
if ( 'date_range' == $facet['type'] ) {
$value = $this->format_date( $value );
}
}
return $value;
}
/**
* Format dates in YYYY-MM-DD
*/
function format_date( $str ) {
if ( 8 == strlen( $str ) && ctype_digit( $str ) ) {
$str = substr( $str, 0, 4 ) . '-' . substr( $str, 4, 2 ) . '-' . substr( $str, 6, 2 );
}
return $str;
}
/**
* Generates a flat array of fields within a specific field group
*/
function flatten_fields( $fields, $field_group, $hierarchy = '', $parents = '' ) {
foreach ( $fields as $field ) {
// append the hierarchy string
$new_hierarchy = $hierarchy . '/' . $field['key'];
// loop again for repeater or group fields
if ( 'repeater' == $field['type'] || 'group' == $field['type'] ) {
$new_parents = $parents . $field['label'] . ' &rarr; ';
$this->parent_type_lookup[ $field['key'] ] = $field['type'];
$this->flatten_fields( $field['sub_fields'], $field_group, $new_hierarchy, $new_parents );
}
else {
$this->fields[] = [
'key' => $field['key'],
'name' => $field['name'],
'label' => $field['label'],
'hierarchy' => trim( $new_hierarchy, '/' ),
'parents' => $parents,
'group_title' => $field_group['title'],
];
}
}
}
/**
* Get the field value (support User Post Type)
* @since 3.4.1
*/
function get_field( $source, $post_id ) {
$hierarchy = explode( '/', substr( $source, 4 ) );
$object_id = apply_filters( 'facetwp_acf_object_id', $post_id );
return get_field( $hierarchy[0], $object_id );
}
/**
* Fallback values for the layout builder
* @since 3.4.0
*
* ACF return formats:
* [image, file] = array, url, id
* [select, checkbox, radio, button_group] = value, label, array (both)
* [post_object, relationship, taxonomy] = object, id
* [user] = array, object, id
* [link] = array, url
*/
function layout_builder_values( $value, $item ) {
global $post;
// exit if not an object or array
if ( is_scalar( $value ) || is_null( $value ) ) {
return $value;
}
$hierarchy = explode( '/', substr( $item['source'], 4 ) );
// support "User Post Type" plugin
$object_id = apply_filters( 'facetwp_acf_object_id', $post->ID );
// get the field properties
$field = get_field_object( $hierarchy[0], $object_id, false, false );
$type = $field['type'];
$format = $field['return_format'] ?? '';
$is_multiple = (bool) ( $field['multiple'] ?? false );
if ( ( 'post_object' == $type || 'relationship' == $type ) && 'object' == $format ) {
$output = [];
$value = is_array( $value ) ? $value : [ $value ];
foreach ( $value as $val ) {
$output[] = '<a href="' . get_permalink( $val->ID ) . '">' . esc_html( $val->post_title ) . '</a>';
}
$value = $output;
}
if ( 'taxonomy' == $type && 'object' == $format ) {
$output = [];
foreach ( $value as $val ) {
$output[] = $val->name;
}
$value = $output;
}
if ( ( 'select' == $type || 'checkbox' == $type || 'radio' == $type || 'button_group' == $type ) && 'array' == $format ) {
$value = $value['label'] ?? wp_list_pluck( $value, 'label' );
}
if ( ( 'image' == $type || 'gallery' == $type ) && 'array' == $format ) {
$value = ( 'image' == $type ) ? [ $value ] : $value;
foreach ( $value as $val ) {
$value = '<img src="' . esc_url( $val['url'] ) . '" title="' . esc_attr( $val['title'] ) . '" alt="' . esc_attr( $val['alt'] ) . '" />';
}
}
if ( 'file' == $type && 'array' == $format ) {
$value = '<a href="' . esc_url( $value['url'] ) . '">' . esc_html( $value['filename'] ) . '</a> (' . size_format( $value['filesize'], 1 ) . ')';
}
if ( 'link' == $type && 'array' == $format ) {
$value = '<a href="' . esc_url( $value['url'] ) . '" target="' . esc_attr( $value['target'] ) . '">' . esc_html( $value['title'] ) . '</a>';
}
if ( 'google_map' == $type ) {
$value = '<a href="https://www.google.com/maps/?q=' . $value['lat'] . ',' . $value['lng'] . '" target="_blank">' . esc_html( $value['address'] ) . '</a>';
}
if ( 'user' == $type && ( 'object' == $format || 'array' == $format ) ) {
$output = [];
$value = $is_multiple ? $value : [ $value ];
foreach ( $value as $val ) {
if ( 'object' == $format ) {
$output[] = $val->display_name;
}
elseif ( 'array' == $format ) {
$output[] = $val['display_name'];
}
}
$value = $output;
}
return $value;
}
}
if ( function_exists( 'acf' ) && version_compare( acf()->settings['version'], '5.0', '>=' ) ) {
FWP()->acf = new FacetWP_Integration_ACF();
}

View File

@@ -0,0 +1,6 @@
(function($) {
$().on('facetwp-loaded', function() {
$('.edd-no-js').addClass('facetwp-hidden');
$('a.edd-add-to-cart').addClass('edd-has-js');
});
})(fUtil);

View File

@@ -0,0 +1,51 @@
<?php
class FacetWP_Integration_EDD
{
function __construct() {
add_filter( 'facetwp_facet_sources', [ $this, 'exclude_data_sources' ] );
add_filter( 'edd_downloads_query', [ $this, 'edd_downloads_query' ] );
add_action( 'facetwp_assets', [ $this, 'assets' ] );
}
/**
* Trigger some EDD code on facetwp-loaded
* @since 2.0.4
*/
function assets( $assets ) {
$assets['edd.js'] = FACETWP_URL . '/includes/integrations/edd/edd.js';
return $assets;
}
/**
* Help FacetWP auto-detect the [downloads] shortcode
* @since 2.0.4
*/
function edd_downloads_query( $query ) {
$query['facetwp'] = true;
return $query;
}
/**
* Exclude specific EDD custom fields
* @since 2.4
*/
function exclude_data_sources( $sources ) {
foreach ( $sources['custom_fields']['choices'] as $key => $val ) {
if ( 0 === strpos( $val, '_edd_' ) ) {
unset( $sources['custom_fields']['choices'][ $key ] );
}
}
return $sources;
}
}
if ( is_plugin_active( 'easy-digital-downloads/easy-digital-downloads.php' ) ) {
new FacetWP_Integration_EDD();
}

View File

@@ -0,0 +1,215 @@
<?php
class FacetWP_Integration_SearchWP
{
public $keywords;
public $swp_query;
public $first_run = true;
function __construct() {
add_filter( 'facetwp_is_main_query', [ $this, 'is_main_query' ], 10, 2 );
add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ], 1000, 2 );
add_filter( 'posts_pre_query', [ $this, 'posts_pre_query' ], 10, 2 );
add_filter( 'posts_results', [ $this, 'posts_results' ], 10, 2 );
add_filter( 'facetwp_facet_filter_posts', [ $this, 'search_facet' ], 10, 2 );
add_filter( 'facetwp_facet_search_engines', [ $this, 'search_engines' ] );
}
/**
* Run and cache the \SWP_Query
*/
function is_main_query( $is_main_query, $query ) {
if ( $is_main_query && $query->is_search() && ! empty( $query->get( 's' ) ) ) {
$args = stripslashes_deep( $this->get_valid_args( $query ) );
$this->keywords = $args['s'];
$this->swp_query = $this->run_query( $args );
$query->set( 'using_searchwp', true );
$query->set( 'searchwp', false );
}
return $is_main_query;
}
/**
* Whitelist supported SWP_Query arguments
*
* @link https://searchwp.com/documentation/classes/swp_query/#arguments
*/
function get_valid_args( $query ) {
$output = [];
$valid = [
's', 'engine', 'post__in', 'post__not_in', 'post_type', 'post_status',
'tax_query', 'meta_query', 'date_query', 'order', 'orderby'
];
foreach ( $valid as $arg ) {
$val = $query->get( $arg );
if ( ! empty( $val ) ) {
$output[ $arg ] = $val;
}
}
return $output;
}
/**
* Modify FacetWP's render() query to use SearchWP's results while bypassing
* WP core search. We're using this additional query to support custom query
* modifications, such as for FacetWP's sort box.
*
* The hook priority (1000) is important because this needs to run after
* FacetWP_Request->update_query_vars()
*/
function pre_get_posts( $query ) {
if ( true === $query->get( 'using_searchwp' ) ) {
if ( true === $query->get( 'facetwp' ) && ! $this->first_run ) {
$query->set( 's', '' );
$post_ids = FWP()->filtered_post_ids;
$post_ids = empty( $post_ids ) ? [ 0 ] : $post_ids;
$query->set( 'post__in', $post_ids );
if ( '' === $query->get( 'post_type' ) ) {
$query->set( 'post_type', 'any' );
$query->set( 'post_status', 'any' );
}
if ( '' === $query->get( 'orderby' ) ) {
$query->set( 'orderby', 'post__in' );
}
}
}
}
/**
* If [facetwp => false] then it's the get_filtered_post_ids() query. Return
* the raw SearchWP results to prevent the additional query.
*
* If [facetwp => true] and [first_run => true] then it's the main WP query. Return
* a non-null value to kill the query, since we don't use the results.
*
* If [facetwp => true] and [first_run => false] then it's the FacetWP render() query.
*/
function posts_pre_query( $posts, $query ) {
if ( true === $query->get( 'using_searchwp' ) ) {
if ( true === $query->get( 'facetwp' ) ) {
$query->set( 's', $this->keywords );
// kill the main WP query
if ( $this->first_run ) {
$this->first_run = false;
$page = max( $query->get( 'paged' ), 1 );
$per_page = (int) $query->get( 'posts_per_page', get_option( 'posts_per_page' ) );
$query->found_posts = count( FWP()->filtered_post_ids );
$query->max_num_pages = ( 0 < $per_page ) ? ceil( $query->found_posts / $per_page ) : 0;
return [];
}
}
else {
return $this->swp_query->posts;
}
}
return $posts;
}
/**
* Apply highlighting if available
*/
function posts_results( $posts, $query ) {
if ( true === $query->get( 'using_searchwp' ) ) {
if ( true === $query->get( 'facetwp' ) && ! $this->first_run ) {
// SearchWP 4.1+
if ( isset( $this->swp_query->query ) ) {
foreach ( $posts as $index => $post ) {
$source = \SearchWP\Utils::get_post_type_source_name( $post->post_type );
$entry = new \SearchWP\Entry( $source, $post->ID, false );
$posts[ $index ] = $entry->native( $this->swp_query->query );
}
}
}
}
return $posts;
}
/**
* For search facets, run \SWP_Query and return matching post IDs
*/
function search_facet( $return, $params ) {
$facet = $params['facet'];
$selected_values = $params['selected_values'];
$selected_values = is_array( $selected_values ) ? $selected_values[0] : $selected_values;
$engine = $facet['search_engine'] ?? '';
if ( 'search' == $facet['type'] && 0 === strpos( $engine, 'swp_' ) ) {
$return = [];
if ( empty( $selected_values ) ) {
$return = 'continue';
}
elseif ( ! empty( FWP()->unfiltered_post_ids ) ) {
$swp_query = $this->run_query([
's' => $selected_values,
'engine' => substr( $engine, 4 ),
'post__in' => FWP()->unfiltered_post_ids
]);
$return = $swp_query->posts;
}
}
return $return;
}
/**
* Run a search and return the \SWP_Query object
*/
function run_query( $args ) {
$overrides = [ 'posts_per_page' => 200, 'fields' => 'ids', 'facetwp' => true ];
$args = array_merge( $args, $overrides );
return new \SWP_Query( $args );
}
/**
* Add engines to the search facet
*/
function search_engines( $engines ) {
if ( version_compare( SEARCHWP_VERSION, '4.0', '>=' ) ) {
$settings = get_option( SEARCHWP_PREFIX . 'engines' );
foreach ( $settings as $key => $info ) {
$engines[ 'swp_' . $key ] = 'SearchWP - ' . $info['label'];
}
}
else {
$settings = get_option( SEARCHWP_PREFIX . 'settings' );
foreach ( $settings['engines'] as $key => $info ) {
$label = $info['searchwp_engine_label'] ?? __( 'Default', 'fwp' );
$engines[ 'swp_' . $key ] = 'SearchWP - ' . $label;
}
}
return $engines;
}
}
if ( defined( 'SEARCHWP_VERSION' ) ) {
new FacetWP_Integration_SearchWP();
}

View File

@@ -0,0 +1,85 @@
<?php
class FacetWP_Integration_WooCommerce_Taxonomy
{
function __construct() {
add_action( 'woocommerce_after_template_part', [ $this, 'add_loop_tag'] );
add_filter( 'get_terms', [ $this, 'adjust_term_counts' ], 10, 3 );
add_filter( 'term_link', [ $this, 'append_url_vars' ], 10, 3 );
}
/**
* Support category listings (Shop page display: Show categories)
* @since 3.3.10
*/
function add_loop_tag( $template_name ) {
if ( 'loop/loop-start.php' == $template_name ) {
echo "<!--fwp-loop-->\n";
}
}
/**
* Adjust the category listing counts when facets are selected
* @since 3.3.10
*/
function adjust_term_counts( $terms, $taxonomy, $query_vars ) {
if ( FWP()->request->is_refresh || ! empty( FWP()->request->url_vars ) ) {
if ( 'product_cat' == reset( $taxonomy ) ) {
global $wpdb, $wp_query;
$sql = $wp_query->request;
if ( false !== ( $pos = strpos( $sql, ' ORDER BY' ) ) ) {
$sql = substr( $sql, 0, $pos );
}
$post_ids = $wpdb->get_col( $sql );
if ( ! empty( $post_ids ) ) {
$term_counts = [];
$post_ids_str = implode( ',', $post_ids );
$query = "
SELECT term_id, COUNT(term_id) AS term_count
FROM {$wpdb->prefix}facetwp_index
WHERE post_id IN ($post_ids_str)
GROUP BY term_id";
$results = $wpdb->get_results( $query );
foreach ( $results as $row ) {
$term_counts[ $row->term_id ] = (int) $row->term_count;
}
foreach ( $terms as $term ) {
$term->count = $term_counts[ $term->term_id ] ?? 0;
}
}
}
}
return $terms;
}
/**
* Append facet URL variables to the category archive links
* @since 3.3.10
*/
function append_url_vars( $term_link, $term, $taxonomy ) {
if ( 'product_cat' == $taxonomy ) {
$query_string = filter_var( $_SERVER['QUERY_STRING'], FILTER_SANITIZE_URL );
if ( ! empty( $query_string ) ) {
$prefix = ( false !== strpos( $query_string, '?' ) ) ? '&' : '?';
$term_link .= $prefix . $query_string;
}
}
return $term_link;
}
}
new FacetWP_Integration_WooCommerce_Taxonomy();

View File

@@ -0,0 +1,34 @@
(function($) {
$().on('facetwp-refresh', function() {
if (! FWP.loaded) {
setup_woocommerce();
}
});
function setup_woocommerce() {
// Intercept WooCommerce pagination
$().on('click', '.woocommerce-pagination a', function(e) {
e.preventDefault();
var matches = $(this).attr('href').match(/\/page\/(\d+)/);
if (null !== matches) {
FWP.paged = parseInt(matches[1]);
FWP.soft_refresh = true;
FWP.refresh();
}
});
// Disable sort handler
$('.woocommerce-ordering').attr('onsubmit', 'event.preventDefault()');
// Intercept WooCommerce sorting
$().on('change', '.woocommerce-ordering .orderby', function(e) {
var qs = new URLSearchParams(window.location.search);
qs.set('orderby', $(this).val());
history.pushState(null, null, window.location.pathname + '?' + qs.toString());
FWP.soft_refresh = true;
FWP.refresh();
});
}
})(fUtil);

View File

@@ -0,0 +1,570 @@
<?php
class FacetWP_Integration_WooCommerce
{
public $cache = [];
public $lookup = [];
public $storage = [];
public $variations = [];
public $post_clauses = false;
function __construct() {
add_action( 'facetwp_assets', [ $this, 'assets' ] );
add_filter( 'facetwp_facet_sources', [ $this, 'facet_sources' ] );
add_filter( 'facetwp_facet_display_value', [ $this, 'translate_hardcoded_choices' ], 10, 2 );
add_filter( 'facetwp_indexer_post_facet', [ $this, 'index_woo_values' ], 10, 2 );
// Support WooCommerce product variations
$is_enabled = ( 'yes' === FWP()->helper->get_setting( 'wc_enable_variations', 'no' ) );
if ( apply_filters( 'facetwp_enable_product_variations', $is_enabled ) ) {
add_filter( 'facetwp_indexer_post_facet_defaults', [ $this, 'force_taxonomy' ], 10, 2 );
add_filter( 'facetwp_indexer_query_args', [ $this, 'index_variations' ] );
add_filter( 'facetwp_index_row', [ $this, 'attribute_variations' ], 1 );
add_filter( 'facetwp_wpdb_sql', [ $this, 'wpdb_sql' ], 10, 2 );
add_filter( 'facetwp_wpdb_get_col', [ $this, 'wpdb_get_col' ], 10, 3 );
add_filter( 'facetwp_filtered_post_ids', [ $this, 'process_variations' ] );
add_filter( 'facetwp_facet_where', [ $this, 'facet_where' ], 10, 2 );
}
// Preserve the WooCommerce sort
add_filter( 'posts_clauses', [ $this, 'preserve_sort' ], 20, 2 );
// Prevent WooCommerce from redirecting to a single result page
add_filter( 'woocommerce_redirect_single_search_result', [ $this, 'redirect_single_search_result' ] );
// Prevent WooCommerce sort (posts_clauses) when doing FacetWP sort
add_filter( 'woocommerce_default_catalog_orderby', [ $this, 'default_catalog_orderby' ] );
// Dynamic counts when Shop Page Display = "Categories" or "Both"
if ( apply_filters( 'facetwp_woocommerce_support_categories_display', false ) ) {
include( FACETWP_DIR . '/includes/integrations/woocommerce/taxonomy.php' );
}
}
/**
* Run WooCommerce handlers on facetwp-refresh
* @since 2.0.9
*/
function assets( $assets ) {
$assets['woocommerce.js'] = FACETWP_URL . '/includes/integrations/woocommerce/woocommerce.js';
return $assets;
}
/**
* Add WooCommerce-specific data sources
* @since 2.1.4
*/
function facet_sources( $sources ) {
$sources['woocommerce'] = [
'label' => __( 'WooCommerce', 'fwp' ),
'choices' => [
'woo/price' => __( 'Price' ),
'woo/sale_price' => __( 'Sale Price' ),
'woo/regular_price' => __( 'Regular Price' ),
'woo/average_rating' => __( 'Average Rating' ),
'woo/stock_status' => __( 'Stock Status' ),
'woo/on_sale' => __( 'On Sale' ),
'woo/featured' => __( 'Featured' ),
'woo/product_type' => __( 'Product Type' ),
],
'weight' => 5
];
// Move WC taxonomy choices
foreach ( $sources['taxonomies']['choices'] as $key => $label ) {
if ( 'tax/product_cat' == $key || 'tax/product_tag' == $key || 0 === strpos( $key, 'tax/pa_' ) ) {
$sources['woocommerce']['choices'][ $key ] = $label;
unset( $sources['taxonomies']['choices'][ $key ] );
}
}
return $sources;
}
/**
* Attributes for WC product variations are stored in postmeta
* @since 2.7.2
*/
function force_taxonomy( $defaults, $params ) {
if ( 0 === strpos( $defaults['facet_source'], 'tax/pa_' ) ) {
$post_id = (int) $defaults['post_id'];
if ( 'product_variation' == get_post_type( $post_id ) ) {
$defaults['facet_source'] = str_replace( 'tax/', 'cf/attribute_', $defaults['facet_source'] );
}
}
return $defaults;
}
/**
* Index product variations
* @since 2.7
*/
function index_variations( $args ) {
// Saving a single product
if ( ! empty( $args['p'] ) ) {
$post_id = (int) $args['p'];
if ( 'product' == get_post_type( $post_id ) ) {
if ( 'variable' == $this->get_product_type( $post_id ) ) {
$product = wc_get_product( $post_id );
if ( false !== $product ) {
$children = $product->get_children();
$args['post_type'] = [ 'product', 'product_variation' ];
$args['post__in'] = $children;
$args['post__in'][] = $post_id;
$args['posts_per_page'] = -1;
unset( $args['p'] );
}
}
}
}
// Force product variations to piggyback products
else {
$pt = (array) $args['post_type'];
if ( in_array( 'any', $pt ) ) {
$pt = get_post_types();
}
if ( in_array( 'product', $pt ) ) {
$pt[] = 'product_variation';
}
$args['post_type'] = $pt;
}
return $args;
}
/**
* When indexing product variations, attribute its parent product
* @since 2.7
*/
function attribute_variations( $params ) {
$post_id = (int) $params['post_id'];
// Set variation_id for all posts
$params['variation_id'] = $post_id;
if ( 'product_variation' == get_post_type( $post_id ) ) {
$params['post_id'] = wp_get_post_parent_id( $post_id );
// Lookup the term name for variation values
if ( 0 === strpos( $params['facet_source'], 'cf/attribute_pa_' ) ) {
$taxonomy = str_replace( 'cf/attribute_', '', $params['facet_source'] );
$term = get_term_by( 'slug', $params['facet_value'], $taxonomy );
if ( false !== $term ) {
$params['term_id'] = $term->term_id;
$params['facet_display_value'] = $term->name;
}
}
}
return $params;
}
/**
* Hijack filter_posts() to grab variation IDs
* @since 2.7
*/
function wpdb_sql( $sql, $facet ) {
$sql = str_replace(
'DISTINCT post_id',
'DISTINCT post_id, GROUP_CONCAT(variation_id) AS variation_ids',
$sql
);
$sql .= ' GROUP BY post_id';
return $sql;
}
/**
* Store a facet's variation IDs
* @since 2.7
*/
function wpdb_get_col( $result, $sql, $facet ) {
global $wpdb;
$facet_name = $facet['name'];
$post_ids = $wpdb->get_col( $sql, 0 ); // arrays of product IDs
$variations = $wpdb->get_col( $sql, 1 ); // variation IDs as arrays of comma-separated strings
foreach ( $post_ids as $index => $post_id ) {
$variations_array = explode( ',', $variations[ $index ] );
$type = in_array( $post_id, $variations_array ) ? 'products' : 'variations';
if ( isset( $this->cache[ $facet_name ][ $type ] ) ) {
foreach ( $variations_array as $id ) {
$this->cache[ $facet_name ][ $type ][] = $id;
}
}
else {
$this->cache[ $facet_name ][ $type ] = $variations_array;
}
}
return $result;
}
/**
* We need lookup arrays for both products and variations
* @since 2.7.1
*/
function generate_lookup_array( $post_ids ) {
global $wpdb;
$output = [];
if ( ! empty( $post_ids ) ) {
$sql = "
SELECT DISTINCT post_id, variation_id
FROM {$wpdb->prefix}facetwp_index
WHERE post_id IN (" . implode( ',', $post_ids ) . ")";
$results = $wpdb->get_results( $sql );
foreach ( $results as $result ) {
$output['get_variations'][ $result->post_id ][] = $result->variation_id;
$output['get_product'][ $result->variation_id ] = $result->post_id;
}
}
return $output;
}
/**
* Determine valid variation IDs
* @since 2.7
*/
function process_variations( $post_ids ) {
if ( empty( $this->cache ) ) {
return $post_ids;
}
$this->lookup = $this->generate_lookup_array( FWP()->unfiltered_post_ids );
// Loop through each facet's data
foreach ( $this->cache as $facet_name => $groups ) {
$this->storage[ $facet_name ] = [];
// Create an array of variation IDs
foreach ( $groups as $type => $ids ) { // products or variations
foreach ( $ids as $id ) {
$this->storage[ $facet_name ][] = $id;
// Lookup variation IDs for each product
if ( 'products' == $type ) {
if ( ! empty( $this->lookup['get_variations'][ $id ] ) ) {
foreach ( $this->lookup['get_variations'][ $id ] as $variation_id ) {
$this->storage[ $facet_name ][] = $variation_id;
}
}
}
}
}
}
$result = $this->calculate_variations();
$this->variations = $result['variations'];
$post_ids = array_intersect( $post_ids, $result['products'] );
$post_ids = empty( $post_ids ) ? [ 0 ] : $post_ids;
return $post_ids;
}
/**
* Calculate variation IDs
* @param mixed $facet_name Facet name to ignore, or FALSE
* @return array Associative array of product IDs + variation IDs
* @since 2.8
*/
function calculate_variations( $facet_name = false ) {
$new = true;
$final_products = [];
$final_variations = [];
// Intersect product + variation IDs across facets
foreach ( $this->storage as $name => $variation_ids ) {
// Skip facets in "OR" mode
if ( $facet_name === $name ) {
continue;
}
$final_variations = ( $new ) ? $variation_ids : array_intersect( $final_variations, $variation_ids );
$new = false;
}
// Lookup each variation's product ID
foreach ( $final_variations as $variation_id ) {
if ( isset( $this->lookup['get_product'][ $variation_id ] ) ) {
$final_products[ $this->lookup['get_product'][ $variation_id ] ] = true; // prevent duplicates
}
}
// Append product IDs to the variations array
$final_products = array_keys( $final_products );
foreach ( $final_products as $id ) {
$final_variations[] = $id;
}
return [
'products' => $final_products,
'variations' => array_unique( $final_variations )
];
}
/**
* Apply variation IDs to load_values() method
* @since 2.7
*/
function facet_where( $where_clause, $facet ) {
// Support facets in "OR" mode
if ( FWP()->helper->facet_is( $facet, 'operator', 'or' ) ) {
$result = $this->calculate_variations( $facet['name'] );
$variations = $result['variations'];
}
else {
$variations = $this->variations;
}
if ( ! empty( $variations ) ) {
$where_clause .= ' AND variation_id IN (' . implode( ',', $variations ) . ')';
}
return $where_clause;
}
/**
* Efficiently grab the product type without wc_get_product()
* @since 3.3.8
*/
function get_product_type( $post_id ) {
global $wpdb;
$sql = "
SELECT t.name
FROM $wpdb->terms t
INNER JOIN $wpdb->term_taxonomy tt ON tt.term_id = t.term_id AND tt.taxonomy = 'product_type'
INNER JOIN $wpdb->term_relationships tr ON tr.term_taxonomy_id = tt.term_taxonomy_id AND tr.object_id = %d";
$type = $wpdb->get_var(
$wpdb->prepare( $sql, $post_id )
);
return ( null !== $type ) ? $type : 'simple';
}
/**
* Index WooCommerce-specific values
* @since 2.1.4
*/
function index_woo_values( $return, $params ) {
$facet = $params['facet'];
$defaults = $params['defaults'];
$post_id = (int) $defaults['post_id'];
$post_type = get_post_type( $post_id );
// Index out of stock products?
$index_all = ( 'yes' === FWP()->helper->get_setting( 'wc_index_all', 'no' ) );
$index_all = apply_filters( 'facetwp_index_all_products', $index_all );
if ( 'product' == $post_type || 'product_variation' == $post_type ) {
$product = wc_get_product( $post_id );
if ( ! $product || ( ! $index_all && ! $product->is_in_stock() ) ) {
return true; // skip
}
}
// Default handling
if ( 'product' != $post_type || empty( $facet['source'] ) ) {
return $return;
}
// Ignore product attributes with "Used for variations" ticked
if ( 0 === strpos( $facet['source'], 'tax/pa_' ) ) {
if ( 'variable' == $this->get_product_type( $post_id ) ) {
$attrs = $product->get_attributes();
$attr_name = str_replace( 'tax/', '', $facet['source'] );
if ( isset( $attrs[ $attr_name ] ) && 1 === $attrs[ $attr_name ]['is_variation'] ) {
return true; // skip
}
}
}
// Custom woo fields
if ( 0 === strpos( $facet['source'], 'woo/' ) ) {
$source = substr( $facet['source'], 4 );
// Price
if ( 'price' == $source || 'sale_price' == $source || 'regular_price' == $source ) {
if ( $product->is_type( 'variable' ) ) {
$method_name = "get_variation_$source";
$price_min = $product->$method_name( 'min' ); // get_variation_price()
$price_max = $product->$method_name( 'max' );
}
else {
$method_name = "get_$source";
$price_min = $price_max = $product->$method_name(); // get_price()
}
$defaults['facet_value'] = $price_min;
$defaults['facet_display_value'] = $price_max;
FWP()->indexer->index_row( $defaults );
}
// Average Rating
elseif ( 'average_rating' == $source ) {
$rating = $product->get_average_rating();
$defaults['facet_value'] = $rating;
$defaults['facet_display_value'] = $rating;
FWP()->indexer->index_row( $defaults );
}
// Stock Status
elseif ( 'stock_status' == $source ) {
$in_stock = $product->is_in_stock();
$defaults['facet_value'] = (int) $in_stock;
$defaults['facet_display_value'] = $in_stock ? 'In Stock' : 'Out of Stock';
FWP()->indexer->index_row( $defaults );
}
// On Sale
elseif ( 'on_sale' == $source ) {
if ( $product->is_on_sale() ) {
$defaults['facet_value'] = 1;
$defaults['facet_display_value'] = 'On Sale';
FWP()->indexer->index_row( $defaults );
}
}
// Featured
elseif ( 'featured' == $source ) {
if ( $product->is_featured() ) {
$defaults['facet_value'] = 1;
$defaults['facet_display_value'] = 'Featured';
FWP()->indexer->index_row( $defaults );
}
}
// Product Type
elseif ( 'product_type' == $source ) {
$type = $product->get_type();
$defaults['facet_value'] = $type;
$defaults['facet_display_value'] = $type;
FWP()->indexer->index_row( $defaults );
}
return true; // skip
}
return $return;
}
/**
* Allow certain hard-coded choices to be translated dynamically
* instead of stored as translated in the index table
* @since 3.9.6
*/
function translate_hardcoded_choices( $label, $params ) {
$source = $params['facet']['source'];
if ( 'woo/stock_status' == $source ) {
$label = ( 'In Stock' == $label ) ? __( 'In Stock', 'fwp-front' ) : __( 'Out of Stock', 'fwp-front' );
}
elseif ( 'woo/on_sale' == $source ) {
$label = __( 'On Sale', 'fwp-front' );
}
elseif ( 'woo/featured' == $source ) {
$label = __( 'Featured', 'fwp-front' );
}
return $label;
}
/**
* WooCommerce removes its sort hooks after the main product_query runs
* We need to preserve the sort for FacetWP to work
*
* @since 3.2.8
*/
function preserve_sort( $clauses, $query ) {
if ( ! apply_filters( 'facetwp_woocommerce_preserve_sort', true ) ) {
return $clauses;
}
$prefix = FWP()->helper->get_setting( 'prefix' );
$using_sort = isset( FWP()->facet->http_params['get'][ $prefix . 'sort' ] );
if ( 'product_query' == $query->get( 'wc_query' ) && true === $query->get( 'facetwp' ) && ! $using_sort ) {
if ( false === $this->post_clauses ) {
$this->post_clauses = $clauses;
}
else {
$clauses['join'] = $this->post_clauses['join'];
$clauses['where'] = $this->post_clauses['where'];
$clauses['orderby'] = $this->post_clauses['orderby'];
// Narrow the main query results
$where_clause = FWP()->facet->where_clause;
if ( ! empty( $where_clause ) ) {
$column = $GLOBALS['wpdb']->posts;
$clauses['where'] .= str_replace( 'post_id', "$column.ID", $where_clause );
}
}
}
return $clauses;
}
/**
* Prevent WooCommerce from redirecting to single result page
* @since 3.3.7
*/
function redirect_single_search_result( $bool ) {
$using_facetwp = ( FWP()->request->is_refresh || ! empty( FWP()->request->url_vars ) );
return $using_facetwp ? false : $bool;
}
/**
* Prevent WooCommerce sort when a FacetWP sort is active
* @since 3.6.8
*/
function default_catalog_orderby( $orderby ) {
$sort = FWP()->helper->get_setting( 'prefix' ) . 'sort';
return isset( $_GET[ $sort ] ) ? 'menu_order' : $orderby;
}
}
if ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
new FacetWP_Integration_WooCommerce();
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Builds or purges the FacetWP index.
*/
class FacetWP_Integration_WP_CLI
{
/**
* Index facet data.
*
* ## OPTIONS
*
* [--ids=<ids>]
* : Index specific post IDs (comma-separated).
*
* [--facets=<facets>]
* : Index specific facet names (comma-separated).
*/
function index( $args, $assoc_args ) {
$index_all = true;
if ( isset( $assoc_args['ids'] ) ) {
if ( empty( $assoc_args['ids'] ) ) {
WP_CLI::error( 'IDs empty.' );
}
$ids = preg_replace( '/\s+/', '', $assoc_args['ids'] );
$ids = explode( ',', $ids );
$post_ids = array_filter( $ids, 'ctype_digit' );
$index_all = false;
}
else {
$post_ids = FWP()->indexer->get_post_ids_to_index();
}
if ( isset( $assoc_args['facets'] ) ) {
if ( empty( $assoc_args['facets'] ) ) {
WP_CLI::error( 'Facets empty.' );
}
$facets = [];
$facet_names = preg_replace( '/\s+/', '', $assoc_args['facets'] );
$facet_names = explode( ',', $facet_names );
foreach ( $facet_names as $name ) {
$facet = FWP()->helper->get_facet_by_name( $name );
if ( false !== $facet ) {
$facets[] = $facet;
}
}
$index_all = false;
}
else {
$facets = FWP()->helper->get_facets();
}
$progress = WP_CLI\Utils\make_progress_bar( 'Indexing:', count( $post_ids ) );
// prep
if ( $index_all ) {
FWP()->indexer->manage_temp_table( 'create' );
}
else {
$assoc_args['pre_index'] = true;
$this->purge( $args, $assoc_args );
}
// manually load value modifiers
FWP()->indexer->load_value_modifiers( $facets );
// index
foreach ( $post_ids as $post_id ) {
FWP()->indexer->index_post( $post_id, $facets );
$progress->tick();
}
// cleanup
if ( $index_all ) {
update_option( 'facetwp_last_indexed', time(), 'no' );
FWP()->indexer->manage_temp_table( 'replace' );
FWP()->indexer->manage_temp_table( 'delete' );
}
$progress->finish();
WP_CLI::success( 'Indexing complete.' );
}
/**
* Purge facet data.
*
* ## OPTIONS
*
* [--ids=<ids>]
* : Purge specific post IDs (comma-separated).
*
* [--facets=<facets>]
* : Purge specific facet names (comma-separated).
*/
function purge( $args, $assoc_args ) {
global $wpdb;
$table = FWP()->indexer->table;
if ( ! isset( $assoc_args['ids'] ) && ! isset( $assoc_args['facets'] ) ) {
$sql = "TRUNCATE TABLE $table";
}
else {
$where = [];
if ( isset( $assoc_args['ids'] ) ) {
if ( empty( $assoc_args['ids'] ) ) {
WP_CLI::error( 'IDs empty.' );
}
$ids = preg_replace( '/\s+/', '', ',' . $assoc_args['ids'] );
$ids = explode( ',', $ids );
$post_ids = array_filter( $ids, 'ctype_digit' );
$post_ids = implode( "','", $post_ids );
$where[] = "post_id IN ('$post_ids')";
}
if ( isset( $assoc_args['facets'] ) ) {
if ( empty( $assoc_args['facets'] ) ) {
WP_CLI::error( 'Facets empty.' );
}
$facet_names = preg_replace( '/\s+/', '', $assoc_args['facets'] );
$facet_names = explode( ',', $facet_names );
$facet_names = array_map( 'esc_sql', $facet_names );
$facet_names = implode( "','", $facet_names );
$where[] = "facet_name IN ('$facet_names')";
}
$sql = "DELETE FROM $table WHERE " . implode( ' AND ', $where );
}
$wpdb->query( $sql );
if ( ! isset( $assoc_args['pre_index'] ) ) {
WP_CLI::success( 'Purge complete.' );
}
}
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'facetwp', 'FacetWP_Integration_WP_CLI' );
}

View File

@@ -0,0 +1,24 @@
<?php
class FacetWP_Integration_WP_Rocket
{
function __construct() {
add_filter( 'rocket_exclude_defer_js', [ $this, 'get_exclusions' ] );
add_filter( 'rocket_delay_js_exclusions', [ $this, 'get_exclusions' ] );
add_filter( 'rocket_cdn_reject_files', [ $this, 'get_exclusions' ] );
}
function get_exclusions( $excluded ) {
$excluded[] = '(.*)facetwp(.*)';
$excluded[] = '(.*)maps.googleapis(.*)';
$excluded[] = '/jquery-?[0-9.]*(.min|.slim|.slim.min)?.js';
return $excluded;
}
}
if ( defined( 'WP_ROCKET_VERSION' ) ) {
new FacetWP_Integration_WP_Rocket();
}

View File

@@ -0,0 +1,78 @@
<?php
/*
Plugin Name: FacetWP
Description: Advanced Filtering for WordPress
Version: 4.1.5
Author: FacetWP, LLC
Author URI: https://facetwp.com/
Copyright 2023 FacetWP, LLC
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
defined( 'ABSPATH' ) or exit;
class FacetWP
{
private static $instance;
function __construct() {
// php check
if ( version_compare( phpversion(), '7.0', '<' ) ) {
add_action( 'admin_notices', array( $this, 'upgrade_notice' ) );
return;
}
// setup variables
define( 'FACETWP_VERSION', '4.1.5' );
define( 'FACETWP_DIR', dirname( __FILE__ ) );
define( 'FACETWP_URL', plugins_url( '', __FILE__ ) );
define( 'FACETWP_BASENAME', plugin_basename( __FILE__ ) );
// get the gears turning
include( FACETWP_DIR . '/includes/class-init.php' );
}
/**
* Singleton
*/
public static function instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self;
}
return self::$instance;
}
/**
* Require PHP 7.0+
*/
function upgrade_notice() {
$message = __( 'FacetWP requires PHP %s or above. Please contact your host and request a PHP upgrade.', 'fwp' );
echo '<div class="error"><p>' . sprintf( $message, '7.0' ) . '</p></div>';
}
}
function FWP() {
return FacetWP::instance();
}
FWP();

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: Catalan\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Qualsevol"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Esborrar"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Localització de clara"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Data"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Data (més recent)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Data (més antic)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Distància"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Data final"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Introduïu les paraules clau"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Introduïu la ubicació"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Cercar"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "En Stock"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Màx"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Min"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "No"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "No hi ha resultats"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "No s'han trobat resultats"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Número"
#: includes/class-renderer.php:512
msgid "of"
msgstr "de"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "Oferta"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Sense Stock"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Per pàgina"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Restablir"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Cercar"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Veure menys"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Veure més"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Veure més {num}"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Ordena per"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Data inici"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Comenceu a escriure"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Títol (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Títol (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Desfer"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Sí"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} seleccionats"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: Danish\n"
"Language: da_DK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& op"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Enhver"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Ryd"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Rud beliggenhed"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Dato"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Dato (nyeste)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Dato (ældste)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Afstand"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Slut dato"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Indtast søgeord"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Indtast lokation"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Gå"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "På lager"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Maks"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Min"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "Nej"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "Ingen resultater"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "Ingen resultater fundet"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Nummer"
#: includes/class-renderer.php:512
msgid "of"
msgstr "af"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "Udsalg"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Ikke på lager"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Per side"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Nulstil"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Søg"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Se mindre"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Læs mere"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Se {num} mere"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Sorter efter"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Start dato"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Begynd at skrive"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Titel (A - Å)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Titel (Å-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Fortryd"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Ja"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} valgt"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Alle"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Zurücksetzen"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Location löschen"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Datum"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Datum (Neueste)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Datum (älteste)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Entfernung"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Enddatum"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Suchbegriff eingeben"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Geben Sie den Ort an"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Los"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "Auf Lager"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Max"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Min"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "Nein"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "Keine Ergebnisse"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "Keine Ergebnisse gefunden"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Nummer"
#: includes/class-renderer.php:512
msgid "of"
msgstr "von"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "Im Angebot"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Ausverkauft"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Pro Seite"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Zurücksetzen"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Suche"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Weniger sehen"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Mehr anzeigen"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "{num} weitere anzeigen"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Sortieren nach"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Startdatum"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Adresse eingeben"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Titel (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Titel (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Rückgängig"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Ja"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} ausgewählt"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: Spanish (Argentina)\n"
"Language: es_AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Cualquier"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Limpiar"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Borrar Ubicaci&oacute;n"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Fecha"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Fecha (más reciente)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Fecha (más antigua)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Distancia"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Último día"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Ingrese palabras claves"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Introducir ubicación"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Ir"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "En Stock"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Max"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Minutos"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "No"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "No hay resultados"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "No se encontraron resultados"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Número"
#: includes/class-renderer.php:512
msgid "of"
msgstr "de"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "A la venta"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Fuera de Stock"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Por página"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Reiniciar"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Buscar"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Ver menos"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Ver más"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Ver {num} más"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Ordenar por"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Fecha de Comienzo"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Comience a escribir"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Título (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Título (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Deshacer"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Sí"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} selecciones"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: Spanish (Spain)\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Cualquiera"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Limpiar"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Borrar ubicación"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Fecha"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Fecha (más reciente)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Fecha (más antigua)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Distancia"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Fecha de finalización"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "¿Qué estás buscando?"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Introducir ubicación"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Ir"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "En Stock"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Máx"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Mín"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "No"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "No hay resultados"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "No se han encontrado resultados"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Número"
#: includes/class-renderer.php:512
msgid "of"
msgstr "de"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "En oferta"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Agotado"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Por página"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Reiniciar"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Buscar"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Ver menos"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Ver más"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Ver {num} más"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Ordenar por"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Fecha de inicio"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Escribe algo"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Título (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Título (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Deshacer"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Sí"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} selecciones"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:42+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: French (France)\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Tous"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Effacer"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Effacer lemplacement"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Date"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Date (plus récent)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Date (la plus ancienne)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Distance"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Date de fin"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Rechercher par nom"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Entrez la localisation"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Aller"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "En stock"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Max"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Min"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "Non"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "Aucun résultat"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "Aucun résultat trouvé"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Nombre"
#: includes/class-renderer.php:512
msgid "of"
msgstr "de"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "Promo"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Rupture de stock"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Par page"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Réinitialiser"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Rechercher"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Voir moins"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Voir plus"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Voir {num} plus"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Trier par"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Date de début"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Commencer à taper"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Titre (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Titre (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Annuler"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Oui"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} sélectionné"

View File

@@ -0,0 +1,185 @@
msgid ""
msgstr ""
"Project-Id-Version: FacetWP (front)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-21 14:01+0000\n"
"PO-Revision-Date: 2022-08-15 12:41+0000\n"
"Last-Translator: Matt Gibbs <hello@facetwp.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Loco https://localise.biz/\n"
"X-Loco-Version: 2.5.0; wp-5.6\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: includes/facets/rating.php:111
msgid "& up"
msgstr "& up"
#: includes/facets/fselect.php:37 includes/facets/fselect.php:80
#: includes/facets/dropdown.php:35 includes/facets/hierarchy.php:26
msgid "Any"
msgstr "Qualsiasi"
#: includes/facets/date_range.php:282
msgid "Clear"
msgstr "Cancella"
#: includes/facets/proximity.php:180
msgid "Clear location"
msgstr "Cancella località"
#: includes/facets/date_range.php:23
msgid "Date"
msgstr "Data"
#: includes/facets/sort.php:154
msgid "Date (Newest)"
msgstr "Data (più recente)"
#: includes/facets/sort.php:161
msgid "Date (Oldest)"
msgstr "Data (più vecchia)"
#: includes/facets/proximity.php:285
msgid "Distance"
msgstr "Distanza"
#: includes/facets/date_range.php:29
msgid "End Date"
msgstr "Data Fine"
#: includes/facets/search.php:20
msgid "Enter keywords"
msgstr "Inserisci parole chiave"
#: includes/facets/proximity.php:64
msgid "Enter location"
msgstr "Inserisci posizione"
#: includes/facets/autocomplete.php:157
msgid "Enter {n} or more characters"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:502
msgid "Featured"
msgstr ""
#: includes/facets/number_range.php:32 includes/facets/autocomplete.php:64
msgid "Go"
msgstr "Vai"
#: includes/class-display.php:175
msgid "Go to next page"
msgstr ""
#: includes/class-display.php:174
msgid "Go to page"
msgstr ""
#: includes/class-display.php:176
msgid "Go to previous page"
msgstr ""
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "In Stock"
msgstr "Disponibile"
#: includes/facets/autocomplete.php:156
msgid "Loading"
msgstr ""
#: includes/facets/number_range.php:29
msgid "Max"
msgstr "Max"
#: includes/facets/number_range.php:26
msgid "Min"
msgstr "Min"
#: includes/integrations/acf/acf.php:293
msgid "No"
msgstr "No"
#: includes/facets/autocomplete.php:97 includes/facets/autocomplete.php:158
msgid "No results"
msgstr "Nessun risultato"
#: includes/class-display.php:194 includes/facets/fselect.php:87
msgid "No results found"
msgstr "Nessun risultato trovato"
#: includes/facets/number_range.php:23
msgid "Number"
msgstr "Numero"
#: includes/class-renderer.php:512
msgid "of"
msgstr "di"
#: includes/integrations/woocommerce/woocommerce.php:499
msgid "On Sale"
msgstr "Offerta"
#: includes/integrations/woocommerce/woocommerce.php:496
msgid "Out of Stock"
msgstr "Esaurito"
#: includes/class-renderer.php:569
msgid "Per page"
msgstr "Per pagina"
#: includes/facets/slider.php:20 includes/facets/reset.php:15
msgid "Reset"
msgstr "Reset"
#: includes/facets/fselect.php:86
msgid "Search"
msgstr "Cerca"
#: includes/facets/checkboxes.php:131 includes/facets/hierarchy.php:161
msgid "See less"
msgstr "Vedi di meno"
#: includes/facets/hierarchy.php:160
msgid "See more"
msgstr "Vedi di più"
#: includes/facets/checkboxes.php:130
msgid "See {num} more"
msgstr "Vedi altri {num}"
#: includes/facets/sort.php:136
msgid "Sort by"
msgstr "Ordina per"
#: includes/facets/date_range.php:26
msgid "Start Date"
msgstr "Data Inizio"
#: includes/facets/autocomplete.php:61
msgid "Start typing"
msgstr "Scrivi l'indirizzo o la città qui"
#: includes/facets/sort.php:140
msgid "Title (A-Z)"
msgstr "Titoli (A-Z)"
#: includes/facets/sort.php:147
msgid "Title (Z-A)"
msgstr "Titoli (Z-A)"
#: includes/facets/rating.php:112
msgid "Undo"
msgstr "Annulla"
#: includes/integrations/acf/acf.php:293
msgid "Yes"
msgstr "Sì"
#: includes/facets/fselect.php:85
msgid "{n} selected"
msgstr "{n} selezionato"

Some files were not shown because too many files have changed in this diff Show More