PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.7
Filter Everything — WordPress & WooCommerce Filters v1.9.7
1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 All 52 releases
filter-everything / assets / js / filter-everything.js

filter-everything.js in Filter Everything — WordPress & WooCommerce Filters 1.9.7, at assets/js/filter-everything.js

4,968 lines 229.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*!
2 * Filter Everything 1.9.7
3 */
4 (function ($) {
5 "use strict";
6 let wpcAjax = wpcFilterFront.wpcAjaxEnabled;
7 let wpcStatusCookieName = wpcFilterFront.wpcStatusCookieName;
8 let wpcMoreLessCookieName = wpcFilterFront.wpcMoreLessCookieName;
9 let wpcWidgetStatusCookieName = wpcFilterFront.wpcWidgetStatusCookieName;
10 let wpcHierachyListCookieName = wpcFilterFront.wpcHierarchyListCookieName;
11 let wpcMobileWidth = wpcFilterFront.wpcMobileWidth;
12 let wpcPostContainers = wpcFilterFront.wpcPostContainers;
13 let wpcAutoScroll = wpcFilterFront.wpcAutoScroll;
14 let wpcAutoScrollOffset = wpcFilterFront.wpcAutoScrollOffset;
15 let wpcWaitCursor = wpcFilterFront.wpcWaitCursor;
16 let wpcPostsPerPage = wpcFilterFront.wpcPostsPerPage;
17 let wpcUseSelect2 = wpcFilterFront.wpcUseSelect2;
18 let wpcDateFilters = wpcFilterFront.wpcDateFilters;
19 let wpcDateFiltersLocale = wpcFilterFront.wpcDateFiltersLocale;
20 let wpcDateFiltersL10n = wpcFilterFront.wpcDateFiltersL10n;
21 let wpcPopupCompatMode = wpcFilterFront.wpcPopupCompatMode;
22 let wpcApplyButtonSets = wpcFilterFront.wpcApplyButtonSets;
23 let wpcQueryOnThePageSets = wpcFilterFront.wpcQueryOnThePageSets;
24 let noPostsContainerMsg = wpcFilterFront.wpcNoPostsContainerMsg;
25 let wpcIsPro = Boolean(wpcFilterFront.wpcIsPro);
26 let permalinksEnabled = Boolean(wpcFilterFront.permalinksEnabled);
27 let wpcMoreLessCount = wpcFilterFront.wpcMoreLessCount;
28 let wpcSearchChipsText = wpcFilterFront.wpcSearchChipsText;
29 let chipsTitle = wpcFilterFront.chipsTitle;
30 let chipsReset = wpcFilterFront.chipsReset;
31 // Instant (client-side) Apply-button recount is active when the server printed
32 // window.wpcFilterJsonData for this page — it does so only when a Set with
33 // use_apply_button=yes is present AND the "Instant recount" option is enabled.
34 // With static-file delivery the data arrives asynchronously: the bootstrap
35 // exposes wpcFilterJsonDataPromise until the blob is fetched and merged.
36 // Without either, Apply-button Sets fall back to the legacy per-click AJAX recount.
37 let wpcInstantRecount = ( typeof window.wpcFilterJsonData !== 'undefined' || typeof window.wpcFilterJsonDataPromise !== 'undefined' );
38 let wpcWidgetContainer = '.wpc-filters-main-wrap';
39 let wpcIsMobile = false;
40 let toReplaceSEO = true;
41 let prevState = false; // Contains SEO Rule availability on a page
42 let currentState = false; // Contains SEO Rule availability on a page
43
44 let seoRuleId = $('#wpc-seo-rule-id').data( 'seoruleid' );
45 if ( seoRuleId > 0 ) {
46 prevState = true;
47 }
48
49 function removeElement($el)
50 {
51 $el.fadeTo(100, 0, function() {
52 $el.slideUp(100, function() {
53 $el.remove();
54 });
55 });
56 }
57
58 // When a PARENT filter selection changes, its children (and grandchildren)
59 // keep whatever the visitor picked earlier — e.g. Brand switched from BMW
60 // to Audi still kept Model = "5 Series", so the next Apply landed on a
61 // zero-result page. Step-by-step configurators reset the chain instead:
62 // clear every descendant selection; the recount that follows the parent
63 // change re-renders placeholders and counters from the fresh DOM state.
64 // Term selections only — a numeric range child keeps its min/max inputs.
65 function wpcResetChildFilters( parentFid, $widget, visited ) {
66 if ( ! parentFid ) return;
67 visited = visited || {};
68 if ( visited[parentFid] ) return; // parent chains are acyclic, but stay safe
69 visited[parentFid] = true;
70
71 $('.wpc-filters-section[data-parent-filter-id="' + parentFid + '"]', $widget).each(function () {
72 const $childSection = $(this);
73
74 $childSection.find('select.wpc-filters-widget-select').each(function () {
75 const $select = $(this);
76 const $default = $select.find('option.wpc-dropdown-default');
77 const defVal = $default.length ? $default.val() : '';
78 if ( $select.val() !== defVal ) {
79 // change.select2 refreshes the rendered selection without
80 // re-entering the filter change handlers
81 $select.val( defVal ).trigger('change.select2');
82 }
83 });
84
85 // Radios/checkboxes (incl. labels view): the recount collector reads
86 // :checked and, for radios, the data-wpc-was-checked attribute
87 $childSection.find('input:checked').not('.wpc-range-list-item').each(function () {
88 $(this).prop('checked', false).attr('data-wpc-was-checked', false).data('wpc-was-checked', false);
89 });
90
91 // Grandchildren may carry a selection too (e.g. arrived via URL)
92 wpcResetChildFilters( $childSection.data('fid'), $widget, visited );
93 });
94 }
95
96 // Named so the label-tap fallback below can invoke the same logic when
97 // the forwarded input click never arrives (iOS Safari, see below)
98 function wpcTermInputClickHandler(e) {
99 // Bumped on every run; the label-tap fallback below compares it
100 this.__wpcClickSeq = (this.__wpcClickSeq || 0) + 1;
101 let wpcLink = $(this).data('wpc-link');
102 let $el = $(this).parents(wpcWidgetContainer);
103 let setId = $el.data('set');
104 let applyButtonMode = false;
105
106 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
107 applyButtonMode = true;
108 }
109
110 if( applyButtonMode && !wpcInstantRecount ){
111 // Legacy Apply-button mode: recount counters on the server per click
112 e.preventDefault();
113 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
114 }else if( applyButtonMode ){
115 // The recount collector selects radios by the [data-wpc-was-checked=true]
116 // ATTRIBUTE — always clear/set BOTH the attribute and jQuery data on the
117 // whole radio group, otherwise the previously selected term (incl. the
118 // server-rendered one) keeps counting as selected alongside the new one
119 const isRadio = $(this).is('input[type="radio"]');
120 if (isRadio && !$(this).hasClass( 'flrt-star-input' )) {
121 if ($(this).data('wpc-was-checked')) {
122 $(this).prop('checked', false).attr('data-wpc-was-checked', false).data('wpc-was-checked', false);
123 } else {
124 $('input[type="radio"][name="' + $(this).attr('name') + '"]').attr('data-wpc-was-checked', false).data('wpc-was-checked', false);
125 $(this).attr('data-wpc-was-checked', true).data('wpc-was-checked', true);
126 }
127 }
128 const isRatingStar = $(this).hasClass('flrt-star-input');
129 if (isRatingStar && isRadio) {
130 const $starContent = $(this).closest('.flrt-stars-wpc-filter-content');
131 $starContent.find('input.flrt-star-input').removeClass('wpc-checked-for-apply-button');
132 if ($(this).data('wpc-was-checked')) {
133 $(this).prop('checked', false).attr('data-wpc-was-checked', false).data('wpc-was-checked', false);
134 } else if($(this).is(':checked')) {
135 $('input[type="radio"][name="' + $(this).attr('name') + '"]').not(this).attr('data-wpc-was-checked', false).data('wpc-was-checked', false);
136 $(this).data('wpc-was-checked', true).attr('data-wpc-was-checked', true);
137 }
138 // The star fill has TWO writers whose ORDER depends on the click
139 // target: a real click lands on the <a> inside the label, so the
140 // '.wpc-filter-content a' handler triggers this input synchronously
141 // and the label click handler repaints AFTERWARDS from its own
142 // stale data (a plain label click runs them in the opposite order).
143 // Repaint once more after the whole click dispatch settles, from
144 // the input state — the single source of truth.
145 const selectedAndAbove = $starContent.data('selectedAndAbove');
146 setTimeout(function () {
147 const $checkedInput = $starContent.find('input.flrt-star-input[data-wpc-was-checked="true"]');
148 const n = $checkedInput.length ? Number($checkedInput.data('ratingNum')) : 0;
149 $starContent.find('label.flrt-star-label').each(function () {
150 const k = Number($(this).data('ratingNum'));
151 const on = n > 0 && (selectedAndAbove ? k >= n : k <= n);
152 $(this).toggleClass('flrt-star-label-hover', on)
153 .toggleClass('flrt-star-label-checked', on && !selectedAndAbove);
154 if (selectedAndAbove) {
155 $(this).toggleClass('flrt-star-label-not-checked', n > 0 && on);
156 } else {
157 $(this).removeClass('flrt-star-label-not-checked');
158 }
159 $(this).data('wpc-was-checked', n > 0 && k === n);
160 });
161 // The count next to the stars has the same two writers. When the
162 // click target is not an <a> (crawler span-links, a bare label),
163 // the label handler reads the radio BEFORE the browser toggles it,
164 // so a deselect kept the old rating's count. Settle it here.
165 if (n > 0) {
166 flrtGetRatingTermCount($starContent.find('label.flrt-rating-numb-' + n), true);
167 } else {
168 $('#flrt-wpc-term-count').text('');
169 }
170 }, 0);
171 }
172
173 if ($(this).hasClass('wpc-range-list-item')) {
174 $('.wpc-range-list-item', $(this).parents('.wpc-filters-range-inputs')).removeClass('wpc-range-list-item-checked');
175
176 if(!applyButtonMode) return;
177
178 const elementData = $(this).data();
179 let wpcEName = (typeof elementData.wpcEName !== 'undefined');
180 let wpcMin = (typeof elementData.min !== 'undefined');
181 let wpcMax = (typeof elementData.max !== 'undefined');
182 let wpcSlugMin= (typeof elementData.wpcSlugMin !== 'undefined');
183 let wpcSlugMax= (typeof elementData.wpcSlugMax !== 'undefined');
184
185 $(this).addClass('wpc-range-list-item-checked');
186
187 let isChecked = $(this).data('wpc-was-checked');
188
189 if(!wpcEName) return;
190
191
192 if(wpcMin){
193 if(!wpcSlugMin) return;
194 let inputNameMin = elementData.wpcSlugMin + elementData.wpcEName;
195 let minVal = elementData.min;
196 let $inputElementMin = $("input[name=" + inputNameMin + "].wpc-filters-range-min", $el)
197 if(minVal !== $inputElementMin.data().min){
198 $inputElementMin.parent().find('.wpc-range-clear').show();
199 }
200 /*if(!minVal || minVal === 0){
201 minVal = $inputElementMin.data().min
202 }*/
203
204 if(!isChecked){
205 minVal = $inputElementMin.data().min;
206 }
207
208 $inputElementMin.attr('value', minVal)
209 $inputElementMin.val(minVal)
210
211 }
212
213 if(wpcMax){
214 if(!wpcSlugMax) return;
215 let inputNameMax = elementData.wpcSlugMax + elementData.wpcEName ;
216 let maxVal = elementData.max;
217 let $inputElementMax = $("input[name=" + inputNameMax + "].wpc-filters-range-max", $el)
218 if(maxVal !== $inputElementMax.data().max){
219 $inputElementMax.parent().find('.wpc-range-clear').show();
220 }
221 /* if(!maxVal || maxVal === 0){
222 maxVal = $inputElementMax.data().max
223 }*/
224
225 if(!isChecked){
226 maxVal = $inputElementMax.data().max
227 }
228
229 $inputElementMax.attr('value', maxVal)
230 $inputElementMax.val(maxVal)
231 }
232
233
234 let form = $(this).parents('.wpc-filters-range-inputs').find('form');
235 if(form.length){
236 $.fn.wpcInitSlider( form );
237 }
238 }
239 // Single-choice parents cascade-reset their child filters; checkbox
240 // parents are multi-select, where narrowing is cumulative — keep them
241 if ( $(this).is('input[type="radio"]') && !$(this).hasClass('flrt-star-input') ) {
242 wpcResetChildFilters( $(this).closest('.wpc-filters-section').data('fid'), $el );
243 }
244 wpcApplyEngine.applyJsMode($el, setId)
245 }else if(wpcAjax){
246 e.preventDefault();
247 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
248 }else{
249 location.href = wpcLink;
250 }
251 }
252 $(document).on('click', '.wpc-filter-content input[type="radio"], .wpc-filter-content input[type="checkbox"]', wpcTermInputClickHandler);
253
254 // iOS Safari never dispatches the forwarded click on a display:none
255 // input (the custom-checkbox pattern the labels view uses), so a label
256 // tap toggles the checkbox natively but the delegated input handler
257 // above is never called — the term looks selected, yet no recount runs
258 // and a second tap is needed. Desktop browsers and Android DO forward
259 // the click. Catch the label click itself and, once the native dispatch
260 // has settled, run the same handler manually unless the input click
261 // already arrived.
262 $(document).on('click', '.wpc-filter-content li.wpc-term-item label', function (e) {
263 if ($(e.target).closest('a').length) return; // <a> terms have their own handler
264 if ($(this).hasClass('flrt-star-label')) return; // rating stars have their own handler
265 if ($(e.target).is('input')) return; // the forwarded input click itself, already handled
266 let input = null;
267 const forId = $(this).attr('for');
268 if (forId) { input = document.getElementById(forId); }
269 if (!input) { input = $(this).closest('.wpc-term-item-content-wrapper').find('input[type="checkbox"], input[type="radio"]')[0]; }
270 if (!input || (input.type !== 'checkbox' && input.type !== 'radio')) return;
271 // Where the browser forwards the click, it is dispatched right after this
272 // label click — before any timer — and bumps the counter. Compare the
273 // counter, not the time: a 500 ms window measured from the START of the
274 // handler expired while a large set's synchronous instant recount was
275 // still running, so the handler ran twice and a radio ended unchecked.
276 const seq = input.__wpcClickSeq || 0;
277 setTimeout(function () {
278 if ((input.__wpcClickSeq || 0) !== seq) return;
279 wpcTermInputClickHandler.call(input, { preventDefault: function () {} });
280 }, 0);
281 });
282
283 $(document).on('change', '.wpc-orderby-select', function (){
284 let wpcSortingForm = $(this).parents('form.wpc-sorting-form');
285 // let wpcSortingVal = $(this).val();
286 let search = '';
287 //@todo bug on mobile force AJAX
288 search = '?' + wpcSortingForm.serialize();
289
290 let wpcLink = wpcSortingForm.attr('action') + search;
291
292 if( wpcFilterFront.wpcAjaxEnabled ) {
293 $('.wpc-filters-main-wrap').each(function (index, element) {
294 let $el = $(element);
295 wpcSendFilterRequest(wpcLink, $el, false);
296 });
297 }else{
298 wpcSortingForm.attr('action', wpcLink);
299 // window.location.href = wpcLink;
300 wpcSortingForm.submit();
301 }
302 });
303
304 $(document).on('change', '.wpc-filter-content select', function (e) {
305
306 var wpcLink = $(this).find('option:selected').data('wpc-link');
307 let $el = $(this).parents(wpcWidgetContainer);
308 let setId = $el.data('set');
309 let applyButtonMode = false;
310
311 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
312 applyButtonMode = true;
313 }
314
315 if( applyButtonMode && !wpcInstantRecount ){
316 e.preventDefault();
317 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
318 }else if( applyButtonMode ){
319 // A dropdown is single-choice: its change cascade-resets child filters
320 wpcResetChildFilters( $(this).closest('.wpc-filters-section').data('fid'), $el );
321 wpcApplyEngine.applyJsMode($el, setId)
322 }else if(wpcAjax){
323 e.preventDefault();
324 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
325 }else{
326 location.href = wpcLink;
327 }
328 });
329
330 // Smart spans render non-indexable chips as <span data-wpc-span-link>
331 $(document).on('click', '.wpc-filter-chip a, .wpc-filter-chip span[data-wpc-span-link], .wpc-filter-chip span.wpc-apply-button-chip', function (e){
332 let wpcLink = $(this).attr('href') || $(this).attr('data-wpc-span-link');
333 let setId = $(this).parents('.wpc-filter-chips-list').data('set');
334 let $el = $('.wpc-filter-set-'+setId);
335 let applyButtonMode = false;
336
337 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
338 // Legacy mode counts only chips INSIDE the filters widget as Apply-button
339 // clicks (pre-1.9.3 behaviour) — standalone chips above the posts navigate
340 // like in non-apply mode. Instant mode handles all chips client-side.
341 if( wpcInstantRecount ? $el.length > 0 : $(this).parents('.wpc-filter-set-'+setId).length > 0 ){
342 applyButtonMode = true;
343 }
344 }
345
346 if( applyButtonMode && !wpcInstantRecount ){
347 e.preventDefault();
348 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
349 }else if( applyButtonMode ){
350 if($(this).hasClass('wpc-apply-button-chips-reset')){
351 e.preventDefault();
352 $(`a.wpc-filters-reset-button.wpc-filters-reset-button-${setId}`).first().click();
353 }
354
355 if($(this).closest('li').hasClass('wpc-chip-search')){
356 e.preventDefault();
357 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
358 }
359
360 if($(this).hasClass('wpc-apply-button-chip') && !$(this).hasClass('wpc-apply-button-chips-reset') && !$(this).closest('li').hasClass('wpc-chip-search')){
361 e.preventDefault();
362 // Removing the LAST chip equals resetting all filters —
363 // behave exactly like the Reset all button
364 const $otherChips = $(this).closest('.wpc-filter-chips-list')
365 .find('li.wpc-filter-chip')
366 .not('.wpc-chip-reset-all')
367 .not($(this).closest('li'));
368 const $resetButton = $(`a.wpc-filters-reset-button.wpc-filters-reset-button-${setId}`).first();
369
370 if( $otherChips.length === 0 && $resetButton.length > 0 ){
371 $resetButton.click();
372 }else{
373 wpcApplyEngine.unsetChip($(this));
374 }
375 }
376 //applyJsMode($el, setId)
377 }else if(wpcAjax) {
378 e.preventDefault();
379 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
380 }else{
381 // A span chip has no native navigation — follow the link manually
382 if( ! $(this).is('a') && wpcLink ){
383 window.location.href = wpcLink;
384 return false;
385 }
386 return true;
387 }
388 });
389
390 $(document).on('click', 'a.wpc-filters-submit-button', function (e){
391
392 if( $(this).hasClass('on-hold') ){
393 if( $(this).data('last') !== 'wpc-search-field' ){
394 e.preventDefault();
395 return false;
396 }
397 }
398
399 let wpcLink = $(this).attr('href');
400 let setId = $(this).parents('.wpc-filters-main-wrap').data('set');
401 let $el = $('.wpc-filter-set-'+setId);
402
403 let applyButtonMode = false;
404
405 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
406 applyButtonMode = true;
407 // return false;
408 }
409
410 if( wpcAjax && wpcQueryOnThePageSets.includes( setId ) ) {
411 e.preventDefault();
412 wpcSendFilterRequest( wpcLink, $el, applyButtonMode && wpcInstantRecount );
413 }else{
414 return true;
415 }
416 });
417
418 $(document).on('click', 'a.wpc-search-clear-icon', function (e){
419 let wpcLink = $(this).attr('href');
420 let setId = $(this).parents('.wpc-filters-main-wrap').data('set');
421 let $el = $('.wpc-filter-set-'+setId);
422 let applyButtonMode = false;
423
424 if( wpcAjax ) {
425 e.preventDefault();
426 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
427 return false;
428 }else{
429 return true;
430 }
431 });
432
433 $(document).on( 'change', '.wpc-search-field', function (e) {
434 let form = $(this).parents(".wpc-filter-search-form");
435
436 let $el = form.parents(wpcWidgetContainer);
437 let setId = $el.data('set');
438 let applyButtonMode = false;
439
440 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
441 applyButtonMode = true;
442 // return false;
443 }
444
445 if( wpcAjax || applyButtonMode ){
446 let search = form.serialize();
447 let wpcLink = form.attr('action') + '?' + search;
448 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
449 return false;
450 } else {
451 form.submit();
452 }
453 });
454
455 $(document).on('submit', '.wpc-filter-search-form', function (e) {
456 let form = $(this);
457
458 let $el = form.parents(wpcWidgetContainer);
459 let setId = $el.data('set');
460 let applyButtonMode = false;
461
462 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
463 applyButtonMode = true;
464 }
465
466 if( wpcAjax || applyButtonMode ){
467 let search = form.serialize();
468 let wpcLink = form.attr('action') + '?' + search;
469 wpcSendFilterRequest( wpcLink, $el, applyButtonMode );
470 return false;
471 } else {
472 return true;
473 // form.submit();
474 }
475
476 });
477
478 $(document).on('click', 'a.wpc-filters-reset-button', function (e){
479
480 if( $(this).hasClass('on-hold') ){
481 e.preventDefault();
482 return false;
483 }
484
485 let wpcLink = $(this).attr('href');
486 let setId = $(this).parents('.wpc-filters-main-wrap').data('set');
487 let $el = $('.wpc-filter-set-'+setId);
488 let applyButtonMode = false;
489
490 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
491 if( $(this).parents('.wpc-filter-set-'+setId).length > 0 ){
492 applyButtonMode = true;
493 }
494 }
495
496 if (applyButtonMode && wpcInstantRecount){
497 e.preventDefault();
498 // With "AJAX for Filters" disabled, Reset all must be a plain page
499 // reload to the clean URL — same as the Apply button navigates to
500 // the built URL. Explicit assign() (not `return true`): the Reset
501 // chip and the last-chip removal trigger this handler with a
502 // programmatic jQuery .click(), which never follows the href.
503 if( ! wpcAjax ){
504 window.location.assign(wpcLink);
505 return;
506 }
507 wpcSendFilterRequest(wpcLink, $el, applyButtonMode);
508 }else if (wpcAjax) {
509 e.preventDefault();
510 if (wpcQueryOnThePageSets.includes(setId)) {
511 wpcSendFilterRequest(wpcLink, $el, false);
512 } else {
513 wpcSendFilterRequest(wpcLink, $el, true);
514 }
515
516 } else {
517 return true;
518 // wpcSendFilterRequest( wpcLink, $el, true );
519 }
520 });
521
522 $(document).on('click', 'i.wpc-toggle-children-list', function (){
523 let tid = $(this).data('tid');
524 let $targetLi = $(this).parent(".wpc-term-item-content-wrapper").parent('li');
525 let $targetFilter = $(this).parents('.wpc-filters-section');
526
527 if ( $targetLi.hasClass( 'wpc-opened' ) ) {
528 $targetLi.removeClass( 'wpc-opened' )
529 .addClass( 'wpc-closed' );
530 setStatusCookie( -tid, wpcHierachyListCookieName );
531 } else if ( $targetLi.hasClass( 'wpc-closed' ) ) {
532 $targetLi.removeClass( 'wpc-closed' )
533 .addClass( 'wpc-opened' );
534 setStatusCookie( tid, wpcHierachyListCookieName );
535 } else {
536 if ( $targetFilter.hasClass( 'wpc-filter-hierarchy-reverse' ) ) {
537 $targetLi.removeClass( 'wpc-opened' ) // For any case
538 .addClass( 'wpc-closed' );
539 setStatusCookie( -tid, wpcHierachyListCookieName );
540 } else {
541 $targetLi.removeClass( 'wpc-closed' ) // For any case
542 .addClass( 'wpc-opened' );
543 setStatusCookie( tid, wpcHierachyListCookieName );
544 }
545 }
546 });
547
548 $(document).on('click', '.wpc-filters-overlay', function (){
549 let setId = $('body').data('set');
550 wpcCloseFiltersContainer(setId);
551 })
552
553 $(document).on('change', '.wpc-filter-range-form input[type="number"]', function (event) {
554
555 let form = $(this).parents('.wpc-filter-range-form');
556 processRangeForm( event, form );
557 });
558
559 $(document).on( 'click','.wpc-open-close-filters-button', function (e){
560 e.preventDefault();
561
562 let openCloseButton = $(this);
563 let wpcSetId = openCloseButton.data('wid');
564 let wpcButtonFilterSetError = openCloseButton.data('wpcButtonFilterSetError');
565 let wpcButtonWidgetError = openCloseButton.data('wpcButtonWidgetError');
566 let widgetContent = $('.wpc-filter-set-'+wpcSetId+' .wpc-filters-widget-content');
567
568 if (typeof wpcButtonFilterSetError !== 'undefined') {
569 alert(wpcButtonFilterSetError);
570 }
571
572 if (typeof wpcButtonWidgetError !== 'undefined') {
573 if(typeof window.wpcFilterWidgetActive === 'undefined'){
574 alert(wpcButtonWidgetError);
575 }
576 }
577
578 if( widgetContent.is(':visible') ){
579 widgetContent.slideUp({
580 duration: 100,
581 complete: function (){
582 $(this).addClass('wpc-closed')
583 .removeClass('wpc-opened');
584 openCloseButton.removeClass('wpc-opened');
585 wpcSetCookie(wpcWidgetStatusCookieName, null, {path: '/', 'max-age': 2592000});
586 }
587 });
588 }else{
589 widgetContent.slideDown({
590 duration: 100,
591 complete: function (){
592 $(this).addClass('wpc-opened')
593 .removeClass('wpc-closed');
594 openCloseButton.addClass('wpc-opened');
595 wpcSetCookie(wpcWidgetStatusCookieName, wpcSetId, {path: '/', 'max-age': 2592000});
596 }
597 });
598 }
599 });
600
601 $(document).on('click', '.wpc-widget-close-icon', function (e){
602 e.preventDefault();
603 let $wrapper = $( this ).parents( wpcWidgetContainer );
604 let setId = $wrapper.data( 'set' );
605 wpcCloseFiltersContainer(setId);
606 });
607
608 $(document).on('click', '.wpc-filters-apply-button', function (e){
609 e.preventDefault();
610 let $wrapper = $( this ).parents( wpcWidgetContainer );
611 let setId = $wrapper.data( 'set' );
612 let $content = $( '.wpc-filter-set-'+setId+' .wpc-filters-widget-content' );
613 let href = $(this).attr( 'href' );
614 let wpcReload = ! $(this).hasClass('wpc-posts-loaded');
615 let wpcZindex = '';
616 let $currentTag = false;
617 let $el = $('.wpc-filter-set-'+setId);
618
619 let applyButtonMode = false;
620
621 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
622 applyButtonMode = true;
623 }
624
625 if(applyButtonMode && wpcInstantRecount){
626 if((wpcFilterFront.wpcAjaxEnabled && wpcAjax) && wpcQueryOnThePageSets.includes( setId ) ) {
627 wpcSendFilterRequest( href, $el, applyButtonMode );
628 }else{
629 location.href = href;
630 }
631 }
632
633
634 $wrapper.removeClass('wpc-container-opened');
635 $('html').removeClass('wpc-overlay-visible');
636 $content.removeClass('wpc-filters-widget-opened');
637 $('.wpc-open-button-'+setId+' .wpc-filters-open-widget').removeClass('wpc-opened');
638
639 if( wpcPopupCompatMode ) {
640 setTimeout(() => {
641 $content.parents().each(function (index, tag) {
642 $currentTag = $(tag);
643 wpcZindex = $currentTag.data('wpczindex');
644
645 // Saved z-index for
646 if (wpcZindex !== 'undefined') {
647 $currentTag.css('z-index', wpcZindex);
648 }
649
650 if ($currentTag.hasClass('wpc-force-visibility')) {
651 $currentTag.removeClass('wpc-force-visibility');
652 }
653 });
654
655 setTimeout(() => {
656 $(".wpc-was-invisible").css('opacity', '1')
657 .removeClass('wpc-was-invisible');
658 }, 300);
659
660 }, 260);
661 }
662
663 if( wpcReload ) {
664 location.href = href;
665 }
666 });
667
668 $(document).on('submit', '.wpc-filter-range-form', function (e) {
669 submitSliderForm(e, $(this));
670 });
671
672 $(document).on('keydown', '.wpc-filters-range-from,.wpc-filters-range-to', function (event){
673 if ( event.which == 13 ) {
674 let fid = $(this).data('fid');
675 processRangeForm( event, $("#wpc-filter-date-range-form-"+ fid ) );
676 }
677 });
678
679 $(document).on('click', '.wpc-filter-content a', function (e) {
680 e.preventDefault();
681 let wpcInputId = $(this).closest('label').attr('for');
682 $(this).closest('label').parent('.wpc-term-item-content-wrapper').parent('.wpc-term-item').find('#'+wpcInputId).trigger('click');
683 });
684
685 $(document).on('click', '.wpc-filters-open-widget', function (e) {
686 e.preventDefault();
687 let setId = $(this).data('wid');
688 wpcOpenContainer( setId );
689 });
690
691 $(document).on('click', '.wpc-filters-close-button', function (e) {
692 e.preventDefault();
693 let wrapper = $(this).parents(wpcWidgetContainer);
694 let setId = wrapper.data('set');
695
696 if( wpcAjax && wpcFilterFront.wpcAjaxEnabled ){
697 let cancelLink = $(this).attr('href');
698 let applyLink = $('.wpc-filter-set-'+setId+' .wpc-filters-apply-button').attr('href');
699
700 if( cancelLink !== applyLink ){
701 wpcSendFilterRequest( cancelLink, wrapper, false,'wpcCloseFiltersContainer' );
702 return;
703 }
704 }
705
706 wpcCloseFiltersContainer(setId);
707 });
708
709 $(document).on('click', 'a.wpc-toggle-a', function (e){
710 e.preventDefault();
711 let fid = $(this).data('fid');
712 let $filterSection = $( ".wpc-filters-section-" + fid );
713 //$( ".wpc-filters-section-" + fid ).toggleClass( 'wpc-show-more' );
714
715 if ( $filterSection.hasClass('wpc-show-more' ) ) {
716 $filterSection.removeClass( 'wpc-show-more' )
717 .addClass( 'wpc-show-less' );
718 setStatusCookie( -fid, wpcMoreLessCookieName );
719 } else if ( $filterSection.hasClass('wpc-show-less' ) ) {
720 $filterSection.removeClass( 'wpc-show-less' )
721 .addClass( 'wpc-show-more' );
722 setStatusCookie( fid, wpcMoreLessCookieName );
723 } else {
724 // No status class detected
725 if( $filterSection.hasClass( 'wpc-filter-has-selected' ) || $filterSection.hasClass( 'wpc-show-more-reverse' ) ) {
726 $filterSection.removeClass( 'wpc-show-more' ) // For any case
727 .addClass( 'wpc-show-less' );
728 setStatusCookie( -fid, wpcMoreLessCookieName );
729 } else {
730 $filterSection.removeClass( 'wpc-show-less' ) // For any case
731 .addClass( 'wpc-show-more' );
732 setStatusCookie( fid, wpcMoreLessCookieName );
733 }
734 }
735 });
736
737 $(document).on('click', '.wpc-filters-main-wrap input', function (e) {
738 let lastInputClass = $(this).attr('class');
739 if ( typeof lastInputClass !== 'undefined' ){
740 $('.wpc-filters-submit-button').data('last', lastInputClass);
741 }
742 });
743
744 $(document).on('click', '.wpc-filter-title button', function (e) {
745 e.preventDefault();
746 let $filterSection = $(this).parents('.wpc-filters-section');
747 let filterId = $filterSection.data( 'fid' );
748
749 if ( $filterSection.hasClass( 'wpc-opened' ) ) {
750 $filterSection.removeClass( 'wpc-opened' )
751 .addClass( 'wpc-closed' );
752 setStatusCookie( -filterId, wpcStatusCookieName );
753 } else if ( $filterSection.hasClass( 'wpc-closed' ) ) {
754 $filterSection.removeClass( 'wpc-closed' )
755 .addClass( 'wpc-opened' );
756 setStatusCookie( filterId, wpcStatusCookieName );
757 } else {
758 if( $filterSection.hasClass( 'wpc-filter-has-selected' ) || $filterSection.hasClass( 'wpc-filter-collapsible-reverse' ) ) {
759 $filterSection.removeClass( 'wpc-opened' )
760 .addClass( 'wpc-closed' );
761 setStatusCookie( -filterId, wpcStatusCookieName );
762 } else {
763 $filterSection.removeClass( 'wpc-closed' )
764 .addClass( 'wpc-opened' );
765 setStatusCookie( filterId, wpcStatusCookieName );
766 }
767 }
768 });
769
770 $( window ).resize(function() {
771 if( window.innerWidth <= wpcMobileWidth ){
772 wpcIsMobile = true;
773 if( wpcFilterFront.showBottomWidget === 'yes' ) {
774 wpcAjax = true;
775 }
776 }else{
777 wpcAjax = wpcFilterFront.wpcAjaxEnabled;
778 wpcIsMobile = false;
779 }
780
781 if ( ! wpcSsMobileBrowser() ){
782 if( wpcUseSelect2 === 'yes' ){
783 $(wpcWidgetContainer).each( function ( index, widget ){
784 let widgetSet = $(widget).data('set');
785 let widgetClass = 'wpc-filter-set-'+widgetSet;
786 wpcInitSelect2(widgetClass);
787 });
788 }
789 }
790
791 });
792
793 if ($.support.pjax) {
794 $(document).on('pjax:end', function() {
795 setTimeout(() => {
796 wpcInitiateAll();
797 }, 300);
798 });
799 }
800
801 $(document).ready(function (){
802 wpcInitiateAll();
803 });
804
805 $(document).on('input', '.wpc-search-field',function (e){
806 let $section = $(this).parents('.wpc-filters-section');
807 let searchOrig = $(this).val();
808 let $search = searchOrig.toLowerCase();
809 let $submitBtn = $(".wpc-filters-submit-button");
810 let theHref = $submitBtn.attr('href');
811
812 if ( typeof theHref !== 'undefined' ){
813 let url = new URL(theHref);
814 url.searchParams.set( 'srch', searchOrig);
815 $submitBtn.attr( 'href', url.href );
816
817 if( $search !== '' ){
818 $section.addClass('wpc-search-active');
819 }else{
820 $section.removeClass('wpc-search-active');
821 }
822 }
823
824 });
825
826 $(document).on('input', '.wpc-filter-search-field',function (e){
827 let $search = $(this).val().toString().toLowerCase();
828 let $section = $(this).parents('.wpc-filters-section');
829 let fid = $section.data('fid');
830
831 if( $search !== '' ){
832 $(".wpc-filter-search-wrapper-"+fid+" .wpc-search-clear").show();
833 $section.addClass('wpc-search-active');
834 }else{
835 $(".wpc-filter-search-wrapper-"+fid+" .wpc-search-clear").hide();
836 $section.removeClass('wpc-search-active');
837 }
838
839 $(".wpc-filters-list-"+fid+" li").each(function( index, value ) {
840 let $li = $(value);
841 // The term name may be rendered as a link, a span (when the
842 // "Disable filter links for crawlers" option turns <a> into
843 // <span>), a colour swatch or a brand logo. Reading the whole
844 // label's text covers every case; 'label a' alone is empty
845 // whenever the links are replaced with spans, which made the
846 // search match nothing and hide every term.
847 let $termName = $(value).find('label').text().toLowerCase();
848 if ($termName.indexOf($search) > -1) {
849 $li.addClass('showli');
850 } else {
851 $li.removeClass('showli');
852 }
853 });
854 });
855
856 $(document).on( 'click', '.wpc-search-clear', function (e){
857 e.preventDefault();
858 let $searchField = $(this).parent(".wpc-filter-search-wrapper").find(".wpc-filter-search-field");
859 $searchField.val('')
860 .trigger('input');
861 })
862
863
864 function isDonePressed( inst ) {
865 return ( ( $('#ui-datepicker-div .ui-datepicker-close.ui-state-hover').length > 0 ) && !inst._keyEvent );
866 }
867
868 function wpcInitiateAll(){
869 $('.wpc-filter-range-form').each( function ( index, form ){
870 $.fn.wpcInitSlider( $(form) );
871 });
872
873 if (window.innerWidth <= wpcMobileWidth) {
874 wpcIsMobile = true;
875 if( wpcFilterFront.showBottomWidget === 'yes' ) {
876 wpcAjax = true;
877 }
878 }
879
880 if( wpcUseSelect2 === 'yes' ){
881 $(wpcWidgetContainer).each( function ( index, widget ){
882 let widgetSet = $(widget).data('set');
883 let widgetClass = 'wpc-filter-set-'+widgetSet;
884 wpcInitSelect2(widgetClass);
885 });
886 }
887
888 if ( wpcDateFilters !== '' ) {
889
890 $.datepicker.regional[wpcDateFiltersLocale] = wpcDateFiltersL10n;
891 $.datepicker.setDefaults(wpcDateFiltersL10n);
892
893 const updatedProperties = {
894 _selectDate : function( id, dateStr ) {
895 var onSelect,
896 target = $( id ),
897 inst = this._getInst( target[ 0 ] );
898
899 dateStr = ( dateStr != null ? dateStr : this._formatDate( inst ) );
900 if ( inst.input ) {
901 inst.input.val( dateStr );
902 }
903 this._updateAlternate( inst );
904
905 onSelect = this._get( inst, "onSelect" );
906 if ( onSelect ) {
907 onSelect.apply( ( inst.input ? inst.input[ 0 ] : null ), [ dateStr, inst ] ); // trigger custom callback
908 } else if ( inst.input ) {
909 inst.input.trigger( "change" ); // fire the change event
910 }
911
912 if ( inst.inline || this._curInst.id.includes( 'wpc-filters-alt-date' ) ) {
913 this._updateDatepicker( inst );
914 } else {
915 this._hideDatepicker();
916 this._lastInput = inst.input[ 0 ];
917 if ( typeof( inst.input[ 0 ] ) !== "object" ) {
918 inst.input.trigger( "focus" ); // restore focus
919 }
920 this._lastInput = null;
921 }
922 }
923 };
924
925 Object.assign( $.datepicker, updatedProperties );
926
927 $.each( wpcDateFilters, function ( fid, dateFilter ) {
928
929 if ( $("#wpc-filters-date-from-"+ fid).length < 1 ) {
930 return true;
931 }
932 let setId = $("#wpc-filters-date-from-"+ fid).data('set');
933
934 let applyButtonMode = false;
935
936 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
937 applyButtonMode = true;
938 }
939 if(applyButtonMode && wpcInstantRecount){
940 wpcDateFiltersL10n.closeText = wpcDateFiltersL10n.applyText
941 }
942
943 let pickerOptions = {};
944 let timeFormat = dateFilter['time_format'].includes('s') ? 'HH.mm.ss' : 'HH.mm.00';
945 if ( dateFilter['date_type'] === 'date' ) {
946 let yearMin = $("#wpc-filters-date-from-"+ fid).data('from').slice(0,4);
947 let yearMax = $("#wpc-filters-date-to-"+ fid).data('to').slice(0,4);
948
949 pickerOptions = {
950 dateFormat: dateFilter['date_format'], // will be shown in visible field
951 altFieldTimeOnly: false,
952 altField: '#wpc-filters-date-from-' + fid,
953 altFormat: 'yy-mm-dd',
954 changeYear: true,
955 yearRange: yearMin+':'+yearMax,
956 changeMonth: true,
957 showButtonPanel: true,
958 onClose: function( dateText, inst ){
959 if( isDonePressed( inst ) ) {
960 if(applyButtonMode && wpcInstantRecount){
961 updateInputDateData(dateText, inst.input)
962 }
963 processRangeForm( event, $("#wpc-filter-date-range-form-"+ fid ) );
964 }
965 },
966 beforeShow: function(input, inst) {
967 $('#ui-datepicker-div').addClass('wpc-filter-datepicker wpc-filter-datepicker-'+ fid);
968 },
969 onUpdateDatepicker: function( inst ) {
970 let $inp = $(inst.input);
971 let w = $inp.outerWidth();
972 $(".wpc-filter-datepicker-"+ fid).css('width', w + 'px');
973 }
974 };
975
976 $( "#wpc-filters-alt-date-from-" + fid ).datepicker( pickerOptions );
977 pickerOptions.altField = '#wpc-filters-date-to-' + fid;
978 $( "#wpc-filters-alt-date-to-" + fid ).datepicker( pickerOptions );
979
980 } else if ( dateFilter['date_type'] === 'datetime' ) {
981 $.timepicker.regional[wpcDateFiltersLocale] = wpcDateFiltersL10n;
982 $.timepicker.setDefaults(wpcDateFiltersL10n);
983 let yearMin = $("#wpc-filters-date-from-"+ fid).data('from').slice(0,4);
984 let yearMax = $("#wpc-filters-date-to-"+ fid).data('to').slice(0,4);
985
986 pickerOptions = {
987 dateFormat: dateFilter['date_format'],
988 timeFormat: dateFilter['time_format'], // Depends from localization
989 altFieldTimeOnly: false,
990 altField: '#wpc-filters-date-from-' + fid,
991 altFormat: 'yy-mm-dd',
992 altTimeFormat: timeFormat, // Depends from format HH.mm.ss or HH.mm.00
993 altSeparator: 't',
994 changeYear: true,
995 yearRange: yearMin+':'+yearMax,
996 changeMonth: true,
997 showButtonPanel: true,
998 controlType: 'select',
999 oneLine: true,
1000 onClose: function( dateText, inst ){
1001 if( isDonePressed( inst )) {
1002 if(applyButtonMode && wpcInstantRecount){
1003 updateInputDateData(dateText, inst.input)
1004 }
1005 processRangeForm( event, $("#wpc-filter-date-range-form-"+ fid ) );
1006 }
1007 },
1008 beforeShow: function(input, inst) {
1009 $('#ui-datepicker-div').addClass('wpc-filter-datepicker wpc-filter-datepicker-'+ fid);
1010 },
1011 onUpdateDatepicker: function( inst ) {
1012 let $inp = $(inst.input);
1013 let w = $inp.outerWidth();
1014 $(".wpc-filter-datepicker-"+ fid).css('width', w + 'px');
1015 }
1016 };
1017
1018 $( "#wpc-filters-alt-date-from-" + fid ).datetimepicker( pickerOptions );
1019 pickerOptions.altField = '#wpc-filters-date-to-' + fid;
1020 $( "#wpc-filters-alt-date-to-" + fid ).datetimepicker( pickerOptions );
1021
1022 } else if ( dateFilter['date_type'] === 'time' ) {
1023 $.timepicker.regional[wpcDateFiltersLocale] = wpcDateFiltersL10n;
1024 $.timepicker.setDefaults(wpcDateFiltersL10n);
1025
1026 pickerOptions = {
1027 timeFormat: dateFilter['time_format'],
1028 altField: '#wpc-filters-date-from-' + fid,
1029 altFieldTimeOnly: false,
1030 altTimeFormat: timeFormat,
1031 controlType: 'select',
1032 oneLine: true,
1033 onClose: function( dateText, inst ){
1034 if( isDonePressed( inst ) ) {
1035 if(applyButtonMode && wpcInstantRecount){
1036 updateInputDateData(dateText, inst.input)
1037 }
1038 processRangeForm( event, $("#wpc-filter-date-range-form-"+ fid ) );
1039 }
1040 },
1041 beforeShow: function(input, inst) {
1042 $('#ui-datepicker-div').addClass('wpc-filter-datepicker wpc-filter-datepicker-'+ fid);
1043 },
1044 onUpdateDatepicker: function( inst ) {
1045 let $inp = $(inst.input);
1046 let w = $inp.outerWidth();
1047 $(".wpc-filter-datepicker-"+ fid).css('width', w + 'px');
1048 }
1049 };
1050
1051 $( "#wpc-filters-alt-date-from-" + fid ).timepicker( pickerOptions );
1052 pickerOptions.altField = '#wpc-filters-date-to-' + fid;
1053 $( "#wpc-filters-alt-date-to-" + fid ).timepicker( pickerOptions );
1054 }
1055 });
1056 }
1057
1058 $('.wpc-help-tip').tipTip({
1059 'activation': 'hover',
1060 'attribute': 'data-tip',
1061 'fadeIn': 50,
1062 'fadeOut': 50,
1063 'delay': 200,
1064 'keepAlive': true,
1065 'maxWidth': "220px",
1066 });
1067 }
1068
1069 function updateInputDateData(dateText, $input){
1070 if(typeof $input.data('wpcTempFrom') !== 'undefined'){
1071 $input.data('wpcTempFrom', dateText)
1072 $input.attr('data-wpc-temp-from', dateText)
1073 }
1074 if(typeof $input.data('wpcTempTo') !== 'undefined'){
1075 $input.data('wpcTempTo', dateText)
1076 $input.attr('data-wpc-temp-to', dateText)
1077 }
1078 }
1079
1080 function wpcSsMobileBrowser() {
1081 const userAgent = navigator.userAgent || navigator.vendor || window.opera;
1082 const isMobile = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
1083
1084 return isMobile;
1085 }
1086
1087 function wpcIsDesktopSafari() {
1088 const userAgent = navigator.userAgent;
1089 const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(userAgent);
1090 const isNotMobile = !/Mobile|iPhone|iPad|iPod/i.test(userAgent);
1091
1092 return isSafari && isNotMobile;
1093 }
1094
1095 function wpcInitSelect2( widgetClass, ) {
1096 if( typeof $.fn.select2 === 'undefined'){
1097 return;
1098 }
1099
1100 // Destroy existing select2 instances before re-initializing
1101 // to prevent duplicate event listeners that block mobile scroll
1102 $('.wpc-filters-widget-select').each(function() {
1103 if ($(this).hasClass('select2-hidden-accessible')) {
1104 $(this).select2('destroy');
1105 }
1106 });
1107 $('.wpc-orderby-select').each(function() {
1108 if ($(this).hasClass('select2-hidden-accessible')) {
1109 $(this).select2('destroy');
1110 }
1111 });
1112
1113 let wpcUserAgent = navigator.userAgent.toLowerCase();
1114 let wpcIsAndroid = wpcUserAgent.indexOf("android") > -1;
1115
1116 let wpcAllowSearchField = 0;
1117 if( wpcIsAndroid ) {
1118 wpcAllowSearchField = Infinity;
1119 }
1120
1121 if( wpcIsDesktopSafari() ){
1122 wpcAllowSearchField = 10;
1123 }
1124
1125 const wpcHideEmpty = $(`.${widgetClass}`).data('wpc-hide-empty');
1126 const useApplyButton = $(`.${widgetClass}`).data('wpc-use-apply-button');
1127
1128 const isHideEmpty = wpcHideEmpty === 'yes';
1129
1130
1131
1132 $('.wpc-filters-widget-select').select2({
1133 dropdownCssClass: 'wpc-filter-everything-dropdown',
1134 dropdownParent: $('.'+widgetClass+' .wpc-filters-widget-content'),
1135 templateResult: function( data, container ) {
1136 let postsCount = $(data.element).data('count');
1137 let wpcSlug = $(data.element).data('wpcSlug');
1138 let wpcEName = $(data.element).data('wpcEName');
1139 if($(data.element).hasClass('wpc-dropdown-default')){
1140 data.text = $(data.element).text();
1141 }
1142 if(wpcSlug !== undefined && wpcEName !== undefined){
1143 $(container).addClass(`select2-${wpcEName}-${wpcSlug}`);
1144 }
1145 $(container).addClass('wpc-select2-term-id-' + data.id);
1146 if($(data.element).hasClass('wpc-show-with-parent-false')){
1147 $(container).addClass('wpc-show-with-parent-false');
1148 }
1149 if($(data.element).hasClass('wpc-ask-to-parent-display')){
1150 $(container).addClass('wpc-ask-to-parent-display');
1151 }
1152 if ( isHideEmpty && useApplyButton && postsCount <= 0 ) {
1153 $(container).addClass('wpc-term-count-hidden-0');
1154 }
1155 return wpcSelect2Template( data );
1156 },
1157 templateSelection: function( data, container ) {
1158 let postsCount = $(data.element).data('count');
1159 let wpcSlug = $(data.element).data('wpcSlug');
1160 let wpcEName = $(data.element).data('wpcEName');;
1161 if($(data.element).hasClass('wpc-dropdown-default')){
1162 data.text = $(data.element).text();
1163 }
1164 if(wpcSlug !== undefined && wpcEName !== undefined){
1165 $(container).addClass(`select2-${wpcEName}-${wpcSlug}`);
1166 }
1167 $(container).addClass('wpc-select2-term-id-' + data.id);
1168 if($(data.element).hasClass('wpc-show-with-parent-false')){
1169 $(container).addClass('wpc-show-with-parent-false');
1170 }
1171 if($(data.element).hasClass('wpc-ask-to-parent-display')){
1172 $(container).addClass('wpc-ask-to-parent-display');
1173 }
1174 if ( isHideEmpty && useApplyButton && postsCount <= 0 ) {
1175 $(container).addClass('wpc-term-count-hidden-0');
1176 }
1177 return wpcSelect2Template( data );
1178 },
1179 minimumResultsForSearch: wpcAllowSearchField,
1180 });
1181
1182 $('.wpc-orderby-select').select2({
1183 dropdownCssClass: 'wpc-filter-everything-dropdown',
1184 dropdownParent: $('.wpc-after-sorting-form'),
1185 templateResult: function(data) {
1186 // We only really care if there is an element to pull classes from
1187 if (!data.element) {
1188 return data.text;
1189 }
1190 let $dr_element = $(data.element);
1191 let $dr_wrapper = $('<span></span>');
1192 $dr_wrapper.addClass($dr_element[0].className);
1193 $dr_wrapper.text(data.text);
1194
1195 return $dr_wrapper;
1196 },
1197 minimumResultsForSearch: Infinity
1198 });
1199 }
1200
1201 function wpcSelect2Template( data ) {
1202 // We only really care if there is an element to pull classes from
1203 if ( ! data.element ) {
1204 return data.text;
1205 }
1206
1207 let theImageSrc = $(data.element).data('image');
1208 let brandImageSrc = $(data.element).data('brand');
1209 let theColor = $(data.element).data('color');
1210 let starRating = $(data.element).data('starRating');
1211 let innerHtml = data.text;
1212 let postsCount = $(data.element).data('count');
1213 let additionalClass = '';
1214
1215 if ( typeof theImageSrc !== 'undefined' ) {
1216
1217 additionalClass = 'wpc-item-has-swatch';
1218 innerHtml = $('<span data-label="' + data.text +'" class="wpc-term-swatch-wrapper wpc-term-swatch-image"><img src="'+theImageSrc+'" class="wpc-term-image" /></span><span class="wpc-term-name">'+data.text+'</span>');
1219
1220 } else if ( typeof theColor !== 'undefined' ) {
1221
1222 additionalClass = 'wpc-item-has-swatch';
1223
1224 let swatch = '<span data-label="' + data.text +'" class="wpc-term-swatch-wrapper">';
1225 if ( theColor === 'none' ){
1226 swatch += '<span class="wpc-term-swatch wpc-no-swatch-yet">';
1227 } else {
1228 swatch += '<span class="wpc-term-swatch" style="background-color:'+theColor+'">';
1229 }
1230 swatch += '</span></span><span class="wpc-term-name">'+data.text+'</span>';
1231
1232 innerHtml = $( swatch );
1233
1234 } else if ( typeof brandImageSrc !== 'undefined' ) {
1235
1236 additionalClass = 'wpc-item-has-brand';
1237 innerHtml = $('<span data-label="' + data.text +'" class="wpc-term-image-wrapper"><img src="'+brandImageSrc+'"/></span><span class="wpc-term-name">'+data.text+'</span>');
1238
1239 } else if ( typeof starRating !== 'undefined' && starRating > 0) {
1240 additionalClass = 'wpc-item-has-star-rating';
1241 innerHtml = $(data.element.innerHTML);
1242 }
1243
1244 let $dr_element = $(data.element);
1245 let $dr_wrapper = $('<span></span>');
1246 $dr_wrapper.addClass($dr_element[0].className);
1247 if ( additionalClass !== '' ){
1248 $dr_wrapper.addClass( additionalClass );
1249 }
1250 $dr_wrapper.html( innerHtml );
1251 if ( typeof postsCount !== 'undefined' ){
1252 $dr_wrapper.append( '<span class="wpc-term-count"><span class="wpc-term-count-brackets-open">(</span><span class="wpc-term-count-value">'+postsCount+'</span><span class="wpc-term-count-brackets-close">)</span></span>' );
1253 }
1254
1255 return $dr_wrapper;
1256 }
1257
1258 function wpcGetCookie(name) {
1259 var matches = document.cookie.match(new RegExp(
1260 "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
1261 ))
1262 return matches ? decodeURIComponent(matches[1]) : undefined
1263 }
1264
1265 //Example: wpcSetCookie('user', 'John', {secure: true, 'max-age': 3600});
1266 function wpcSetCookie(name, value, props) {
1267 props = props || {}
1268 let exp = props.expires
1269 if (typeof exp == "number" && exp) {
1270 let d = new Date()
1271 d.setTime(d.getTime() + exp*1000)
1272 exp = props.expires = d
1273 }
1274
1275 if(exp && exp.toUTCString) { props.expires = exp.toUTCString() }
1276 value = encodeURIComponent(value)
1277
1278 let updatedCookie = name + "=" + value
1279 for(let propName in props){
1280 updatedCookie += "; " + propName
1281 let propValue = props[propName]
1282 if(propValue !== true){ updatedCookie += "=" + propValue }
1283 }
1284 document.cookie = updatedCookie
1285 }
1286
1287 function setStatusCookie( fid, wpcListCookieName )
1288 {
1289 let status = wpcGetCookie(wpcListCookieName);
1290 let _fids = new Array();
1291
1292 fid = fid.toString();
1293
1294 // In case there is no Cookies yet
1295 if( typeof status === 'undefined' ){
1296 status = '';
1297 }else{
1298 status = status.trim();
1299 _fids = status.split(',');
1300 }
1301
1302 // Filter from empty elements
1303 _fids = _fids.filter(function (el) {
1304 return el != '';
1305 });
1306
1307 // Remove possible existing closed/opened to avoid double commands e.g. 151 and -151
1308 let reversal = -fid;
1309 let pos = _fids.indexOf( reversal.toString() );
1310
1311 if ( pos !== -1 ) {
1312 _fids.splice(pos, 1);
1313 }
1314
1315 if( _fids.indexOf(fid) === -1 ){
1316 _fids.push(fid);
1317
1318 let newStatus = '';
1319
1320 if( _fids.length === 0 ){
1321 newStatus = fid;
1322 }else{
1323 newStatus = _fids.join();
1324 }
1325
1326 wpcSetCookie( wpcListCookieName, newStatus, {path: '/', 'max-age': 2592000} )
1327 }
1328
1329 }
1330
1331 function wpcCloseFiltersContainer(setId)
1332 {
1333 let $wrapper = $('.wpc-filter-set-'+setId);
1334 let $content = $('.wpc-filter-set-'+setId+' .wpc-filters-widget-content');
1335 $('.wpc-open-button-'+setId+' .wpc-filters-open-widget').removeClass('wpc-opened');
1336 $('html').removeClass('wpc-overlay-visible');
1337 $content.removeClass('wpc-filters-widget-opened');
1338
1339 if( wpcPopupCompatMode ) {
1340 setTimeout(() => {
1341
1342 let wpcZindex = '';
1343 let $currentTag = false;
1344
1345 $content.parents().each(function (index, tag) {
1346 $currentTag = $(tag);
1347 wpcZindex = $currentTag.data('wpczindex');
1348 // Saved z-index for
1349 if (wpcZindex !== 'undefined') {
1350 $currentTag.css('z-index', wpcZindex);
1351 }
1352
1353 if ($currentTag.hasClass('wpc-force-visibility')) {
1354 $currentTag.removeClass('wpc-force-visibility');
1355 }
1356 });
1357
1358 setTimeout(() => {
1359 $(".wpc-was-invisible").css('opacity', '1')
1360 .removeClass('wpc-was-invisible');
1361 }, 300);
1362
1363 }, 260);
1364 }
1365
1366 $wrapper.removeClass('wpc-container-opened');
1367 }
1368
1369 function wpcOpenFiltersContainer(setId)
1370 {
1371 let $wrapper = $('.wpc-filter-set-'+setId);
1372 let $content = $('.wpc-filter-set-'+setId+' .wpc-filters-widget-content');
1373 let wpcZindex = '';
1374 let wpcVisibility = '';
1375 let wpcTransform = '';
1376 let $currentTag = false;
1377
1378 if( $content.length < 1 ){
1379 return true;
1380 }
1381
1382 if( wpcPopupCompatMode ) {
1383 $content.parents().each(function (index, tag) {
1384 $currentTag = $(tag);
1385 wpcZindex = $currentTag.css('z-index');
1386 wpcVisibility = $currentTag.is(":visible");
1387 wpcTransform = $currentTag.css('transform');
1388
1389 // Save current z-index for future
1390 if (wpcZindex !== 'auto') {
1391 $currentTag.data('wpczindex', wpcZindex);
1392 }
1393
1394 $currentTag.css('z-index', 'auto');
1395
1396 // Save current display, opacity and visibility values
1397 if (!wpcVisibility || wpcTransform !== 'none') {
1398 if (!$currentTag.hasClass('widget_wpc_filters_widget')
1399 &&
1400 !$currentTag.hasClass('wpc-filters-main-wrap')
1401 ) {
1402 $currentTag.css('opacity', '0');
1403 $currentTag.addClass('wpc-force-visibility wpc-was-invisible');
1404 }
1405 }
1406 });
1407
1408 if( wpcUseSelect2 === 'yes' ){
1409 wpcInitSelect2( 'wpc-filter-set-'+setId );
1410 }
1411 }
1412
1413 $('.wpc-open-button-'+setId+' .wpc-filters-open-widget').addClass('wpc-opened');
1414 $('html').addClass('wpc-overlay-visible');
1415 $('body').data('set', setId);
1416
1417 $content.addClass('wpc-filters-widget-opened');
1418 $wrapper.addClass('wpc-container-opened');
1419 $('.wpc-filter-set-'+setId+' .wpc-filters-close-button').attr('href', window.location.href);
1420
1421 }
1422
1423 function wpcOpenContainer( setId ) {
1424 let $wrapper = $( '.wpc-filter-set-'+setId );
1425
1426 if( $wrapper.length < 1 ){
1427 alert('There is no filter widget with ID '+setId+' on this page');
1428 return;
1429 }
1430
1431 if( $wrapper.hasClass('wpc-container-opened') ){
1432 wpcCloseFiltersContainer(setId);
1433 }else{
1434 wpcOpenFiltersContainer(setId);
1435 }
1436 }
1437
1438 function wpcLockApplyButton( setId )
1439 {
1440 $(".wpc-filter-set-"+setId).addClass('is-active');
1441 // We have only to check what the element was last focused
1442 // if( $('.wpc-search-field').length < 1 ){
1443 $(".wpc-filter-set-"+setId+" .wpc-filters-submit-button").addClass('on-hold');
1444 $(".wpc-filter-set-"+setId+" .wpc-filters-reset-button").addClass('on-hold');
1445 // }
1446 }
1447
1448 function wpcUnlockApplyButton( setId )
1449 {
1450 $(".wpc-filter-set-"+setId).removeClass('is-active');
1451 $(".wpc-filter-set-"+setId+" .wpc-filters-submit-button").removeClass('on-hold');
1452 $(".wpc-filter-set-"+setId+" .wpc-filters-reset-button").removeClass('on-hold');
1453 }
1454
1455 function wpcShowSpinner()
1456 {
1457 $('.wpc-spinner, html').addClass('is-active');
1458 }
1459
1460 function wpcHideSpinner()
1461 {
1462 $('.wpc-spinner, html').removeClass('is-active');
1463 }
1464
1465 $.fn.wpcInitSlider = function ( form ) {
1466
1467 let $el = form.parents(wpcWidgetContainer);
1468 let setId = $el.data('set');
1469 let applyButtonMode = false;
1470
1471 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes( setId ) ){
1472 applyButtonMode = true;
1473 }
1474 // Default valued at start
1475 let $min = form.find('.wpc-filters-range-min');
1476 let $max = form.find('.wpc-filters-range-max');
1477 let $slider = form.find('.wpc-filters-range-slider-control');
1478 let step = parseFloat( $min.attr('step') );
1479
1480 let initialMinVal = parseFloat( $min.data('min') );
1481 let initialMaxVal = parseFloat( $max.data('max') );
1482
1483 // Values after applying filter
1484 let curMinVal = parseFloat( $min.val() );
1485 let curMaxVal = parseFloat( $max.val() );
1486
1487 if( curMaxVal !== initialMaxVal ){
1488 $max.parent().find('.wpc-range-clear').show();
1489 }else{
1490 $max.parent().find('.wpc-range-clear').hide();
1491 }
1492
1493 if( curMinVal !== initialMinVal ){
1494 $min.parent().find('.wpc-range-clear').show();
1495 }else{
1496 $min.parent().find('.wpc-range-clear').hide();
1497 }
1498
1499
1500 // Setting value into form inputs when slider is moving
1501 $slider.slider({
1502 min: initialMinVal,
1503 max: initialMaxVal,
1504 values: [curMinVal, curMaxVal],
1505 range: true,
1506 step: step,
1507 slide: function (event, elem) {
1508 let instantMinVal = elem.values[0];
1509 let instantMaxVal = elem.values[1];
1510
1511 $min.val(instantMinVal);
1512 $max.val(instantMaxVal);
1513 },
1514 change: function (event) {
1515 // It is better always to submit slider automatically to avoid empty intersection occurrence
1516 submitSliderForm(event, form);
1517 }
1518 });
1519
1520 form.submit(function (e) {
1521 //Remove ? sign if form is empty
1522 if (($(this).serialize().length === 0)) {
1523 e.preventDefault();
1524 window.location.assign(window.location.pathname);
1525 }
1526 });
1527 }
1528
1529 function submitSliderForm(event, form) {
1530 if (event.originalEvent) {
1531 processRangeForm( event, form );
1532 }
1533 }
1534
1535 $(document).on('click', '.wpc-filters-range-min-column .wpc-range-clear', function(event) {
1536 let rangeInput = $(this).parent().find('input');
1537 let minVal = rangeInput.data('min');
1538 $(this).hide();
1539 rangeInput.val(minVal).change();
1540 });
1541
1542 $(document).on('click', '.wpc-filters-range-max-column .wpc-range-clear', function(event) {
1543 let rangeInput = $(this).parent().find('input');
1544 let maxVal = rangeInput.data('max');
1545 $(this).hide();
1546 rangeInput.val(maxVal).change();
1547 });
1548
1549 function processRangeForm( event, form ){
1550 let $el = form.parents(wpcWidgetContainer);
1551 let setId = $el.data('set');
1552 let applyButtonMode = false;
1553
1554 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes( setId ) ){
1555 applyButtonMode = true;
1556 }
1557 let low_suffix = 'min';
1558 let high_suffix = 'max';
1559
1560 if ( form.hasClass('wpc-filter-date-range-form') ) {
1561 low_suffix = 'from';
1562 high_suffix = 'to';
1563 }
1564
1565 let $min = form.find( '.wpc-filters-range-' + low_suffix );
1566 let $max = form.find( '.wpc-filters-range-' + high_suffix );
1567
1568 if ( low_suffix === 'min' && high_suffix === 'max' ) {
1569 var curMinVal = parseFloat( $min.val() );
1570 var curMaxVal = parseFloat( $max.val() );
1571 } else {
1572 var curMinVal = $min.val().toString();
1573 var curMaxVal = $max.val().toString();
1574 }
1575
1576 var initialMin = $min.data( low_suffix );
1577 var initialMax = $max.data( high_suffix );
1578
1579 // The form has slider
1580 if( form.hasClass('wpc-form-has-slider') ){
1581 let $slider = form.find('.wpc-filters-range-slider-control');
1582 // in Case of e.type === 'change' we have to set slider values
1583 if ( event.type === 'change' ){
1584 $slider.slider("option", "values", [curMinVal, curMaxVal]);
1585 }
1586 }
1587
1588 if(!applyButtonMode || !wpcInstantRecount){
1589 if (curMinVal === initialMin) {
1590 $min.attr('disabled', true);
1591 }
1592
1593 if (curMaxVal === initialMax) {
1594 $max.attr('disabled', true);
1595 }
1596 }
1597
1598 if (applyButtonMode && wpcInstantRecount) {
1599 wpcApplyEngine.compareInputWithRangeList(form, applyButtonMode);
1600 wpcApplyEngine.applyJsMode($el, setId)
1601 } else if (wpcAjax || applyButtonMode) {
1602 event.preventDefault();
1603 let search = form.serialize();
1604 let questionParam = '?'
1605 if (!search) {
1606 questionParam = '';
1607 }
1608 let wpcLink = form.attr('action') + questionParam + search;
1609
1610 wpcSendFilterRequest(wpcLink, $el, applyButtonMode);
1611
1612 $min.attr('disabled', true);
1613 $max.attr('disabled', true);
1614
1615 } else {
1616 form.trigger('submit');
1617 }
1618 }
1619
1620 function wpcSendFilterRequest( link, widget, applyButtonMode, onComplete ){
1621
1622 onComplete = (typeof onComplete !== 'undefined') ? onComplete : false;
1623 removeElement($('.wpc-front-error'));
1624
1625 let requestParams = {};
1626 requestParams.flrt_ajax_link = link;
1627 requestParams.wpcAjaxAction = 'filter';
1628 let setId = widget.data('set');
1629 let widgetClass = 'wpc-filter-set-'+setId;
1630 let targetPostsContainer = wpcPostContainers['default'];
1631 let wpcUsedRouter = false;
1632
1633 if( typeof wpcPostContainers[setId] !== "undefined" ){
1634 targetPostsContainer = wpcPostContainers[setId];
1635 }
1636
1637 // Disable Apply button for Pop-up widget as its behavior is the same
1638 if( applyButtonMode ){
1639 if( $("body").hasClass("wpc_show_bottom_widget") ){
1640 if( window.innerWidth <= wpcMobileWidth ){
1641 applyButtonMode = false;
1642 }
1643 }
1644 }
1645
1646 $.ajax({
1647 'method': 'POST',
1648 'data': requestParams,
1649 'url': link,
1650 'dataType': 'html',
1651 beforeSend: function () {
1652 if( wpcWaitCursor ){
1653 $('html, body').css("cursor", "wait");
1654 }
1655
1656 let $a_el = $(widget).find('.wpc-filters-apply-button');
1657
1658 $a_el.removeClass('wpc-posts-loaded');
1659
1660 let oldLink = $a_el.attr('href');
1661
1662 $a_el.attr('href', link);
1663 $a_el.data('href', oldLink);
1664
1665 // $(".wpc-filters-section-"+setId).find(".wpc-filters-submit-button").attr('href', link);
1666
1667 if( applyButtonMode ){
1668 wpcLockApplyButton( setId );
1669 // Legacy Apply-button mode only locks the button (pre-1.9.3 behaviour)
1670 if( wpcInstantRecount ){
1671 wpcShowSpinner();
1672 }
1673 }else{
1674 wpcShowSpinner();
1675 }
1676 },
1677 complete: function () {
1678 if(onComplete !== false){
1679 eval(onComplete+'(setId)');
1680 }
1681 if( wpcWaitCursor ) {
1682 $('html, body').css("cursor", "auto");
1683 }
1684
1685 wpcInitiateAll();
1686
1687 if( applyButtonMode ){
1688 wpcUnlockApplyButton(setId);
1689 if( wpcInstantRecount ){
1690 wpcHideSpinner();
1691 }
1692 } else if( !wpcUsedRouter ) {
1693 wpcHideSpinner();
1694 }
1695 },
1696 success: function ( response ) {
1697 if ( typeof response !== 'undefined' ) {
1698 // Products
1699 // Wrap response to allow .find method search inner elements.
1700 response = '<div class="responseWrapper">'+response+'</div>';
1701 let $response = $(response);
1702 let $responsePostsContainer = $response.find(targetPostsContainer);
1703 let currentSeoRuleId = $response.find('#wpc-seo-rule-id').data('seoruleid');
1704 let isFilterRequest = $response.find('.wpc-filters-main-wrap').hasClass('wpc-filter-request');
1705
1706 if($('#wpc-filter-everything-js-before').length > 0){
1707 wpcApplyEngine.updateWpcFilterJsonData(response)
1708 }
1709
1710
1711 if ( currentSeoRuleId > 0 ) {
1712 currentState = true;
1713 } else {
1714 currentState = false;
1715 }
1716
1717 if ( ! currentState && ! prevState ) {
1718 toReplaceSEO = false;
1719 } else {
1720 toReplaceSEO = true;
1721 }
1722
1723 if( applyButtonMode ){
1724 // Filters Widget
1725 wpcReloadFiltersWidget( $response, widgetClass );
1726 if(window.innerWidth > wpcMobileWidth ){
1727 if(isFilterRequest){
1728 wpcEnableStickyButtons(true);
1729 wpcUpdateStickyButtons();
1730 }
1731
1732 if(!isFilterRequest){
1733 wpcEnableStickyButtons(false);
1734 wpcUpdateStickyButtons();
1735 }
1736 }
1737 if( !wpcInstantRecount ){
1738 // Legacy Apply-button recount: a per-click request refreshes
1739 // only the widget and counters; posts wait for the Apply click
1740 return;
1741 }
1742 }
1743
1744 if(!applyButtonMode && !isFilterRequest && window.innerWidth > wpcMobileWidth ){
1745 wpcEnableStickyButtons(false);
1746 wpcUpdateStickyButtons();
1747 }
1748
1749 if( ( $responsePostsContainer.length > 0 ) && wpcFilterFront.wpcAjaxEnabled && wpcQueryOnThePageSets.includes( setId ) ){
1750 if( isFilterRequest ) {
1751 $("body").addClass('wpc_is_filter_request');
1752 } else {
1753 $("body").removeClass('wpc_is_filter_request');
1754 }
1755 // But this works on TV also
1756 //Try reinitializing the WooCommerce product-collection block via Interactivity Router
1757 wpcUsedRouter = false;
1758 const wpcImportmap = document.querySelector('script[type="importmap"]');
1759 if( wpcImportmap ){
1760 try {
1761 const wpcMap = JSON.parse(wpcImportmap.textContent);
1762 const wpcRouterUrl = wpcMap.imports && wpcMap.imports['@wordpress/interactivity-router'];
1763
1764 if( wpcRouterUrl && $(targetPostsContainer).find('[data-wp-interactive]').length > 0 ){
1765 wpcUsedRouter = true;
1766 import(wpcRouterUrl).then(function(module){
1767 if( module.actions && typeof module.actions.navigate === 'function' ){
1768 module.actions.navigate(link, { force: true }).then(function(){
1769 wpcHideSpinner();
1770 }).catch(function(){
1771 $(targetPostsContainer).html( $responsePostsContainer.html() );
1772 wpcHideSpinner();
1773 });
1774 }
1775 }).catch(function(){
1776 $(targetPostsContainer).html( $responsePostsContainer.html() );
1777 wpcHideSpinner();
1778 });
1779 }
1780 } catch(e) {}
1781 }
1782
1783 if( !wpcUsedRouter ){
1784 $(targetPostsContainer).html( $responsePostsContainer.html() );
1785 }
1786
1787 // Re-init Divi modules
1788 const isDiviTheme = document.body.classList.contains('wp-theme-Divi') ||
1789 document.body.classList.contains('theme-Divi');
1790 if( isDiviTheme && typeof window.et_pb_init_modules === 'function' ){
1791 window.et_pb_init_modules();
1792 }
1793 // wpcPostsWereLoaded = true;
1794
1795 // Mark the "Show" button to not reload content
1796 $(widget).find('.wpc-filters-apply-button').addClass('wpc-posts-loaded');
1797
1798 //@todo update selected terms if them outside of posts container
1799
1800 if ( toReplaceSEO ) {
1801 let responseTitle = $response.find('title').text();
1802 let responseCanonical = $response.find('link[rel="canonical"]').attr('href');
1803
1804 // If h1 outside of posts container
1805 if( $responsePostsContainer.find('h1').length < 1 ){
1806 if( $response.find('h1').length > 0){
1807 $('h1')[0].replaceWith( $response.find('h1')[0] );
1808 }
1809 }
1810
1811 // If seoText container is outside from posts container
1812 if( $responsePostsContainer.find('.wpc-page-seo-description').length < 1 ){
1813 let wpcSeoTextContainer = $response.find('.wpc-page-seo-description');
1814 let originalSeoTextContainer = $('.wpc-page-seo-description');
1815 if( wpcSeoTextContainer.length > 0 && originalSeoTextContainer.length > 0){
1816 $('.wpc-page-seo-description')[0].replaceWith( wpcSeoTextContainer[0] );
1817 }
1818 }
1819
1820 // Replace title
1821 if( typeof responseTitle !== 'undefined' && responseTitle !== '' ){
1822 $(document).attr( 'title', responseTitle );
1823 }
1824
1825 // Handle <meta name="description" /> tag
1826 handleMetaTag('description', response);
1827
1828 // Handle <meta name="robots" /> tag
1829 handleMetaTag('robots', response);
1830
1831 // Handle Canonical
1832 if( typeof responseCanonical !== 'undefined' && responseCanonical !== '' ){
1833 // Replace content if tag exists
1834 if( $('link[rel="canonical"]').length > 0 ){
1835 $('link[rel="canonical"]').attr('href', responseCanonical );
1836 } else {
1837 // Append meta tag
1838 $('head').append('<link rel="canonical" href="'+responseCanonical+'" />');
1839 }
1840 }else{
1841 if( $('link[rel="canonical"]').length > 0 ){
1842 $('link[rel="canonical"]').remove();
1843 }
1844 }
1845 }
1846
1847 // If Filters open button outside of posts container
1848 if( $responsePostsContainer.find('.wpc-open-button-'+setId).length < 1 ) {
1849 let wpcButtonInnerContent = $response.find('.wpc-open-button-'+setId+' .wpc-button-inner');
1850
1851 if( wpcButtonInnerContent.length > 0 ) {
1852 $('.wpc-open-button-'+setId).each( function ( bIndex, bUtton ) {
1853 if ( $(this).parent('div').hasClass('wpc-filters-main-wrap') ){
1854 return true;
1855 }
1856 $(this).find(".wpc-button-inner").replaceWith( wpcButtonInnerContent[0] );
1857 } );
1858 }
1859 }
1860
1861 window.history.pushState({wpcHandler: 'wpcFilterEverything'}, null, link);
1862
1863 prevState = currentState;
1864 } else {
1865 if ( $(targetPostsContainer).length === 0 && wpcFilterFront.wpcAjaxEnabled ) {
1866 alert( noPostsContainerMsg );
1867 }
1868 }
1869
1870 let wpcPostsFound = $response.find('.'+widgetClass).find('.wpc-posts-found').data('found');
1871 wpcPostsFound = parseFloat( wpcPostsFound );
1872
1873 // Chips
1874 wpcReloadChips( $response );
1875
1876 // Sorting widget
1877 wpcReloadSorting( $response );
1878
1879 // Filters Widget. It modifies $response so it is better to fire it in the end
1880 wpcReloadFiltersWidget( $response, widgetClass );
1881
1882
1883 //trigger events
1884 $(document).trigger( 'ready' );
1885 $(window).trigger( 'scroll' );
1886 $(window).trigger( 'resize' );
1887
1888 // a3 Lazy Load support
1889 $(window).trigger( 'lazyshow' );
1890
1891 wpcFixWoocommerceOrder();
1892
1893 //check rating stars
1894 flrtStarCheck();
1895
1896 let applyButtonFilterSet = false;
1897 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes( setId ) ){
1898 applyButtonFilterSet = true;
1899 }
1900
1901 if( ! wpcIsMobile && wpcAutoScroll && ( wpcPostsFound < wpcPostsPerPage[setId] || applyButtonFilterSet ) ){
1902 if( $(targetPostsContainer).length > 0 ){
1903 $('body, html').animate({ scrollTop:$(targetPostsContainer).offset().top - wpcAutoScrollOffset });
1904 }
1905 }
1906
1907 // Re-init Elementor actions
1908 if( typeof( elementorFrontend ) !== 'undefined' ){
1909 if( $responsePostsContainer.hasClass('elementor-element') ){
1910 $(targetPostsContainer+'.elementor-element').each(
1911 function() {
1912 elementorFrontend.elementsHandler.runReadyTrigger($(this));
1913 }
1914 );
1915 } else {
1916 $(targetPostsContainer+' .elementor-element').each(
1917 function() {
1918 elementorFrontend.elementsHandler.runReadyTrigger($(this));
1919 }
1920 );
1921 }
1922 }
1923 }
1924 },
1925
1926 error: function (response) {
1927 wpcHideSpinner();
1928 let $a_el = $(widget).find('.wpc-filters-apply-button');
1929 let oldLink = $a_el.data('href');
1930 $a_el.attr('href', oldLink );
1931 }
1932 });
1933
1934 }
1935
1936 function handleMetaTag( tagName, response )
1937 {
1938
1939 let tagContent = $(response).find('meta[name="'+tagName+'"]').attr('content');
1940 if( typeof tagContent !== 'undefined' ){
1941 // Replace content if tag exists
1942 if( $('meta[name="'+tagName+'"]').length > 0 ){
1943 $('meta[name="'+tagName+'"]').attr('content', tagContent );
1944 } else {
1945 // Append meta tag
1946 $('head').append('<meta name="'+tagName+'" content="'+tagContent+'" />');
1947 }
1948 }else{
1949 if( $('meta[name="'+tagName+'"]').length > 0 ){
1950 $('meta[name="'+tagName+'"]').remove();
1951 }
1952 }
1953 }
1954
1955 function wpcFixWoocommerceOrder() {
1956 $('.woocommerce-ordering').on('change', 'select.orderby', function () {
1957 $(this).closest('form').submit();
1958 });
1959 }
1960
1961 // The found-posts value must always be bare digits — the button template
1962 // supplies the literal parentheses around the span
1963 function wpcBareCount( value ) {
1964 return String( value === undefined || value === null ? '' : value ).replace( /[()\s]/g, '' );
1965 }
1966
1967 function wpcReloadFiltersWidget( $response, widgetClass ){
1968 // Replace parts
1969 // let targetWidget = '.'+widgetClass;
1970 // let $response = $response;
1971 // It seems we need to reload all widgets available on the page
1972 if( wpcIsMobile === true && ( wpcFilterFront.showBottomWidget === 'yes' ) ){
1973
1974 $(wpcWidgetContainer).each( function ( index, widget ){
1975 let widgetSet = $(widget).data('set');
1976 let widgetClass = '.wpc-filter-set-'+widgetSet;
1977
1978 // .wpc-filters-scroll-container
1979 // .wpc-filters-widget-containers-wrapper
1980 let newWidget = $response.find(widgetClass+' .wpc-filters-scroll-container');
1981 let newPostsFound = $response.find(widgetClass+' .wpc-filters-found-posts');
1982
1983 // Replace all filters and chips
1984 if( newWidget.length > 0 ){
1985 $(widgetClass).find('.wpc-filters-scroll-container').replaceWith( newWidget );
1986 }
1987 // Replace found posts number. The template already wraps this
1988 // span in literal parentheses, so the value written INSIDE must
1989 // be bare digits — wpcBareCount() strips parens that a stale
1990 // cached widget response may still carry, which otherwise
1991 // rendered as "Show ((60))".
1992 if( newPostsFound.length > 0 ){
1993 $(widgetClass).find('.wpc-filters-found-posts').html( wpcBareCount( newPostsFound.html() ) );
1994 }
1995
1996 if( wpcApplyButtonSets.includes( widgetSet ) ){
1997 let applyLink = $(widgetClass+" .wpc-filters-submit-button").attr('href');
1998 if( applyLink !== '' ){
1999 $(".wpc-filters-widget-controls-container .wpc-filters-submit-button").attr('href', applyLink);
2000 }
2001 }
2002 });
2003
2004 } else {
2005 $(wpcWidgetContainer).each( function ( index, widget ) {
2006 let widgetSet = $(widget).data('set');
2007 let widgetClass = '.wpc-filter-set-'+widgetSet;
2008
2009 let newWidget = $response.find(widgetClass);
2010 if (newWidget.length > 0) {
2011 $(widgetClass).replaceWith(newWidget);
2012 }
2013 });
2014 }
2015 }
2016
2017 function wpcReloadSorting( $response ){
2018 let wpcSortingForms = $response.find('.wpc-sorting-form');
2019 if ( wpcSortingForms.length < 1 ) {
2020 return;
2021 }
2022 let originalSortingForms = $(".wpc-sorting-form");
2023
2024 if( wpcSortingForms.length > 0 ){
2025 wpcSortingForms.each( function ( index, elem ){
2026 originalSortingForms[index].replaceWith(elem);
2027 });
2028 }
2029 }
2030
2031 function wpcReloadChips( $response ){
2032 let $chips = $(".wpc-filter-chips-list");
2033 if ( $chips.length < 1 ) {
2034 return;
2035 }
2036
2037 $chips.each( function ( index, chipsWidget ) {
2038
2039 if( ( wpcIsMobile === true && ( wpcFilterFront.showBottomWidget !== 'yes' ) ) || wpcIsMobile === false ){
2040 // Do not replace Chips inside Filters widget
2041 if ( $(this).parent('div').hasClass('wpc-inner-widget-chips-wrapper') ){
2042 return true;
2043 }
2044 }
2045
2046 let chipsSetCount = $(chipsWidget).data('setcount');
2047 let chipsWidgetClass = '.wpc-filter-chips-'+chipsSetCount;
2048 let newChipsInstance = $response.find(chipsWidgetClass);
2049
2050 if ( newChipsInstance.length > 0 ) {
2051 // Do not use $(this) because reloaded widget kills it
2052 $(chipsWidgetClass).replaceWith( newChipsInstance );
2053 }
2054 });
2055 }
2056
2057 window.addEventListener( 'popstate', function ( e ) {
2058 // @todo the last history step sometimes doesn't reload
2059 if( e.state !== null && e.state.hasOwnProperty('wpcHandler') ){
2060 if( e.state.wpcHandler === 'wpcFilterEverything' ){
2061 window.location.reload(true);
2062 }
2063 }
2064 });
2065
2066 //check rating stars after upload
2067 function flrtStarCheck(){
2068 if($('label.flrt-star-label-checked').length > 0){
2069 $('#flrt-wpc-term-count').removeClass('flrt-change-blocked');
2070 let ratingNumChecked = $('label.flrt-star-label-checked').data('ratingNum');
2071 let selectedAndAbove = $('.flrt-stars-wpc-filter-content').data('selectedAndAbove');
2072 flrtGetRatingTermCount($('label.flrt-star-label-checked'))
2073 $('label.flrt-star-label').each(function( index ) {
2074 index += 1;
2075 if(!selectedAndAbove && index <= ratingNumChecked){
2076 $('label.flrt-rating-numb-' + index).addClass('flrt-star-label-hover');
2077 }else if(selectedAndAbove && index >= ratingNumChecked) {
2078 $('label.flrt-rating-numb-' + index).addClass('flrt-star-label-hover');
2079 }
2080 });
2081 }
2082 }
2083 flrtStarCheck();
2084
2085 $(document).on('mouseenter', 'label.flrt-star-label', function() {
2086 if(!$('#flrt-wpc-term-count').hasClass('flrt-change-blocked')) {
2087 let ratingNum = $(this).data('ratingNum');
2088 let selectedAndAbove = $('.flrt-stars-wpc-filter-content').data('selectedAndAbove');
2089 $('label.flrt-remove-star-check').removeClass('flrt-star-label-not-checked');
2090 flrtGetRatingTermCount($(this));
2091 for (let i = 0; i <= 5; i++) {
2092 if (!selectedAndAbove && i <= ratingNum) {
2093 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2094 } else if (selectedAndAbove && i >= ratingNum) {
2095 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2096 } else {
2097 $('label.flrt-rating-numb-' + i).removeClass('flrt-star-label-hover');
2098 }
2099 }
2100 }
2101 });
2102
2103 $(document).on('click', 'label.flrt-star-label', function() {
2104
2105 let $el = $(this).parents(wpcWidgetContainer);
2106 let setId = $el.data('set');
2107 let applyButtonMode = false;
2108
2109 if( setId > 0 && wpcApplyButtonSets.length > 0 && wpcApplyButtonSets.includes(setId) ){
2110 applyButtonMode = true;
2111 }
2112
2113 // The blocked flag freezes hover until the widget HTML is reloaded.
2114 // In instant Apply mode no reload follows the click, so the flag
2115 // would never be cleared and hover would stay dead until a full
2116 // page refresh
2117 if( !( applyButtonMode && wpcInstantRecount ) ){
2118 $('#flrt-wpc-term-count').addClass('flrt-change-blocked');
2119 }
2120
2121 let ratingNum = $(this).data('ratingNum');
2122 let selectedAndAbove = $('.flrt-stars-wpc-filter-content').data('selectedAndAbove');
2123 $('label.flrt-remove-star-check').removeClass('flrt-star-label-not-checked');
2124
2125
2126 flrtGetRatingTermCount($(this));
2127 for (let i = 0; i <= 5; i++) {
2128 if(!selectedAndAbove && i <= ratingNum){
2129 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2130 }if(selectedAndAbove && i >= ratingNum){
2131 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2132 }
2133 }
2134
2135 if(applyButtonMode && wpcInstantRecount) {
2136 const inputId = $(this).attr('for')
2137 const input = $('#'+inputId);
2138 // Clear state classes from the previous selection before painting the
2139 // new one, otherwise -checked/-not-checked leak when switching ratings
2140 $(this).parents('.flrt-stars-wpc-filter-content').find('label.flrt-star-label')
2141 .removeClass('flrt-star-label-hover flrt-star-label-checked flrt-star-label-not-checked')
2142 for (let i = 0; i <= 5; i++) {
2143 if(!selectedAndAbove && i <= ratingNum){
2144 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover flrt-star-label-checked');
2145 }if(selectedAndAbove && i >= ratingNum){
2146 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover flrt-star-label-not-checked');
2147 }
2148 }
2149 for (let i = 0; i <= 5; i++) {
2150 if(ratingNum != i){
2151 $('label.flrt-rating-numb-' + i).data('wpc-was-checked', false);
2152 }
2153 }
2154 if ($(this).data('wpc-was-checked')) {
2155 // Deselect: clear ALL state classes at once — the same result the
2156 // input-state repaint produces for "nothing selected". Removing them
2157 // one-by-one by re-querying '.flrt-star-label-hover' left the
2158 // -checked/-not-checked classes behind after the hover class was gone
2159 $(this).parents('.flrt-stars-wpc-filter-content').find('label.flrt-star-label')
2160 .removeClass('flrt-star-label-hover flrt-star-label-checked flrt-star-label-not-checked')
2161 $(this).data('wpc-was-checked', false)
2162 } else {
2163 $(this).data('wpc-was-checked', true);
2164 }
2165 flrtGetRatingTermCount($(this), applyButtonMode);
2166 /*$(this).parents('.flrt-stars-wpc-filter-content')
2167 .find('.flrt-star-label-hover')
2168 .removeClass('flrt-star-label-hover');*/
2169 }
2170
2171 });
2172
2173 $(document).on('mouseleave', '.flrt-stars-filter', function() {
2174 if(!$('#flrt-wpc-term-count').hasClass('flrt-change-blocked')){
2175 // The checked input is the single source of truth for the selected
2176 // rating: in instant Apply mode the client repaint puts
2177 // .flrt-star-label-checked on EVERY label up to the selected one,
2178 // so reading ratingNum from the first such label restores a wrong
2179 // (always one-star) state
2180 const $checkedInput = $(this).find('input.flrt-star-input:checked');
2181 if($checkedInput.length > 0){
2182 let ratingNumChecked = Number($checkedInput.data('ratingNum'));
2183 let selectedAndAbove = $('.flrt-stars-wpc-filter-content').data('selectedAndAbove');
2184 flrtGetRatingTermCount($('label.flrt-rating-numb-' + ratingNumChecked))
2185 $('label.flrt-remove-star-check').addClass('flrt-star-label-not-checked');
2186 for (let i = 0; i <= 5; i++) {
2187 if(!selectedAndAbove && i <= ratingNumChecked){
2188 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2189 }else if(selectedAndAbove && i >= ratingNumChecked){
2190 $('label.flrt-rating-numb-' + i).addClass('flrt-star-label-hover');
2191 }else{
2192 $('label.flrt-rating-numb-' + i).removeClass('flrt-star-label-hover');
2193 }
2194 }
2195 }else{
2196 $('#flrt-wpc-term-count').text('');
2197 for (let i = 1; i <= 5; i++) {
2198 $('label.flrt-rating-numb-' + i).removeClass('flrt-star-label-hover');
2199 }
2200 }
2201 }
2202 });
2203
2204
2205 function flrtGetRatingTermCount(el, applyButtonMode = false){
2206
2207 if(!$('#flrt-wpc-term-count').hasClass('flrt-change-blocked') || applyButtonMode){
2208 let wpcTermCount = el.data('wpcTermCount');
2209 let selectedAndAbove = $('.flrt-stars-wpc-filter-content').data('selectedAndAbove');
2210 let showTermCount = $('.flrt-stars-wpc-filter-content').data('showTermCount');
2211 if(showTermCount) {
2212 if(selectedAndAbove){
2213 let ratingNum = el.data('ratingNum');
2214 let totalTerms = 0;
2215 for (let i = ratingNum; i <= 5; i++) {
2216 let showTermCount = $('label.flrt-rating-numb-' + i).data('wpcTermCount');
2217 totalTerms += showTermCount;
2218 }
2219 $('#flrt-wpc-term-count').text(totalTerms);
2220 }else{
2221 $('#flrt-wpc-term-count').text(wpcTermCount);
2222 }
2223 if(applyButtonMode){
2224 const checked = $('input:checked', el.parents('.flrt-stars-filter'));
2225 if(!checked.length){
2226 $('#flrt-wpc-term-count').text('');
2227 }
2228 }
2229 }
2230 }
2231 }
2232
2233
2234 // Sticky buttons: enabled lazily after first user interaction with filters
2235 let wpcStickyButtonsActivated = false;
2236 let wpcStickyForceOnActivate = false; // If true, fix immediately after first click even before chips are rendered
2237
2238 // Checks if any filter is selected (chips present, inputs checked or a
2239 // dropdown holds a non-default option — incl. its select2 rendering)
2240 function wpcHasAnyFilterSelected() {
2241 const $wrapper = $('.wpc-filters-widget-wrapper');
2242 if (!$wrapper.length) return false;
2243
2244 const hasChecks = $wrapper.find('input[type=checkbox]:checked, input[type=radio]:checked').length > 0;
2245 // Smart spans render non-indexable chips as spans — they count too
2246 const hasChips = $wrapper.find('.wpc-filter-chip a, .wpc-filter-chip [data-wpc-span-link], .wpc-filter-chip span.wpc-apply-button-chip').length > 0;
2247 const hasSelects = $wrapper.find('.wpc-filters-widget-select option:selected')
2248 .not('.wpc-dropdown-default').length > 0;
2249
2250 if (hasChecks || hasChips || hasSelects) {
2251 return true;
2252 }
2253
2254 // Numeric range and date inputs come pre-filled with their (possibly
2255 // narrowed) bounds — only a value differing from them is a selection.
2256 // Same rules as the Apply-URL builder in processElements: equality
2257 // with any bound attribute, or an applied date equal to the absolute
2258 // bound, means "not filtering".
2259 let hasRanges = false;
2260 $wrapper.find('form.wpc-filter-range-form input, form.wpc-filter-date-range-form-visible input')
2261 .not('.wpc-range-list-item').each(function () {
2262 const data = $(this).data();
2263 let val = $(this).val();
2264 if (typeof data.wpcEName === 'undefined' || val === '') return;
2265 if (typeof data.wpcTempFrom !== 'undefined' && typeof data.wpcAbsFrom !== 'undefined' && data.wpcTempFrom === data.wpcAbsFrom) return;
2266 if (typeof data.wpcTempTo !== 'undefined' && typeof data.wpcAbsTo !== 'undefined' && data.wpcTempTo === data.wpcAbsTo) return;
2267 if (typeof data.wpcAbsFrom !== 'undefined' && data.wpcAbsFrom === val) return;
2268 if (typeof data.wpcAbsTo !== 'undefined' && data.wpcAbsTo === val) return;
2269 if (typeof data.min !== 'undefined' || typeof data.max !== 'undefined') {
2270 val = Number(val);
2271 }
2272 if (typeof data.absMin !== 'undefined' && data.absMin === val) return;
2273 if (typeof data.min !== 'undefined' && data.min === val) return;
2274 if (typeof data.absMax !== 'undefined' && data.absMax === val) return;
2275 if (typeof data.max !== 'undefined' && data.max === val) return;
2276 hasRanges = true;
2277 return false;
2278 });
2279
2280 if (hasRanges) {
2281 return true;
2282 }
2283
2284 const $search = $wrapper.find('.wpc-filter-search-form input.wpc-search-field');
2285 return $search.length > 0 && String($search.val() || '') !== '';
2286 }
2287
2288 // Enable sticky buttons after user interaction; attach handlers once
2289 function wpcEnableStickyButtons(force = false) {
2290 if (!wpcStickyButtonsActivated) {
2291 const handler = wpcDebounce(wpcUpdateStickyButtons, 0);
2292 $(window).on('scroll.wpcStickyButtons', handler);
2293 wpcStickyButtonsActivated = true;
2294 }
2295
2296 // Force immediate fixation on first click (no need to scroll)
2297 if (force) {
2298 wpcStickyForceOnActivate = true;
2299 }else{
2300 wpcStickyForceOnActivate = false
2301 }
2302
2303 // Perform initial calculation right away so buttons become fixed immediately after click
2304 wpcUpdateStickyButtons();
2305 }
2306
2307
2308
2309 // Updates fixed positioning of sticky buttons based on scroll position
2310 function wpcUpdateStickyButtons() {
2311 const $allButtons = $('.wpc-sticky-buttons');
2312
2313 // If not activated yet, keep buttons unfixed
2314 if (!wpcStickyButtonsActivated) {
2315 return;
2316 }
2317
2318 $allButtons.each(function () {
2319 const $buttons = $(this);
2320 const el = $buttons[0];
2321 if (!el) return;
2322 const stickyButtonsWidth = el.getBoundingClientRect().width;
2323
2324 const rect = el.getBoundingClientRect();
2325 const styles = window.getComputedStyle(el);
2326 const marginTop = parseFloat(styles.marginTop) || 0;
2327 const marginBottom = parseFloat(styles.marginBottom) || 0;
2328 const stickyButtonsHeight = rect.height;
2329
2330 if (stickyButtonsWidth === 0 || stickyButtonsHeight === 0) {
2331 return;
2332 }
2333
2334
2335
2336 const stickyButtonsHeightWithMargin = rect.height + marginTop + marginBottom;
2337 const $wrapper = $buttons.closest('.wpc-filters-scroll-container');
2338
2339 if (!wpcStickyForceOnActivate && !wpcHasAnyFilterSelected()) {
2340 if ($buttons.hasClass('wpc-is-fixed-apply-button')) {
2341 $buttons.removeClass('wpc-is-fixed-apply-button').attr('style', '');
2342 }
2343 const prevPos = $wrapper.data('wpc-prev-position');
2344 if (typeof prevPos !== 'undefined') {
2345 $wrapper.css('position', prevPos);
2346 $wrapper.removeData('wpc-prev-position');
2347 }
2348
2349 const $ph = $buttons.prev('.wpc-sticky-placeholder');
2350 if ($ph.length) $ph.remove();
2351 return;
2352 }
2353
2354
2355 if (!$wrapper.length) {
2356 return;
2357 }
2358
2359 const winTop = $(window).scrollTop();
2360 const winH = $(window).height();
2361 const winBottom = winTop + winH;
2362
2363 const wrOffset = $wrapper.offset();
2364 if (!wrOffset) {
2365 return;
2366 }
2367
2368 const wrTop = wrOffset.top;
2369 const wrHeight = $wrapper.outerHeight();
2370 const wrBottom = wrTop + wrHeight;
2371
2372
2373 const ensurePlaceholder = (h) => {
2374 let $ph = $buttons.prev('.wpc-sticky-placeholder');
2375 if (!$ph.length) {
2376 $ph = $('<div class="wpc-sticky-placeholder" aria-hidden="true"></div>');
2377 $buttons.before($ph);
2378 }
2379 $ph.css('height', stickyButtonsHeightWithMargin + 'px');
2380 return $ph;
2381 };
2382
2383
2384
2385 const btnH = $buttons.outerHeight();
2386
2387 const wrLeft = wrOffset.left;
2388
2389 const buttonsStyles = window.getComputedStyle(el);
2390 const buttonsTop = ($('#wpadminbar').length > 0 ? $('#wpadminbar').outerHeight() : 0) + (parseFloat(buttonsStyles.getPropertyValue('--sticky-top')) || parseFloat(el.dataset.stickyTop) || 16);
2391 parseFloat(el.dataset.stickyTop) || 16;
2392 const buttonsBottom = parseFloat(buttonsStyles.getPropertyValue('--sticky-bottom')) ||
2393 parseFloat(el.dataset.stickyBottom) || 16;
2394
2395 let currentPosition = $buttons.css('position');
2396 let currentTop = currentPosition === 'fixed' ? parseFloat($buttons.css('top')) : null;
2397 let currentBottom = currentPosition === 'fixed' ? parseFloat($buttons.css('bottom')) : null;
2398
2399 let css = {
2400 position: 'fixed',
2401 left: wrLeft + 'px',
2402 bottom: 0,
2403 top: 'auto',
2404 width: stickyButtonsWidth + 'px',
2405 zIndex: 9999,
2406 }
2407
2408 if (currentPosition === 'fixed') {
2409 if (currentTop !== null) {
2410 css.top = currentTop + 'px';
2411 }
2412 if (currentBottom !== null) {
2413 css.bottom = currentBottom + 'px';
2414 }
2415 }
2416
2417 const $ph = ensurePlaceholder(btnH);
2418
2419 if ($ph.length) {
2420 let buttonsPlaceholderOffset = $('.wpc-sticky-placeholder').offset();
2421 buttonsPlaceholderOffset.bottom = buttonsPlaceholderOffset.top + $('.wpc-sticky-placeholder').outerHeight();
2422
2423 if (winTop > buttonsPlaceholderOffset.top) {
2424 $buttons.addClass('wpc-is-fixed-apply-button');
2425 css.top = buttonsTop + 'px';
2426 css.left = wrLeft + 'px';
2427 css.bottom = 'auto';
2428 $buttons.css(css);
2429 } else if (winBottom < buttonsPlaceholderOffset.bottom) {
2430 $buttons.addClass('wpc-is-fixed-apply-button');
2431 css.top = 'auto';
2432 css.bottom = buttonsBottom + 'px';
2433 $buttons.css(css);
2434 }else{
2435 $('.wpc-sticky-placeholder').remove();
2436 css.position = 'relative';
2437 css.top = 0;
2438 css.left = 0;
2439 $buttons.css(css);
2440 $buttons.removeClass('wpc-is-fixed-apply-button');
2441 }
2442 }
2443 });
2444 }
2445
2446
2447 function wpcDebounce(fn, wait) {
2448 let t;
2449 return function() {
2450 clearTimeout(t);
2451 const args = arguments;
2452 const ctx = this;
2453 t = setTimeout(function() { fn.apply(ctx, args); }, wait);
2454 }
2455 }
2456
2457 $(function() {
2458 if (wpcHasAnyFilterSelected() && window.innerWidth > wpcMobileWidth ) {
2459 wpcEnableStickyButtons(true);
2460 }
2461 });
2462
2463
2464
2465 $.fn.tipTip = function(options) {
2466 var defaults = {
2467 activation: "hover",
2468 keepAlive: false,
2469 maxWidth: "200px",
2470 edgeOffset: 3,
2471 defaultPosition: "bottom",
2472 delay: 400,
2473 fadeIn: 200,
2474 fadeOut: 200,
2475 attribute: "title",
2476 content: false, // HTML or String to fill TipTIp with
2477 enter: function(){},
2478 exit: function(){}
2479 };
2480 var opts = $.extend(defaults, options);
2481
2482 // Setup tip tip elements and render them to the DOM
2483 if($("#tiptip_holder").length <= 0){
2484 var tiptip_holder = $('<div id="tiptip_holder" style="max-width:'+ opts.maxWidth +';"></div>');
2485 var tiptip_content = $('<div id="tiptip_content"></div>');
2486 var tiptip_arrow = $('<div id="tiptip_arrow"></div>');
2487 $("body").append(tiptip_holder.html(tiptip_content).prepend(tiptip_arrow.html('<div id="tiptip_arrow_inner"></div>')));
2488 } else {
2489 var tiptip_holder = $("#tiptip_holder");
2490 var tiptip_content = $("#tiptip_content");
2491 var tiptip_arrow = $("#tiptip_arrow");
2492 }
2493
2494 return this.each(function(){
2495 var org_elem = $(this);
2496 if(opts.content){
2497 var org_title = opts.content;
2498 } else {
2499 var org_title = org_elem.attr(opts.attribute);
2500 }
2501 if(org_title != ""){
2502 if(!opts.content){
2503 org_elem.removeAttr(opts.attribute); //remove original Attribute
2504 }
2505 var timeout = false;
2506
2507 if(opts.activation == "hover"){
2508 org_elem.hover(function(){
2509 active_tiptip();
2510 }, function(){
2511 if(!opts.keepAlive || !tiptip_holder.is(':hover')){
2512 deactive_tiptip();
2513 }
2514 });
2515 if(opts.keepAlive){
2516 tiptip_holder.hover(function(){}, function(){
2517 deactive_tiptip();
2518 });
2519 }
2520 } else if(opts.activation == "focus"){
2521 org_elem.focus(function(){
2522 active_tiptip();
2523 }).blur(function(){
2524 deactive_tiptip();
2525 });
2526 } else if(opts.activation == "click"){
2527 org_elem.click(function(){
2528 active_tiptip();
2529 return false;
2530 }).hover(function(){},function(){
2531 if(!opts.keepAlive){
2532 deactive_tiptip();
2533 }
2534 });
2535 if(opts.keepAlive){
2536 tiptip_holder.hover(function(){}, function(){
2537 deactive_tiptip();
2538 });
2539 }
2540 }
2541
2542 function active_tiptip(){
2543 opts.enter.call(this);
2544 tiptip_content.html(org_title);
2545 tiptip_holder.hide().removeAttr("class").css("margin","0");
2546 tiptip_arrow.removeAttr("style");
2547
2548 var top = parseInt(org_elem.offset()['top']);
2549 var left = parseInt(org_elem.offset()['left']);
2550 var org_width = parseInt(org_elem.outerWidth());
2551 var org_height = parseInt(org_elem.outerHeight());
2552 var tip_w = tiptip_holder.outerWidth();
2553 var tip_h = tiptip_holder.outerHeight();
2554 var w_compare = Math.round((org_width - tip_w) / 2);
2555 var h_compare = Math.round((org_height - tip_h) / 2);
2556 var marg_left = Math.round(left + w_compare);
2557 var marg_top = Math.round(top + org_height + opts.edgeOffset);
2558 var t_class = "";
2559 var arrow_top = "";
2560 var arrow_left = Math.round(tip_w - 12) / 2;
2561
2562 if(opts.defaultPosition == "bottom"){
2563 t_class = "_bottom";
2564 } else if(opts.defaultPosition == "top"){
2565 t_class = "_top";
2566 } else if(opts.defaultPosition == "left"){
2567 t_class = "_left";
2568 } else if(opts.defaultPosition == "right"){
2569 t_class = "_right";
2570 }
2571
2572 var right_compare = (w_compare + left) < parseInt($(window).scrollLeft());
2573 var left_compare = (tip_w + left) > parseInt($(window).width());
2574
2575 if((right_compare && w_compare < 0) || (t_class == "_right" && !left_compare) || (t_class == "_left" && left < (tip_w + opts.edgeOffset + 5))){
2576 t_class = "_right";
2577 arrow_top = Math.round(tip_h - 13) / 2;
2578 arrow_left = -12;
2579 marg_left = Math.round(left + org_width + opts.edgeOffset);
2580 marg_top = Math.round(top + h_compare);
2581 } else if((left_compare && w_compare < 0) || (t_class == "_left" && !right_compare)){
2582 t_class = "_left";
2583 arrow_top = Math.round(tip_h - 13) / 2;
2584 arrow_left = Math.round(tip_w);
2585 marg_left = Math.round(left - (tip_w + opts.edgeOffset + 5));
2586 marg_top = Math.round(top + h_compare);
2587 }
2588
2589 var top_compare = (top + org_height + opts.edgeOffset + tip_h + 8) > parseInt($(window).height() + $(window).scrollTop());
2590 var bottom_compare = ((top + org_height) - (opts.edgeOffset + tip_h + 8)) < 0;
2591
2592 if(top_compare || (t_class == "_bottom" && top_compare) || (t_class == "_top" && !bottom_compare)){
2593 if(t_class == "_top" || t_class == "_bottom"){
2594 t_class = "_top";
2595 } else {
2596 t_class = t_class+"_top";
2597 }
2598 arrow_top = tip_h;
2599 marg_top = Math.round(top - (tip_h + 5 + opts.edgeOffset));
2600 } else if(bottom_compare | (t_class == "_top" && bottom_compare) || (t_class == "_bottom" && !top_compare)){
2601 if(t_class == "_top" || t_class == "_bottom"){
2602 t_class = "_bottom";
2603 } else {
2604 t_class = t_class+"_bottom";
2605 }
2606 arrow_top = -12;
2607 marg_top = Math.round(top + org_height + opts.edgeOffset);
2608 }
2609
2610 if(t_class == "_right_top" || t_class == "_left_top"){
2611 marg_top = marg_top + 5;
2612 } else if(t_class == "_right_bottom" || t_class == "_left_bottom"){
2613 marg_top = marg_top - 5;
2614 }
2615 if(t_class == "_left_top" || t_class == "_left_bottom"){
2616 marg_left = marg_left + 5;
2617 }
2618 tiptip_arrow.css({"margin-left": arrow_left+"px", "margin-top": arrow_top+"px"});
2619 tiptip_holder.css({"margin-left": marg_left+"px", "margin-top": marg_top+"px"}).attr("class","tip"+t_class);
2620
2621 if (timeout){ clearTimeout(timeout); }
2622 timeout = setTimeout(function(){ tiptip_holder.stop(true,true).fadeIn(opts.fadeIn); }, opts.delay);
2623 }
2624
2625 function deactive_tiptip(){
2626 opts.exit.call(this);
2627 if (timeout){ clearTimeout(timeout); }
2628 tiptip_holder.fadeOut(opts.fadeOut);
2629 }
2630 }
2631 });
2632 }
2633
2634 /* =========================================================================
2635 * Apply-button recount engine.
2636 *
2637 * Client-side counters recount, chips and apply-URL building for Filter
2638 * Sets with use_apply_button = 'yes'. On such pages the server prints
2639 * window.wpcFilterJsonData BEFORE this script (Plugin::inlineScriptJsonData,
2640 * wp_add_inline_script 'before'). On every other page the guard below
2641 * replaces the whole engine with no-ops, so none of its code can run.
2642 * ========================================================================= */
2643 const wpcApplyEngine = (function () {
2644
2645 if (typeof window.wpcFilterJsonData === 'undefined' && typeof window.wpcFilterJsonDataPromise === 'undefined') {
2646 const noop = function () {};
2647 return {
2648 applyJsMode: noop,
2649 compareInputWithRangeList: noop,
2650 unsetChip: noop,
2651 updateWpcFilterJsonData: noop
2652 };
2653 }
2654
2655 // Static-file delivery resolves the data asynchronously; if a user
2656 // interaction beats the blob fetch, park it on the promise (all engine
2657 // entry points are fire-and-forget, so deferral is transparent and the
2658 // registration order of .then callbacks preserves the call order)
2659 function wpcWhenDataReady(fn) {
2660 if (typeof window.wpcFilterJsonData !== 'undefined') {
2661 fn();
2662 return;
2663 }
2664 if (window.wpcFilterJsonDataPromise) {
2665 wpcShowSpinner();
2666 window.wpcFilterJsonDataPromise.then(function () {
2667 wpcHideSpinner();
2668 if (typeof window.wpcFilterJsonData !== 'undefined') fn();
2669 });
2670 }
2671 }
2672
2673 // Array.prototype.push(...arr) / Math.min(...arr) put every element on the
2674 // CALL STACK — at 100k-product scale the term post lists exceed V8's
2675 // argument limit and throw "Maximum call stack size exceeded"; loop instead.
2676 // Min/max coerce like Math.min does: meta_values may carry numeric strings.
2677 function wpcPushAll(target, items) {
2678 for (let i = 0; i < items.length; i++) target.push(items[i]);
2679 return target;
2680 }
2681 function wpcArrayMin(items) {
2682 let min = Infinity;
2683 for (let i = 0; i < items.length; i++) {
2684 const v = Number(items[i]);
2685 if (v < min) min = v;
2686 }
2687 return min;
2688 }
2689 function wpcArrayMax(items) {
2690 let max = -Infinity;
2691 for (let i = 0; i < items.length; i++) {
2692 const v = Number(items[i]);
2693 if (v > max) max = v;
2694 }
2695 return max;
2696 }
2697
2698 function updateCounters($el, setId) {
2699
2700 const filterSetData = wpcFilterJsonData[setId];
2701 filterSetData.chips = [];
2702 filterSetData.tempFilteredAllPostsIds = {};
2703 filterSetData.tempFilteredTerms = {};
2704 delete filterSetData.filteredPostsIds;
2705
2706 // Needed by the collectors below: filtering parent-keyed dates against the
2707 // expanded universe alone loses every variable product from a date selection
2708 const { inSetUniverse } = wpcUniverse(setId);
2709
2710
2711 const $all = $('input:checked:not(.wpc-range-list-item), option:selected:not(.wpc-range-list-item), form.wpc-filter-range-form input:not(.wpc-range-list-item), form.wpc-filter-date-range-form-visible input, input[data-wpc-was-checked=true]', $el);
2712
2713 //const allPostsSet = new Set(getAllPostsIdsInArray(setId));
2714
2715
2716 const getEntityItems = (data) => {
2717 let wpcSlug = wpcTermSlug(data.wpcSlug);
2718
2719 if (typeof data.min !== 'undefined') wpcSlug = 'min';
2720 if (typeof data.max !== 'undefined') wpcSlug = 'max';
2721 if (typeof data.wpcTempFrom !== 'undefined') wpcSlug = 'from';
2722 if (typeof data.wpcTempTo !== 'undefined') wpcSlug = 'to';
2723
2724 const allEntityItems = filterSetData.allEntities[data.wpcEName].items;
2725 for (const item of Object.values(allEntityItems)) {
2726 if (item.slug == wpcSlug) return item.posts;
2727 }
2728 return [];
2729 };
2730
2731 const getAllEntityItemsForRange = (data) => {
2732 let wpcSlug = wpcTermSlug(data.wpcSlug);
2733 if(data.min === undefined && data.max === undefined){
2734 return;
2735 }
2736 if (typeof data.min !== 'undefined') wpcSlug = 'min';
2737 if (typeof data.max !== 'undefined') wpcSlug = 'max';
2738
2739 const allEntityItems = filterSetData.allEntities[data.wpcEName].items;
2740 for (const [key, item] of Object.entries(allEntityItems)) {
2741 if (item.slug === wpcSlug){
2742 wpcFilterJsonData[setId].allEntities[data.wpcEName].items[key].posts = Object.keys(item.meta_values).map(Number);
2743 return Object.keys(item.meta_values).map(Number);
2744 }
2745 }
2746 return [];
2747 };
2748
2749 const filterRangeItems = ($currentEl, postsIds, setId) => {
2750 const data = $currentEl.data();
2751 const postsArray = Array.isArray(postsIds) ? postsIds : Object.values(postsIds);
2752 const filteredPostsIds = [];
2753
2754 if (typeof data.min !== 'undefined' || typeof data.max !== 'undefined') {
2755 const $form = $currentEl.parents('.wpc-filter-range-form');
2756 const $minEl = $('.wpc-filters-range-min', $form);
2757 const $maxEl = $('.wpc-filters-range-max', $form);
2758 const minCurVal = +$minEl.val();
2759 const maxCurVal = +$maxEl.val();
2760
2761 // An untouched side means NO bound: the server renders the input
2762 // VALUE and data-min/max as placeholders scoped to the CURRENT
2763 // result set, so using them as limits kept phantom constraints
2764 // after another selection changed (numeric twin of the untouched-
2765 // date fix). Untouched = value equals the Abs bound OR the current
2766 // placeholder — exactly the checks buildUrlForApplyButton uses
2767 // before omitting the query param.
2768 const minUntouched = ($minEl.data().absMin === minCurVal) || (Number($minEl.data().min) === minCurVal);
2769 const maxUntouched = ($maxEl.data().absMax === maxCurVal) || (Number($maxEl.data().max) === maxCurVal);
2770
2771 if (minUntouched && maxUntouched) {
2772 return postsIds;
2773 }
2774
2775 const minBound = minUntouched ? -Infinity : minCurVal;
2776 const maxBound = maxUntouched ? Infinity : maxCurVal;
2777
2778 let filterPostsIds = [];
2779 for (const postId of postsArray) {
2780 if(inSetUniverse(postId)){
2781 filterPostsIds.push(+postId);
2782 }
2783 }
2784 const entityNumberValues = filterSetData.allEntities[data.wpcEName].items.min.meta_values;
2785 for (const postId of filterPostsIds) {
2786 const postValues = entityNumberValues[postId];
2787 if (postValues !== undefined) {
2788 for (let postVal of postValues) {
2789 if (postVal >= minBound && postVal <= maxBound) {
2790 filteredPostsIds.push(postId);
2791 }
2792 }
2793 }
2794 }
2795 return filteredPostsIds;
2796 }
2797
2798
2799 if (typeof data.wpcTempFrom !== 'undefined' || typeof data.wpcTempTo !== 'undefined') {
2800 const $form = $currentEl.parents('.wpc-filters-range-inputs form');
2801 const $fromEl = $('.wpc-filters-range-from.hasDatepicker', $form);
2802 const $toEl = $('.wpc-filters-range-to.hasDatepicker', $form);
2803 const from = $fromEl.data('wpcTempFrom');
2804 const to = $toEl.data('wpcTempTo');
2805
2806 if ($fromEl.data('wpcAbsFrom') === from && $toEl.data('wpcAbsTo') === to) {
2807 return postsIds;
2808 }
2809
2810 const entityDateValues = filterSetData.allEntities[data.wpcEName].items.from.meta_values;
2811 const dateType = $fromEl.data('wpcDateType');
2812 let dateFrom = from ? $fromEl[wpcPickerFn()]('getDate') : null;
2813 let dateTo = to ? $toEl[wpcPickerFn()]('getDate') : null;
2814
2815 // An untouched field means NO bound, not "clamp to the displayed
2816 // Abs value": on an applied page the server renders Abs bounds
2817 // scoped to the CURRENT result set (they are placeholders), so
2818 // clamping cut off products outside the previous selection when
2819 // another filter changed. Mirrors buildUrlForApplyButton, which
2820 // omits the date param for untouched fields.
2821 if($fromEl.val() === $fromEl.data('wpcAbsFrom')){
2822 dateFrom = null;
2823 }
2824
2825 if($toEl.val() === $toEl.data('wpcAbsTo')){
2826 dateTo = null;
2827 }
2828
2829 // meta_values dates are minute-precision (seconds zeroed) — keep
2830 // picked boundaries in the same precision
2831 if (dateFrom) dateFrom.setSeconds(0, 0);
2832 if (dateTo) dateTo.setSeconds(0, 0);
2833
2834
2835 let todayStr = null;
2836 if (dateType === 'time') {
2837 const t = new Date();
2838 todayStr = `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
2839 }
2840
2841 //const variationMap = wpcFilterJsonData.product_variations_map;
2842
2843
2844 // Candidates come from the meta_values KEYS, not from item.posts:
2845 // the server intersects item.posts with the variation-expanded
2846 // universe (variable parents replaced by variation ids there),
2847 // while post dates are keyed by PARENT ids — using item.posts
2848 // dropped every variable product from a date selection
2849 let filterPostsIds = [];
2850 for (const postId of Object.keys(entityDateValues).map(Number)) {
2851 if(inSetUniverse(postId)){
2852 filterPostsIds.push(postId);
2853 }
2854 }
2855
2856 for (const postId of filterPostsIds) {
2857 const dateValStr = entityDateValues[postId];
2858 if (!dateValStr) continue;
2859
2860 let dateVal;
2861 if (dateType === 'time') {
2862 const timePart = dateValStr.substring(dateValStr.indexOf(' ') + 1);
2863 if (!timePart) continue;
2864 dateVal = new Date(`${todayStr} ${timePart}`);
2865 } else {
2866 dateVal = new Date(dateValStr);
2867 }
2868
2869 const isAfterFrom = !dateFrom || dateVal >= dateFrom;
2870 const isBeforeTo = !dateTo || dateVal <= dateTo;
2871 if (isAfterFrom && isBeforeTo){
2872 filteredPostsIds.push(postId);
2873 }
2874 }
2875 }
2876
2877 return filteredPostsIds;
2878 };
2879
2880
2881 const filterLogic = (entityName, postIds, logic) => {
2882 let posts;
2883
2884 if (logic === 'and' && postIds.length > 1) {
2885
2886 let set = new Set(postIds[0]);
2887 for (let i = 1; i < postIds.length; i++) {
2888 set = new Set(postIds[i].filter(id => set.has(id)));
2889 }
2890 posts = [...set];
2891 } else {
2892 posts = [...new Set(postIds.flat())];
2893 }
2894
2895 return posts;
2896 };
2897
2898
2899
2900 const countTerms = ($elements, setId) => {
2901 const tempFilteredAllPostsIds = {};
2902 const tempFilteredTerms = {};
2903 const setData = wpcFilterJsonData[setId];
2904
2905
2906 $elements.each((index, el) => {
2907 const $currentEl = $(el);
2908 const data = $currentEl.data();
2909 if (typeof data.wpcEName === 'undefined' || typeof data.wpcSlug === 'undefined') return;
2910
2911 let currentPostsIds = getEntityItems(data);
2912 if (!tempFilteredTerms[data.wpcEName]) {
2913 tempFilteredTerms[data.wpcEName] = [];
2914 }
2915 tempFilteredTerms[data.wpcEName].push(wpcTermSlug(data.wpcSlug));
2916
2917 const isRange = typeof data.min !== 'undefined' || typeof data.max !== 'undefined';
2918 const isDate = typeof data.wpcTempFrom !== 'undefined' || typeof data.wpcTempTo !== 'undefined';
2919 const { used_for_variations: usedForVariations } = setData.allEntities[data.wpcEName].filter;
2920 const isUsedForVariations = usedForVariations === 'yes' || usedForVariations === true;
2921
2922 if (isRange || isDate) {
2923 if (isRange) {
2924 currentPostsIds = getAllEntityItemsForRange(data);
2925 const $form = $currentEl.parents('.wpc-filter-range-form');
2926 const $minEl = $('.wpc-filters-range-min', $form);
2927 const $maxEl = $('.wpc-filters-range-max', $form);
2928 const min = $minEl.data().min;
2929 const max = $maxEl.data().max;
2930
2931 if (min !== +$minEl.val() || max !== +$maxEl.val()) {
2932 currentPostsIds = filterRangeItems($currentEl, currentPostsIds, setId);
2933 if (currentPostsIds.length) {
2934 if (!tempFilteredAllPostsIds[data.wpcEName]) tempFilteredAllPostsIds[data.wpcEName] = [];
2935 tempFilteredAllPostsIds[data.wpcEName][index] = currentPostsIds;
2936 }
2937 collectChips($currentEl, setId);
2938 }
2939 }
2940
2941
2942 if (isDate) {
2943 const $form = $currentEl.parents('.wpc-filters-range-inputs');
2944 const $fromEl = $('.wpc-filters-range-from', $form);
2945 const $toEl = $('.wpc-filters-range-to', $form);
2946
2947 if ($fromEl.data('wpcTempFrom') !== $fromEl.data('wpcAbsFrom') ||
2948 $toEl.data('wpcTempTo') !== $toEl.data('wpcAbsTo')) {
2949
2950 currentPostsIds = filterRangeItems($currentEl, currentPostsIds, setId);
2951 if (currentPostsIds.length) {
2952 if (!tempFilteredAllPostsIds[data.wpcEName]) tempFilteredAllPostsIds[data.wpcEName] = [];
2953 tempFilteredAllPostsIds[data.wpcEName][index] = currentPostsIds;
2954 }
2955 collectChips($currentEl, setId);
2956 }
2957 }
2958
2959 } else {
2960 if (!tempFilteredAllPostsIds[data.wpcEName]) tempFilteredAllPostsIds[data.wpcEName] = [];
2961 tempFilteredAllPostsIds[data.wpcEName][index] = currentPostsIds;
2962 collectChips($currentEl, setId);
2963 }
2964 });
2965 filterSetData.tempFilteredTerms = tempFilteredTerms;
2966 for (const [entityName, postIds] of Object.entries(tempFilteredAllPostsIds)) {
2967 filterSetData.tempFilteredAllPostsIds[entityName] = filterLogic(entityName, postIds, setId);
2968 }
2969 };
2970
2971
2972
2973 function getAllPostsIdsInArray(setId) {
2974 const ids = wpcFilterJsonData[setId].allPostsIds;
2975 if (Array.isArray(ids)) return wpcFilterJsonData.allPostsIds.map(Number);
2976 return Object.keys(ids).map(Number);
2977 }
2978 const changeFiltersWithParents = ($el, setId) => {
2979 const $filtersWithParent = $('.wpc-has-parent-filter', $el);
2980 const hideEmpty = (filterSetData.settings.hide_empty !== undefined) ? wpcFilterJsonData[setId].settings.hide_empty : false;
2981 const isHideEmpty = hideEmpty === 'yes';
2982 let $is_parent_unchecked = false;
2983 $filtersWithParent.each((index, filter) => {
2984 const $filter = $(filter);
2985 const parentFilterId = $filter.data('parentFilterId');
2986 if(parentFilterId !== undefined){
2987 const hideUntilParent = ($filter.data('hideUntilParent') === 1);
2988 const filterEName = $filter.data('filterEName');
2989 const $parentFilter = $('.wpc-filters-section-' + parentFilterId, $el);
2990 const parentFilterEName = $parentFilter.data('filterEName');
2991 const $select = $filter.find('select');
2992 const $wpcDropdownDefault = $select.find('option.wpc-dropdown-default');
2993 const wpcDropdownDefaultData = $wpcDropdownDefault.data();
2994 const isSelect2 = $select.hasClass('select2-hidden-accessible');
2995 const moreLess = filterSetData.allEntities[filterEName].filter.more_less;
2996 const entity = filterSetData.allEntities[filterEName].filter.entity;
2997 const isMoreLess = moreLess === 'yes';
2998 const hideEmptyFilter = (filterSetData.settings.hide_empty_filter !== undefined) ? wpcFilterJsonData[setId].settings.hide_empty_filter : false;
2999 const isHideEmptyFilter = hideEmptyFilter === 'yes';
3000 if(
3001 filterSetData.tempFilteredAllPostsIds[parentFilterEName] !== undefined
3002 &&
3003 filterSetData.tempFilteredAllPostsIds[parentFilterEName].length > 0
3004 ){
3005 const allParentFilterPosts = filterSetData.tempFilteredAllPostsIds[parentFilterEName];
3006 if ($select.length) {
3007 $filter.find('option.wpc-term-item').each((index, elem) => {
3008 const data = $(elem).data();
3009 const entityItems = getEntityItems(data);
3010 const hasMatch = entityItems && entityItems.length > 0 &&
3011 entityItems.some(id => allParentFilterPosts.includes(Number(id)));
3012 if(hasMatch){
3013 $(elem).addClass('wpc-show-with-parent-true')
3014 if ($(elem).hasClass('wpc-has-terms')) {
3015 if(!$(elem).is(':selected')){
3016 $(elem).removeClass('wpc-show-with-parent-false');
3017 }
3018
3019 } else {
3020 if(isHideEmpty){
3021 $(elem).addClass('wpc-show-with-parent-false');
3022 }else{
3023 $(elem).removeClass('wpc-show-with-parent-false');
3024 }
3025 }
3026 }else{
3027 if(!$(elem).is(':selected')) {
3028 $(elem).addClass('wpc-show-with-parent-false').removeClass('wpc-show-with-parent-true');
3029 }
3030 }
3031 });
3032 $('.wpc-dropdown-default-' + filterEName, $el).text(wpcDropdownDefaultData.wpcDefaultOptionText)
3033 } else {
3034 $filter.find('li.wpc-term-item').each((index, elem) => {
3035 const data = $(elem).find('input').data();
3036 const entityItems = getEntityItems(data);
3037 const hasMatch = entityItems && entityItems.length > 0 &&
3038 entityItems.some(id => allParentFilterPosts.includes(Number(id)));
3039 if(hasMatch){
3040 $(elem).addClass('wpc-show-with-parent-true')
3041 if ($(elem).hasClass('wpc-has-terms')) {
3042 $(elem).removeClass('wpc-show-with-parent-false');
3043 } else {
3044 if(isHideEmpty){
3045 if(!$(elem).find('input').is(':checked')){
3046 $(elem).addClass('wpc-show-with-parent-false');
3047 }
3048 }else{
3049 $(elem).removeClass('wpc-show-with-parent-false');
3050 }
3051 }
3052 }else{
3053 if(!$(elem).find('input').is(':checked')) {
3054 $(elem).addClass('wpc-show-with-parent-false').removeClass('wpc-show-with-parent-true');
3055 }
3056 }
3057 });
3058
3059
3060
3061 let $wpcTermsItems = $filter.find(`.wpc-filters-ul-list li.wpc-show-with-parent-true`);
3062 $wpcTermsItems.removeClass('wpc-not-hidden-term');
3063 const $wpcHasTermsItems = $filter.find(`.wpc-filters-ul-list li.wpc-has-terms.wpc-show-with-parent-true`)
3064 const $wpcHasCheckedTermsItems = $filter.find(`.wpc-filters-ul-list input:checked, .wpc-filters-ul-list option:selected`)
3065 let wpcHasTerms = $wpcHasTermsItems.length || $wpcHasCheckedTermsItems.length
3066 const isHideFilterElement = (entity === 'post_meta_num' || entity === 'post_date' || entity === 'tax_numeric' || entity === 'post_meta_num');
3067
3068 if (isHideFilterElement && wpcFilterJsonData[setId].entityPostsCount[data.wpcEName] !== undefined) {
3069 wpcHasTerms = wpcFilterJsonData[setId].entityPostsCount[data.wpcEName] === true ? 1 : 0;
3070 }
3071 if(wpcHasTerms <= 0 && isHideEmptyFilter){
3072 $filter.addClass('wpc-filters-section-0');
3073 }else{
3074 $filter.removeClass('wpc-filters-section-0');
3075 }
3076
3077 // Same as in updateCountersHtml: trust the rendered
3078 // section class when the (possibly stale) JSON config
3079 // says the filter has no More/Less
3080 if(isMoreLess || $filter.hasClass('wpc-filter-more-less')){
3081 if(wpcHasTerms <= +wpcMoreLessCount){
3082 $filter.addClass('wpc-filter-few-terms');
3083 }else{
3084 $filter.removeClass('wpc-filter-few-terms');
3085 }
3086 if(isHideEmpty){
3087 $wpcTermsItems.filter('li.wpc-has-terms.wpc-show-with-parent-true').slice(0, +wpcMoreLessCount).each(function() {
3088 $(this).addClass('wpc-not-hidden-term');
3089 });
3090 }else{
3091 $wpcTermsItems.filter('li.wpc-show-with-parent-true').slice(0, +wpcMoreLessCount).each(function() {
3092 $(this).addClass('wpc-not-hidden-term');
3093 });
3094 }
3095 }
3096
3097 }
3098
3099 if($filter.find('.wpc-has-terms').length > 0 || $filter.hasClass('wpc-range-filter')){
3100 $filter.find('.wpc-ask-to-parent-display').addClass('wpc-ask-to-parent-display-none');
3101 $filter.removeClass('wpc-parent-filter-terms-unselected').addClass('wpc-parent-filter-terms-selected')
3102 if(hideUntilParent){
3103 $filter.removeClass('wpc-hide-terms-until-parent-unselected')
3104 }
3105 }else{
3106 if(isHideEmpty){
3107 $filter.find('.wpc-ask-to-parent-display').removeClass('wpc-ask-to-parent-display-none');
3108 $filter.removeClass('wpc-parent-filter-terms-selected').addClass('wpc-parent-filter-terms-unselected')
3109 if(hideUntilParent){
3110 $filter.addClass('wpc-hide-terms-until-parent-unselected')
3111 }
3112 }else{
3113 $filter.find('wpc-ask-to-parent-display-none').removeClass('.wpc-ask-to-parent-display');
3114 $filter.removeClass('wpc-parent-filter-terms-unselected').addClass('wpc-parent-filter-terms-selected')
3115 if(hideUntilParent){
3116 $filter.removeClass('wpc-hide-terms-until-parent-unselected')
3117 }
3118 }
3119 }
3120 if($filter.hasClass('wpc-range-from-elem')){
3121 $filter.find('.wpc-ask-to-parent-display').addClass('wpc-ask-to-parent-display-none');
3122 $filter.removeClass('wpc-show-range-with-parent-false')
3123 }
3124 if($filter.hasClass('wpc-stars-rating-block')){
3125 $filter.find('.wpc-ask-to-parent-display').addClass('wpc-ask-to-parent-display-none');
3126 $filter.removeClass('wpc-parent-filter-terms-unselected');
3127 }
3128 }else{
3129 if(filterSetData.tempFilteredAllPostsIds[filterEName] !== undefined
3130 &&
3131 filterSetData.tempFilteredAllPostsIds[filterEName].length > 0){
3132 $filter.addClass('wpc-child-selected-no-parent')
3133 $filter.find('input:checked').prop('checked', false);
3134 $filter.find('option:selected').prop('selected', false);
3135 delete filterSetData.tempFilteredAllPostsIds[filterEName];
3136 $is_parent_unchecked = true;
3137 }else{
3138 $filter.removeClass('wpc-child-selected-no-parent')
3139 }
3140
3141 if(hideUntilParent){
3142 $filter.addClass('wpc-hide-terms-until-parent-unselected')
3143 }
3144
3145 $filter.find('.wpc-ask-to-parent-display').removeClass('wpc-ask-to-parent-display-none');
3146 $filter.removeClass('wpc-parent-filter-terms-selected').addClass('wpc-parent-filter-terms-unselected')
3147 if($select.length) {
3148 $filter.find('option.wpc-term-item').each((index, elem) => {
3149 if(!$(elem).is(':selected')){
3150 $(elem).addClass('wpc-show-with-parent-false');
3151 }
3152 });
3153 $('.wpc-dropdown-default-' + filterEName, $el).text(wpcDropdownDefaultData.wpcSelectParentText)
3154 }else{
3155 $filter.find('li.wpc-term-item').each((index, elem) => {
3156 if(!$(elem).is(':checked')){
3157 $(elem).addClass('wpc-show-with-parent-false');
3158 }
3159 });
3160 }
3161 if($filter.hasClass('wpc-range-from-elem')){
3162 $filter.addClass('wpc-show-range-with-parent-false')
3163 $filter.find('.wpc-ask-to-parent-display').removeClass('wpc-ask-to-parent-display-none');
3164 }
3165 if($filter.hasClass('wpc-stars-rating-block')){
3166 $filter.find('.wpc-ask-to-parent-display').removeClass('wpc-ask-to-parent-display-none');
3167 $filter.addClass('wpc-parent-filter-terms-unselected');
3168 }
3169 }
3170 }
3171
3172 });
3173 if($is_parent_unchecked){
3174 applyJsMode($el, setId);
3175 }
3176 }
3177
3178 countTerms($all, setId);
3179 updateChipsList(setId);
3180 changeCounters($el, setId);
3181 changeFiltersWithParents($el, setId)
3182 }
3183
3184 function changeCounters($el, setId){
3185
3186 const $otherFilterSetsOnPage = $(wpcWidgetContainer);
3187 const hasMultipleWidgets = $otherFilterSetsOnPage.length > 1;
3188
3189 if (hasMultipleWidgets) {
3190 let tempPostIds = [];
3191 let relatedSetsIds = [];
3192 $otherFilterSetsOnPage.each((index, filterSetWidget) => {
3193 const widgetSetId = $(filterSetWidget).data('set');
3194
3195 const relatedSets = wpcFilterJsonData[widgetSetId].relatedSets;
3196
3197 if(relatedSetsIds[relatedSets] === undefined){
3198 relatedSetsIds[relatedSets] = [];
3199 }
3200 if(relatedSetsIds[relatedSets][widgetSetId] === undefined){
3201 relatedSetsIds[relatedSets][widgetSetId] = [];
3202 }
3203 relatedSetsIds[relatedSets][widgetSetId] = wpcFilterJsonData[widgetSetId].filteredPostsIds
3204
3205
3206 });
3207
3208 if (Object.keys(relatedSetsIds).length > 0) {
3209 let setParams = [];
3210 Object.entries(relatedSetsIds).forEach(([relatedSets, widgetSetIds]) => {
3211 const intersectionPosts = Object.values(widgetSetIds).reduce((acc, arr) => {
3212 const set = new Set(arr);
3213 return acc.filter(id => set.has(id));
3214 }, []);
3215 Object.entries(widgetSetIds).forEach(([relatedSetId, widgetPostIds]) => {
3216 wpcFilterJsonData[relatedSetId].filteredPostsIds = intersectionPosts;
3217 Object.entries(widgetSetIds).forEach(([relatedSetIdNext, widgetPostIdsNext]) => {
3218 if(relatedSetIdNext !== relatedSetId){
3219 const filteredPostsIds = wpcFilterJsonData[relatedSetId].tempFilteredAllPostsIds;
3220 if (filteredPostsIds && typeof filteredPostsIds === 'object') {
3221 Object.entries(filteredPostsIds).forEach(([eName, entityPostIds]) => {
3222 if(wpcFilterJsonData[relatedSetIdNext].tempFilteredAllPostsIds === undefined){
3223 wpcFilterJsonData[relatedSetIdNext].tempFilteredAllPostsIds = [];
3224 }
3225 if(typeof wpcFilterJsonData[relatedSetIdNext].tempFilteredAllPostsIds[eName] === 'undefined'){
3226 wpcFilterJsonData[relatedSetIdNext].tempFilteredAllPostsIds[eName] = entityPostIds;
3227 }
3228 });
3229 }
3230 }
3231 });
3232 });
3233 });
3234 }
3235 }
3236
3237 const setData = wpcFilterJsonData[setId];
3238 let allEntities = JSON.parse(JSON.stringify(wpcFilterJsonData[setId].allEntities));
3239 const tempFiltered = setData.tempFilteredAllPostsIds;
3240 const hasTempFilters = Object.keys(tempFiltered).length > 0;
3241 let entityPostsCount = [];
3242 let allPostsIds = getVariation(Object.keys(setData.allPostsIds), false);
3243 const { allPostsUniverseSet, allPostsExpandedSet, inSetUniverse } = wpcUniverse(setId);
3244
3245 // Final displayed count for a candidate id list. On variations-as-products
3246 // shops item.posts carry ALL variations of the matched parents (the server
3247 // clamps its final counts against the set universe in calcTermCount) —
3248 // mirror that clamp here, in variation space. On regular shops the
3249 // parent-space collapse compensates already (parents are universe-bound),
3250 // so the behavior there is unchanged.
3251 const wpcDisplayCount = (ids) => (wpcFilterJsonData.variationsAsProducts
3252 ? wpcCountSpace(ids.filter(inSetUniverse))
3253 : wpcCountSpace(ids)).length;
3254
3255 if (hasTempFilters) {
3256 for (const [tempEntityName, postIds] of Object.entries(tempFiltered)) {
3257 // Cross-entity intersections run in PARENT (product-level) space,
3258 // mirroring the server SQL where every filter is an independent
3259 // product-level clause: a product with a blue variation at 65 and a
3260 // non-blue variation at 72 DOES match color-blue + price 70..100,
3261 // even though no single variation satisfies both conditions
3262 const parentIdSet = new Set(postIds.map(wpcParentOf));
3263
3264 for (const [entityName, entity] of Object.entries(allEntities)) {
3265 const { logic, range_list_input: rangeList, used_for_variations: usedForVariations } = entity.filter;
3266 const isAndLogic = logic === 'and';
3267 const isUsedForVariations = usedForVariations === 'yes' || usedForVariations === true;
3268 const hasOnlyOneElement = Object.keys(tempFiltered).length === 1 && tempFiltered[entityName] !== undefined;
3269
3270 for (const [index, item] of Object.entries(entity.items)) {
3271 let tempPostIdsArray;
3272 if (tempEntityName === entityName) {
3273 tempPostIdsArray = isAndLogic
3274 ? item.posts.filter(x => parentIdSet.has(wpcParentOf(x)) && inSetUniverse(x))
3275 : item.posts.filter(x => inSetUniverse(x));
3276 } else {
3277 tempPostIdsArray = item.posts.filter(x => parentIdSet.has(wpcParentOf(x)));
3278 }
3279
3280 allEntities[entityName].items[index].posts = tempPostIdsArray;
3281 allEntities[entityName].items[index].count = tempPostIdsArray.length;
3282
3283 // On variations-as-products shops the DISPLAY count must be
3284 // taken from the pre-normalization list: getVariation() below
3285 // collapses non-ufv lists to parent ids for the cross-entity
3286 // intersections, but the visible catalog unit is a variation
3287 // (e.g. Variable: 467 raw -> universe-clamped 141, while the
3288 // collapsed list would count 40 parents).
3289 const wpcDisplayIds = tempPostIdsArray;
3290
3291 tempPostIdsArray = getVariation(tempPostIdsArray, isUsedForVariations);
3292
3293 // Displayed counters are distinct products (parent space), as
3294 // the server counts them — non-ufv lists also carry variation
3295 // ids alongside parents, so a plain .length over-counts. On
3296 // variations-as-products shops counts stay in variation space,
3297 // clamped to the set universe.
3298 allEntities[entityName].items[index].cross_count = wpcDisplayCount(
3299 wpcFilterJsonData.variationsAsProducts ? wpcDisplayIds : tempPostIdsArray
3300 );
3301 if(entityPostsCount[entityName] === undefined){
3302 entityPostsCount[entityName] = [];
3303 }
3304 wpcPushAll(entityPostsCount[entityName], tempPostIdsArray);
3305
3306 if (typeof rangeList === 'object') {
3307 if (hasOnlyOneElement) {
3308 tempPostIdsArray = setData.allEntities[entityName].items[index].posts;
3309 tempPostIdsArray = getVariation(tempPostIdsArray, isUsedForVariations);
3310 }else{
3311 tempPostIdsArray = setData.allEntities[entityName].items[index].posts;
3312 const rangeTempFiltered = {...tempFiltered};
3313
3314 delete rangeTempFiltered[entityName];
3315 for (const [rangeTempEntityName, rangePostIds] of Object.entries(rangeTempFiltered)) {
3316 const rangeParentSet = new Set(rangePostIds.map(wpcParentOf));
3317 tempPostIdsArray = tempPostIdsArray.filter(x => rangeParentSet.has(wpcParentOf(x)));
3318 }
3319
3320 tempPostIdsArray = getVariation(tempPostIdsArray, isUsedForVariations);
3321 }
3322
3323 for (const [indexRange, range] of Object.entries(rangeList)) {
3324 const rangeListMinVal = Number(range.range_list_min_val);
3325 const rangeListMaxVal = Number(range.range_list_max_val);
3326 let rangePostIds = [];
3327
3328 for (const postId of tempPostIdsArray) {
3329 const postValues = allEntities[entityName].items[index].meta_values[postId];
3330 if (postValues !== undefined) {
3331 for (let postVal of postValues) {
3332 postVal = Number(postVal);
3333 // inSetUniverse, not the parent-only set: with used_for_variations
3334 // the candidate ids (and meta_values keys) are VARIATION ids,
3335 // which live only in the expanded universe
3336 if (inSetUniverse(postId) &&
3337 ((rangeListMaxVal === 0 && rangeListMinVal <= postVal) ||
3338 (rangeListMinVal <= postVal && rangeListMaxVal >= postVal))) {
3339 rangePostIds.push(postId);
3340 }
3341 }
3342 }
3343 }
3344
3345 rangePostIds = getVariation(rangePostIds, isUsedForVariations);
3346
3347 allEntities[entityName].items[index].range_list_input[indexRange] = wpcDisplayCount(rangePostIds);
3348 }
3349 }
3350 }
3351 }
3352 }
3353 }
3354
3355 setData.filteredPostsIds = [];
3356
3357 if (hasTempFilters) {
3358 for (const postIds of Object.values(tempFiltered)) {
3359 wpcPushAll(setData.filteredPostsIds, postIds);
3360 }
3361 } else {
3362 for (const [indexEntity, entity] of Object.entries(allEntities)) {
3363 const { logic, range_list_input: rangeList, used_for_variations: usedForVariations } = entity.filter;
3364 const isUsedForVariations = usedForVariations === 'yes' || usedForVariations === true;
3365 for (const [indexItem, item] of Object.entries(entity.items)) {
3366 let postIds = item.posts;
3367 wpcPushAll(setData.filteredPostsIds, item.posts);
3368 postIds = getVariation(postIds, isUsedForVariations);
3369 // Same pre-normalization display source as the hasTempFilters
3370 // branch: non-ufv lists get parent-collapsed by getVariation()
3371 // right above, which under-counts on variations-as-products shops.
3372 allEntities[indexEntity].items[indexItem].cross_count = wpcDisplayCount(
3373 wpcFilterJsonData.variationsAsProducts ? item.posts : postIds
3374 );
3375
3376
3377 if(entityPostsCount[indexEntity] === undefined){
3378 entityPostsCount[indexEntity] = [];
3379 }
3380 wpcPushAll(entityPostsCount[indexEntity], postIds);
3381
3382 if (typeof rangeList === 'object') {
3383
3384 postIds = getVariation(postIds, isUsedForVariations);
3385
3386 for (const [indexRange, range] of Object.entries(rangeList)) {
3387 const rangeListMinVal = Number(range.range_list_min_val);
3388 const rangeListMaxVal = Number(range.range_list_max_val);
3389 let rangePostIds = [];
3390
3391 for (const postId of postIds) {
3392 const postValues = allEntities[indexEntity].items[indexItem].meta_values[postId];
3393 if (postValues !== undefined) {
3394 for (let postVal of postValues) {
3395 postVal = Number(postVal);
3396 // inSetUniverse — variation ids of ufv filters live only in the
3397 // expanded universe (see the twin scan above)
3398 if (inSetUniverse(postId) &&
3399 ((rangeListMaxVal === 0 && rangeListMinVal <= postVal) ||
3400 (rangeListMinVal <= postVal && rangeListMaxVal >= postVal))) {
3401 rangePostIds.push(postId);
3402 }
3403 }
3404 }
3405 }
3406
3407 rangePostIds = getVariation(rangePostIds, isUsedForVariations);
3408
3409 allEntities[indexEntity].items[indexItem].range_list_input[indexRange] = rangePostIds.length;
3410 }
3411 }
3412 }
3413 }
3414 }
3415
3416 setData.countFilteredPostsIds = [];
3417 let isFiltered = false;
3418 if (hasTempFilters) {
3419 for (const [entityName, postsIds] of Object.entries(tempFiltered)) {
3420
3421 // Product-level (parent-space) intersection: every filter is an
3422 // independent product-level clause in the server query, so a product
3423 // matches when ANY of its variations satisfies each filter — not
3424 // necessarily the same variation for all of them. On
3425 // variations-as-products shops the intersection (and the Apply
3426 // total derived from it) stays in variation space instead,
3427 // matching the server's ungated getBetweenFiltersIntersect.
3428 // Never count posts outside the set universe (see inSetUniverse above)
3429 const parentIds = new Set(wpcCountSpace(postsIds.filter(inSetUniverse)));
3430
3431 if(setData.countFilteredPostsIds.length === 0 && !isFiltered){
3432 setData.countFilteredPostsIds = [...parentIds];
3433 }
3434
3435 setData.countFilteredPostsIds = setData.countFilteredPostsIds.filter(x => parentIds.has(x));
3436 isFiltered = true;
3437 }
3438 }
3439
3440 setData.filteredPostsIds = [...new Set(setData.filteredPostsIds)];
3441
3442 for (const [entityName, postIds] of Object.entries(entityPostsCount)) {
3443 const uniquePostIds = [...new Set(postIds)];
3444 entityPostsCount[entityName] = uniquePostIds.some(id => allPostsUniverseSet.has(Number(id)));
3445 }
3446 setData.entityPostsCount = entityPostsCount;
3447
3448 updateCountersHtml(allEntities, setId);
3449 updateRangeInput(allEntities, setId);
3450 }
3451
3452 const getVariation = (tempPostIdsArray, isUsedForVariations) => {
3453 if(!wpcIsPro) return tempPostIdsArray;
3454 let postIdsWithoutVariation = [];
3455
3456 tempPostIdsArray.forEach(function(postId, indexPostId) {
3457 let isVariation = wpcFilterJsonData.product_variations_map[postId] !== undefined;
3458 if(!isUsedForVariations && isVariation){
3459 // Map the variation to its parent product (the Set dedups).
3460 // Dropping variations here made numeric-range counts lose every
3461 // variable product: their _price data is represented by
3462 // variation IDs, while the server counts parent products whose
3463 // variation price matches the range.
3464 postIdsWithoutVariation.push(Number(wpcFilterJsonData.product_variations_map[postId]));
3465 }else{
3466 postIdsWithoutVariation.push(postId);
3467 }
3468 });
3469
3470 return [...new Set(postIdsWithoutVariation)]
3471 }
3472
3473 // Final DISPLAYED-count space. On shops that list variations as standalone
3474 // catalog items (XStore's variable_products_detach — the server sets
3475 // wpcFilterJsonData.variationsAsProducts) the visible item unit IS a
3476 // variation, so term counters and the Apply total must stay in variation
3477 // space; everywhere else counts collapse to distinct parent products.
3478 // Mirrors the server-side wpc_from_variations_to_products counting gate.
3479 // Space normalization for intersections / universe membership keeps using
3480 // getVariation() directly and is deliberately NOT affected by the flag.
3481 const wpcCountSpace = (postIdsArray) =>
3482 wpcFilterJsonData.variationsAsProducts ? [...new Set(postIdsArray)] : getVariation(postIdsArray, false);
3483
3484
3485
3486 // The set universe in both spaces: parent products and the variation-expanded
3487 // one (variable parents replaced by their variations there). meta_values can
3488 // be keyed by EITHER space — PARENT ids for post dates and product-level
3489 // values, VARIATION ids for used-for-variations data — so every universe
3490 // membership check must accept both spaces.
3491 const wpcUniverse = (setId) => {
3492 const expandedKeys = Object.keys(wpcFilterJsonData[setId].allPostsIds);
3493 const allPostsExpandedSet = new Set(expandedKeys.map(Number));
3494 const allPostsUniverseSet = new Set(getVariation(expandedKeys, false).map(Number));
3495 const inSetUniverse = (id) => allPostsUniverseSet.has(+id) || allPostsExpandedSet.has(+id);
3496 return { allPostsExpandedSet, allPostsUniverseSet, inSetUniverse };
3497 };
3498
3499 // jQuery .data() type-casts attribute values ("0" -> number 0,
3500 // "false" -> boolean false), while slugs in the inline JSON are always
3501 // strings — normalize every slug read from .data() before comparing or
3502 // collecting, or terms named "0"/"false" silently stop matching.
3503 const wpcTermSlug = (slug) => (slug === undefined ? undefined : String(slug));
3504
3505 // The timepicker addon is enqueued only when a date+time filter exists on
3506 // the page; date-only pages have just the stock jQuery UI datepicker (with
3507 // the addon present, datetimepicker get/setDate works on both kinds).
3508 const wpcPickerFn = () => ($.fn.datetimepicker ? 'datetimepicker' : 'datepicker');
3509
3510 // Terminate a built URL's path exactly like PHP's user_trailingslashit()
3511 // does for the server-rendered links (wpcFilterJsonData.trailingSlash
3512 // carries the site convention) — a mismatched slash costs a 301 redirect
3513 // on every Apply click and breaks the newUrl/applyDataUrl comparisons
3514 const wpcNormalizeTrailingSlash = (url) => {
3515 if (typeof wpcFilterJsonData.trailingSlash === 'undefined' || url.pathname === '/') {
3516 return url;
3517 }
3518 if (wpcFilterJsonData.trailingSlash) {
3519 if (!url.pathname.endsWith('/')) {
3520 url.pathname += '/';
3521 }
3522 } else {
3523 url.pathname = url.pathname.replace(/\/+$/, '');
3524 }
3525 return url;
3526 };
3527
3528 // Maps a variation id to its parent product id (identity for non-variations).
3529 // Cross-entity logic must compare PRODUCTS: the server treats every filter as
3530 // an independent product-level clause, so intersecting raw variation ids would
3531 // require one variation to satisfy all filters at once and lose products.
3532 const wpcParentOf = (postId) => {
3533 if (!wpcIsPro || !wpcFilterJsonData.product_variations_map) return Number(postId);
3534 const parent = wpcFilterJsonData.product_variations_map[postId];
3535 return parent !== undefined ? Number(parent) : Number(postId);
3536 };
3537
3538 // Range bounds must reflect the INTERSECTION of the other selected filters
3539 // (filteredPostsIds is a UNION across entities). The intersection is taken in
3540 // parent space and then expanded back to the whole universe families, because
3541 // the bounds cover ALL price rows of the surviving products — like the server
3542 const intersectTempFiltered = (jsonData, excludeEname) => {
3543 let parentInter = null;
3544 for (const [eName, ids] of Object.entries(jsonData.tempFilteredAllPostsIds)) {
3545 if (eName === excludeEname) continue;
3546 const parents = new Set(getVariation(ids, false));
3547 if (parentInter === null) {
3548 parentInter = parents;
3549 } else {
3550 const prev = parentInter;
3551 parentInter = new Set();
3552 for (const p of parents) {
3553 if (prev.has(p)) {
3554 parentInter.add(p);
3555 }
3556 }
3557 }
3558 }
3559 if (parentInter === null) {
3560 return Object.values(jsonData.filteredPostsIds);
3561 }
3562 // Parent ids first: post dates and product-level meta rows are keyed by
3563 // them, and the expanded universe below only carries variation ids for
3564 // variable products. Duplicated simple ids are harmless for min/max.
3565 const familyIds = [...parentInter];
3566 for (const key of Object.keys(jsonData.allPostsIds)) {
3567 const id = Number(key);
3568 if (parentInter.has(wpcParentOf(id))) {
3569 familyIds.push(id);
3570 }
3571 }
3572 return familyIds;
3573 };
3574
3575 const updateRangeInput = (allEntities, setId) => {
3576 const $filterSetEl = $('.wpc-filter-set-' + setId);
3577 const $rangeInputs = $('.wpc-filters-range-inputs', $filterSetEl);
3578 const setData = wpcFilterJsonData[setId];
3579 const isSearchUrl = wpcIsSearchUrl()
3580
3581 if(isSearchUrl && !setData.allPostsIds.length){
3582 return;
3583 }
3584
3585 $rangeInputs.each(function() {
3586 const $container = $(this);
3587 const $forms = $('form', $container);
3588 const $rangeListInputs = $('.wpc-range-list-item:checked', $container);
3589 const rangeListData = $rangeListInputs.length ? $rangeListInputs.data() : null;
3590 const rangeListMin = rangeListData ? rangeListData.min : '';
3591 const rangeListMax = rangeListData ? rangeListData.max : '';
3592
3593 $forms.each(function() {
3594 const $form = $(this);
3595
3596 // Number range
3597 const $min = $('.wpc-filters-range-min', $form);
3598 const $max = $('.wpc-filters-range-max', $form);
3599
3600 if ($min.length && $max.length) {
3601 const minData = $min.data();
3602 const maxData = $max.data();
3603 const min = Number(minData.min);
3604 const max = Number(maxData.max);
3605 const minCurVal = Number($min.val());
3606 const maxCurVal = Number($max.val());
3607
3608 if (minCurVal == min && max == maxCurVal) {
3609 const $inputs = $('input', $form);
3610 const jsonData = wpcFilterJsonData[setId];
3611 const tempFilteredLength = Object.keys(jsonData.tempFilteredAllPostsIds).length;
3612
3613 $inputs.each(function() {
3614 const $input = $(this);
3615 const data = $input.data();
3616 const wpcEname = data.wpcEName;
3617
3618 if (typeof data.min !== "undefined") {
3619 const hasOnlyOneElement = tempFilteredLength === 1 &&
3620 typeof jsonData.tempFilteredAllPostsIds[wpcEname] !== 'undefined';
3621
3622 if (rangeListMin !== '') {
3623 $input.val(rangeListMin);
3624 } else {
3625 let dataMin = data.absMin;
3626 let minVal = data.absMin;
3627
3628 if (!hasOnlyOneElement && jsonData.filteredPostsIds.length) {
3629 const minValuePostIds = [];
3630 // items may arrive without the 'min' key (e.g. reindexed to a plain array)
3631 const minItem = jsonData.allEntities[wpcEname] ? jsonData.allEntities[wpcEname].items['min'] : undefined;
3632 const metaValues = minItem ? minItem.meta_values : {};
3633
3634 for (const postId of intersectTempFiltered(jsonData, wpcEname)) {
3635 if (setData.allPostsIds[postId] === undefined) continue;
3636 const values = metaValues[postId];
3637 if (typeof values !== 'undefined') {
3638 for (const val of values) {
3639 minValuePostIds.push(val);
3640 }
3641 }
3642 }
3643
3644
3645 if (minValuePostIds.length) {
3646 minVal = wpcArrayMin(minValuePostIds);
3647 dataMin = minVal;
3648 }
3649 } else if (hasOnlyOneElement) {
3650 minVal = $input.val();
3651 }
3652
3653 $input
3654 .data('min', dataMin)
3655 .attr('data-min', dataMin)
3656 .val(minVal);
3657 }
3658 }
3659
3660 if (typeof data.max !== "undefined") {
3661 const hasOnlyOneElement = tempFilteredLength === 1 &&
3662 typeof jsonData.tempFilteredAllPostsIds[wpcEname] !== 'undefined';
3663
3664 if (rangeListMax !== '') {
3665 $input.val(rangeListMax);
3666 } else {
3667 let dataMax = data.absMax;
3668 let maxVal = data.absMax;
3669
3670 if (!hasOnlyOneElement && jsonData.filteredPostsIds.length) {
3671 const maxValuePostIds = [];
3672 // items may arrive without the 'max' key (e.g. reindexed to a plain array)
3673 const maxItem = jsonData.allEntities[wpcEname] ? jsonData.allEntities[wpcEname].items['max'] : undefined;
3674 const metaValues = maxItem ? maxItem.meta_values : {};
3675
3676 for (const postId of intersectTempFiltered(jsonData, wpcEname)) {
3677 if (setData.allPostsIds[postId] === undefined) continue;
3678 const values = metaValues[postId];
3679 if (typeof values !== 'undefined') {
3680 for (const val of values) {
3681 maxValuePostIds.push(val);
3682 }
3683 }
3684 }
3685
3686 if (maxValuePostIds.length) {
3687 maxVal = wpcArrayMax(maxValuePostIds);
3688 dataMax = maxVal;
3689 }else{
3690 maxVal = dataMax = 0;
3691 }
3692 } else if (hasOnlyOneElement) {
3693 maxVal = $input.val();
3694 }
3695
3696 $input
3697 .data('max', dataMax)
3698 .attr('data-max', dataMax)
3699 .val(maxVal);
3700 }
3701 }
3702 });
3703
3704 $.fn.wpcInitSlider($form);
3705 }
3706 }
3707
3708 // Date range
3709 const $fromEl = $('.wpc-filters-range-from.hasDatepicker', $container);
3710 const $toEl = $('.wpc-filters-range-to.hasDatepicker', $container);
3711
3712 if ($fromEl.length && $toEl.length) {
3713 const fromData = $fromEl.data();
3714 const toData = $toEl.data();
3715
3716 if (fromData.wpcTempFrom == fromData.wpcAbsFrom && toData.wpcAbsTo == toData.wpcTempTo) {
3717 const jsonData = wpcFilterJsonData[setId];
3718 const filteredPostIds = Object.values(jsonData.filteredPostsIds);
3719 const hasFilteredPosts = filteredPostIds.length > 0;
3720 const tempFilteredLength = Object.keys(jsonData.tempFilteredAllPostsIds).length;
3721
3722 $('input', $form).each(function() {
3723 const data = $(this).data();
3724 const wpcEname = data.wpcEName;
3725 const hasOnlyOneElement = tempFilteredLength === 1 &&
3726 wpcEname in jsonData.tempFilteredAllPostsIds;
3727
3728 const shouldFilter = !hasOnlyOneElement && hasFilteredPosts;
3729 const boundPostIds = shouldFilter ? intersectTempFiltered(jsonData, wpcEname) : filteredPostIds;
3730
3731 const findDateBound = (metaValues, findMin) => {
3732 let bound = findMin ? Infinity : -Infinity;
3733 for (let i = 0; i < boundPostIds.length; i++) {
3734 const val = metaValues[boundPostIds[i]];
3735 if (val !== undefined) {
3736 const ts = Date.parse(val);
3737 if (findMin ? ts < bound : ts > bound) bound = ts;
3738 }
3739 }
3740 return isFinite(bound) ? new Date(bound) : null;
3741 };
3742
3743 const pickerFn = wpcPickerFn();
3744
3745 if (data.wpcTempFrom !== undefined) {
3746 let setDate = new Date(data.wpcAbsFromRaw);
3747 if (shouldFilter) {
3748 const fromItem = jsonData.allEntities[wpcEname] ? jsonData.allEntities[wpcEname].items['from'] : undefined;
3749 const metaValues = fromItem ? fromItem.meta_values : {};
3750 setDate = findDateBound(metaValues, true) ?? setDate;
3751 }
3752 $(this)[pickerFn]('setDate', setDate);
3753 }
3754
3755 if (data.wpcTempTo !== undefined) {
3756 let setDate = new Date(data.wpcAbsToRaw);
3757 if (shouldFilter) {
3758 const toItem = jsonData.allEntities[wpcEname] ? jsonData.allEntities[wpcEname].items['to'] : undefined;
3759 const metaValues = toItem ? toItem.meta_values : {};
3760 setDate = findDateBound(metaValues, false) ?? setDate;
3761 }
3762 $(this)[pickerFn]('setDate', setDate);
3763 }
3764 });
3765 }
3766 }
3767 });
3768 });
3769 }
3770
3771 const updateCountersHtml = (allEntities, setId) => {
3772 const $filterSetEl = $('.wpc-filter-set-' + setId);
3773 const hideEmpty = (wpcFilterJsonData[setId].settings.hide_empty !== undefined) ? wpcFilterJsonData[setId].settings.hide_empty : false;
3774 const isHideEmpty = hideEmpty === 'yes';
3775 const hideEmptyFilter = (wpcFilterJsonData[setId].settings.hide_empty_filter !== undefined) ? wpcFilterJsonData[setId].settings.hide_empty_filter : false;
3776 const isHideEmptyFilter = hideEmptyFilter === 'yes';
3777
3778 const selectorsCache = {};
3779
3780 Object.entries(allEntities).forEach(([entityName, entityData]) => {
3781 let rangeList = entityData.filter.range_list_input;
3782 const entity = entityData.filter.entity;
3783 const entitySelector = '[data-wpc-e-name="' + entityName + '"]';
3784 const moreLess = entityData.filter.more_less;
3785 const isMoreLess = moreLess === 'yes';
3786
3787 Object.entries(entityData.items).forEach(([index, item]) => {
3788 const itemSlug = item.slug;
3789 const selectorKey = entityName + '|' + itemSlug;
3790
3791 if (!selectorsCache[selectorKey]) {
3792 selectorsCache[selectorKey] = {
3793 $el: $(entitySelector + '[data-wpc-slug="' + itemSlug + '"]', $filterSetEl).not('.wpc-apply-button-chip'),
3794 $parent: null
3795 };
3796 }
3797
3798 const cacheItem = selectorsCache[selectorKey];
3799 const $el = cacheItem.$el;
3800
3801 if($el.length){
3802 if (!cacheItem.$parent) {
3803 cacheItem.$parent = $el.parent();
3804 }
3805 const $parent = cacheItem.$parent;
3806
3807 let $counter = $('.wpc-term-count-value', $parent);
3808 // closest(), NOT parents(): in a hierarchy parents() climbs to
3809 // ancestor term-items too, so a zero-count child poisoned its
3810 // parents with the count-0/hidden-0 classes.
3811 // With "Show counters" disabled the counter spans do not exist,
3812 // but the empty-term state classes must still be maintained —
3813 // derive the li from the input itself then. Options and stars
3814 // keep the counter-based (empty) path: their own branches below
3815 // manage their state.
3816 const isGenericInput = !$el.is('option') && !$el.hasClass('flrt-star-input');
3817 let $parentItem = isGenericInput ? $el.closest('.wpc-term-item') : $counter.closest('.wpc-term-item');
3818 if($counter.length){
3819 $counter.text(item.cross_count);
3820 }
3821 if($counter.length || isGenericInput){
3822 if(item.cross_count === 0){
3823 $parentItem.addClass('wpc-term-count-0')
3824 } else {
3825 $parentItem.removeClass('wpc-term-count-0')
3826 }
3827 }
3828
3829 if(isHideEmpty){
3830 if(item.cross_count <= 0){
3831 $parentItem.addClass('wpc-term-count-hidden-0').removeClass('wpc-has-terms');
3832 if($el.is(':checked')){
3833 $parentItem.addClass('wpc-term-count-hidden-checked-0')
3834 }else{
3835 $parentItem.removeClass('wpc-term-count-hidden-checked-0')
3836 }
3837 } else {
3838 $parentItem.removeClass('wpc-term-count-hidden-0 wpc-term-count-hidden-checked-0').addClass('wpc-has-terms');
3839 }
3840 }else{
3841 if(item.cross_count <= 0){
3842 if(!$el.is(':checked')){
3843 $parentItem.removeClass('wpc-has-terms');
3844 }
3845 } else {
3846 $parentItem.addClass('wpc-has-terms');
3847 }
3848 }
3849
3850
3851
3852 let $starLabel = $('.flrt-star-label', $parent);
3853 if($starLabel.length && typeof $starLabel.data().wpcTermCount !== 'undefined'){
3854 $starLabel.attr('data-wpc-term-count', item.cross_count);
3855 $starLabel.data('wpcTermCount', item.cross_count);
3856 // Stars have no counter spans, so the generic branch above
3857 // never toggles the zero-class that drives the disabled look
3858 const $starItem = $starLabel.closest('.wpc-term-item');
3859 if(item.cross_count === 0){
3860 $starItem.addClass('wpc-term-count-0');
3861 }else{
3862 $starItem.removeClass('wpc-term-count-0');
3863 }
3864 // The visible number next to the stars is written only on
3865 // hover/click and then frozen via .flrt-change-blocked, so a
3866 // recount would otherwise leave a stale value there
3867 if($el.is(':checked')){
3868 const showTermCount = $starLabel.closest('.flrt-stars-wpc-filter-content').data('showTermCount');
3869 if(showTermCount){
3870 $('#flrt-wpc-term-count', $filterSetEl).text(item.cross_count);
3871 }
3872 }
3873 }
3874
3875 if($el.is('option')){
3876 const isSelect2 = $el.closest('select').hasClass('select2-hidden-accessible');
3877 const $select = $($el.closest('select'), $filterSetEl);
3878 const $childOption = $select.find(`.wpc-term-id-${$el.val()}`);
3879 if (isHideEmpty) {
3880 if ($childOption.length) {
3881 if (item.cross_count <= 0) {
3882 $childOption.addClass('wpc-term-count-hidden-0').removeClass('wpc-has-terms');
3883 if($el.is(':selected')){
3884 $parentItem.addClass('wpc-term-count-hidden-checked-0')
3885 }else{
3886 $parentItem.removeClass('wpc-term-count-hidden-checked-0')
3887 }
3888 } else {
3889 $childOption.removeClass('wpc-term-count-hidden-0').addClass('wpc-has-terms');
3890 }
3891 }
3892 } else {
3893 if(item.cross_count <= 0){
3894 if(!$el.is(':selected')){
3895 $childOption.removeClass('wpc-has-terms');
3896 }
3897 } else {
3898 $childOption.addClass('wpc-has-terms');
3899 }
3900 }
3901 if(!isSelect2){
3902 $el[0].textContent = $el[0].textContent.replace(/\(\d+\)/, '(' + item.cross_count + ')');
3903 }
3904 if(isSelect2){
3905 // Non-ASCII term slugs are percent-encoded ("%" breaks Sizzle selectors)
3906 const $select2El = $('.' + $.escapeSelector(`select2-${entityName}-${itemSlug}`))
3907 if($select2El.length){
3908 $select2El.find('.wpc-term-count-value').text(item.cross_count);
3909 }
3910 //wpcInitSelect2( 'wpc-filter-set-'+setId );
3911 }
3912 }
3913
3914 if(typeof $el.data().count !== 'undefined'){
3915 $el.attr('data-count', item.cross_count);
3916 $el.data('count', item.cross_count);
3917 }
3918 }
3919
3920 if (typeof rangeList === 'object') {
3921 Object.entries(entityData.items[index].range_list_input).forEach(([indexRange, count]) => {
3922 const rangeId = 'wpc-radio-' + entity + '-' + entityName + '-' + indexRange;
3923 const $rangeEl = $('#' + rangeId + '.wpc-range-list-item', $filterSetEl);
3924 if ($rangeEl.length) {
3925 let $counter = $('.wpc-term-count-value', $rangeEl.parent());
3926 // The li must not depend on the counter span existing —
3927 // with "Show counters" off the state classes still apply
3928 let $parentItem = $rangeEl.closest('.wpc-term-item');
3929 if ($counter.length) {
3930 $counter.text(count);
3931 }
3932 if (count === 0) {
3933 $parentItem.addClass('wpc-term-count-0')
3934 } else {
3935 $parentItem.removeClass('wpc-term-count-0')
3936 }
3937 if(isHideEmpty){
3938 if(count === 0){
3939 $parentItem.addClass('wpc-term-count-hidden-0')
3940 if($rangeEl.is(':checked')){
3941 $parentItem.addClass('wpc-term-count-hidden-checked-0')
3942 }else{
3943 $parentItem.removeClass('wpc-term-count-hidden-checked-0')
3944 }
3945 } else {
3946 $parentItem.removeClass('wpc-term-count-hidden-0 wpc-term-count-hidden-checked-0')
3947 }
3948 }
3949 }
3950 });
3951 }
3952 });
3953
3954 const $section = $(`.wpc-filters-section.wpc-filter-${entityName}`, $filterSetEl)
3955 let $wpcTermsItems = $section.find(`.wpc-filters-ul-list li, .wpc-filters-widget-select option`);
3956 let $wpcHasTermsItems = $section.find(`.wpc-filters-ul-list li.wpc-has-terms, .wpc-filters-widget-select option.wpc-has-terms`);
3957 const $wpcHasCheckedTermsItems = $section.find(`.wpc-filters-ul-list input:checked, .wpc-filters-ul-list option:selected`)
3958 $wpcTermsItems.removeClass('wpc-not-hidden-term');
3959 // Views without per-term counter spans (e.g. the stars rating) have no
3960 // li.wpc-has-terms bookkeeping, so a section hidden by hide_empty_filter
3961 // could never reappear — decide from the fresh recount data as well.
3962 // Skip the empty-slug pseudo-term: meta entities carry a ''-item for
3963 // products WITHOUT the field — it never renders as a selectable term,
3964 // but its cross_count kept whole custom-field sections visible
3965 const hasAnyTermPosts = Object.values(entityData.items).some(item => (item.slug ?? '') !== '' && (item.cross_count || 0) > 0);
3966 let wpcHasTerms = $wpcHasTermsItems.length || $wpcHasCheckedTermsItems.length || (hasAnyTermPosts ? 1 : 0)
3967 const isHideFilterElement = (entity === 'post_meta_num' || entity === 'post_date' || entity === 'tax_numeric' || entity === 'post_meta_num');
3968
3969 if (isHideFilterElement && wpcFilterJsonData[setId].entityPostsCount[entityName] !== undefined) {
3970 wpcHasTerms = wpcFilterJsonData[setId].entityPostsCount[entityName] === true ? 1 : 0;
3971 }
3972
3973 if(wpcHasTerms <= 0 && isHideEmptyFilter){
3974 $section.addClass('wpc-filters-section-0');
3975 }else{
3976 $section.removeClass('wpc-filters-section-0');
3977 }
3978
3979 // The blob can outlive the filter configuration (its file name now
3980 // fingerprints the configs, but an already-written stale file or a
3981 // missed invalidation must not blank the whole filter): the
3982 // server-rendered section class is per-request truth for More/Less
3983 const isMoreLessSection = isMoreLess || $section.hasClass('wpc-filter-more-less');
3984 if (isMoreLessSection) {
3985 if(!isHideEmpty){
3986 wpcHasTerms = $wpcTermsItems.length;
3987 }
3988
3989 if (wpcHasTerms <= +wpcMoreLessCount) {
3990 $section.addClass('wpc-filter-few-terms');
3991 } else {
3992 $section.removeClass('wpc-filter-few-terms');
3993 }
3994
3995 if (isHideEmpty) {
3996 $wpcTermsItems.filter('li.wpc-has-terms').slice(0, +wpcMoreLessCount).each(function () {
3997 $(this).addClass('wpc-not-hidden-term');
3998 });
3999 } else {
4000 $wpcTermsItems.filter('li').slice(0, +wpcMoreLessCount).each(function () {
4001 $(this).addClass('wpc-not-hidden-term');
4002 });
4003 }
4004 }
4005 });
4006
4007 // The parents' disabled look is gated by :not(.wpc-has-not-empty-children)
4008 // (see the dynamic CSS in Plugin.php) — recompute the class from the fresh
4009 // counts, or a hierarchy parent whose children all dropped to 0 keeps the
4010 // stale server-rendered class and never greys out
4011 $('li.wpc-has-children', $filterSetEl).each(function () {
4012 const hasNotEmpty = $(this).children('ul.children')
4013 .find('li.wpc-term-item:not(.wpc-term-count-0)').length > 0;
4014 $(this).toggleClass('wpc-has-not-empty-children', hasNotEmpty);
4015 });
4016
4017 const $otherFilterSetsOnPage = $(wpcWidgetContainer);
4018 const hasMultipleWidgets = $otherFilterSetsOnPage.length > 1;
4019
4020 if (hasMultipleWidgets) {
4021 let tempPostIds = [];
4022 $otherFilterSetsOnPage.each((index, filterSetWidget) => {
4023 const widgetSetId = $(filterSetWidget).data('set');
4024 if (wpcFilterJsonData[widgetSetId] && wpcFilterJsonData[widgetSetId].countFilteredPostsIds) {
4025
4026 const currentPostIds = wpcFilterJsonData[widgetSetId].countFilteredPostsIds;
4027
4028 if (index === 0) {
4029 tempPostIds = new Set(currentPostIds);
4030
4031 } else {
4032 tempPostIds = new Set(currentPostIds.filter(x => tempPostIds.has(x)));
4033
4034 }
4035 } else {
4036 if (index === 0) {
4037 tempPostIds = new Set();
4038 } else {
4039 tempPostIds = new Set();
4040 }
4041 }
4042 wpcFilterJsonData[widgetSetId].countFilteredPostsIds = [...tempPostIds];
4043 updateApplyButtonData(widgetSetId);
4044 });
4045 }else{
4046 updateApplyButtonData(setId);
4047 }
4048
4049 function updateApplyButtonData(setId){
4050 let filteredPostsCount = wpcFilterJsonData[setId].countFilteredPostsIds.length;
4051 const $applyButton = $('.wpc-filter-set-' + setId + ' .wpc-pc-apply-button');
4052 const $mobileApplyButton = $('.wpc-filter-set-' + setId + ' .wpc-filters-apply-button');
4053 if (filteredPostsCount <= 0){
4054 $applyButton.text('');
4055 $mobileApplyButton.find('.wpc-filters-found-posts').text(filteredPostsCount);
4056 wpcEnableStickyButtons(false);
4057 }else{
4058 $applyButton.text('(' + filteredPostsCount + ')');
4059 $mobileApplyButton.find('.wpc-filters-found-posts').text(filteredPostsCount);
4060 }
4061
4062 if(filteredPostsCount){
4063 $applyButton.removeClass('wpc-hidden-term-count');
4064 }else{
4065 $applyButton.addClass('wpc-hidden-term-count');
4066 }
4067
4068 let foundKey = null, foundIndex = null;
4069 let chips = [];
4070
4071 if(wpcFilterJsonData[setId]['chips'] !== undefined){
4072 Object.assign(chips, wpcFilterJsonData[setId]['chips']);
4073
4074 Object.keys(chips).forEach(key => {
4075 if (Array.isArray(chips[key])) {
4076 const idx = chips[key].findIndex(item => {
4077 return typeof item === 'object' &&
4078 item !== null &&
4079 item.entityClass === 'wpc-chip-search';
4080 });
4081 if (idx !== -1) {
4082 foundKey = key;
4083 foundIndex = idx;
4084 }
4085 }
4086 });
4087 if (foundKey !== null) {
4088 delete chips[foundKey];
4089 }
4090 }
4091
4092 const isAnyFilter = chips !== undefined && Object.keys(chips).length > 0;
4093
4094 if(!isAnyFilter){
4095 filteredPostsCount = wpcFilterJsonData[setId].totalAllPostsIds;
4096 $mobileApplyButton.find('.wpc-filters-found-posts').text(filteredPostsCount);
4097 }
4098 }
4099 }
4100
4101 const changeSearchUrl = (url, $el, setId) => {
4102 url = new URL(url);
4103 const $searchInput = $('.wpc-filter-search-form input.wpc-search-field', $el);
4104 if ($searchInput === undefined) return;
4105
4106
4107 const searchSlug = $searchInput.attr('name');
4108 const searchVal = $searchInput.val();
4109
4110 if (searchVal !== undefined) {
4111 url.searchParams.delete(searchSlug);
4112 let newUrl = url.toString();
4113 $searchInput.closest('.wpc-filter-search-form').find('a.wpc-search-clear-icon').attr('href', newUrl);
4114 const chips = $('.wpc-filter-chips-' + setId + ' .wpc-chip-search a');
4115
4116 if(chips === undefined || chips.length === 0) return;
4117
4118 chips.each(function() {
4119 $(this).attr('href', newUrl);
4120 });
4121 }
4122 }
4123
4124 function buildUrlForApplyButton($el, setId) {
4125 const $inputs = $('input:checked', '.wpc-filters-main-wrap').not('.wpc-range-list-item');
4126 const $options = $('option:selected', '.wpc-filters-main-wrap').not('.wpc-range-list-item');
4127 const $rangeForms = $('form.wpc-filter-range-form input', '.wpc-filters-main-wrap').not('.wpc-range-list-item');
4128 const $dateForms = $('form.wpc-filter-date-range-form-visible input', '.wpc-filters-main-wrap');
4129 let baseUrl = wpcFilterJsonData.domain;
4130 const $submitButton = $('.wpc-filters-submit-button');
4131 const applyUrl = $submitButton.data('wpcApplyUrl');
4132 // The location permalink WITHOUT current filter segments. data-wpc-apply-url
4133 // carries the selections-applied URL (no-JS fallback + sticky-state compare),
4134 // so appending fresh segments to IT duplicated the segments already in the
4135 // page URL. Fall back to it only when the attribute is absent — a theme
4136 // may still ship an overridden copy of the apply-button template.
4137 const applyBaseUrl = $submitButton.data('wpcApplyBaseUrl');
4138 const applyButtonPage = $submitButton.data('applyButtonPage');
4139 const applyBase = (applyBaseUrl !== undefined && applyBaseUrl !== '') ? applyBaseUrl : applyUrl;
4140 if(applyBase !== undefined && applyButtonPage){
4141 // Alternative Location: filter segments are appended to this base
4142 // via RELATIVE URL resolution below, which drops the last path
4143 // segment of a base without a trailing slash ("/shop" -> "/") —
4144 // the PHP-rendered location permalink is not slash-terminated
4145 const applyUrlObj = new URL(applyBase, window.location.href);
4146 if (!applyUrlObj.pathname.endsWith('/')) {
4147 applyUrlObj.pathname += '/';
4148 }
4149 baseUrl = applyUrlObj.href;
4150 }
4151
4152 let filterSetData = wpcFilterJsonData[setId];
4153
4154 let urlParams = {};
4155 let urlParamsWithoutSlug = {};
4156 let urlParamsWithoutPermalinks = {};
4157
4158
4159 const processElements = ($elements, setId) => {
4160 $elements.each(function(index, el) {
4161 const $currentEl = $(el);
4162 const data = $currentEl.data();
4163
4164 if (typeof data.wpcEName === "undefined") {
4165 return;
4166 }
4167
4168 let entity = filterSetData.allEntities[data.wpcEName]['filter']['entity'];
4169 if(entity!== undefined && entity === 'taxonomy'){
4170 if ($currentEl.is(':disabled:checked')) {
4171 return;
4172 }
4173 }
4174
4175 // Permalink maps are keyed by "entity#e_name": e_name alone collides
4176 // when two filters share it (Sale Price num vs On Sale exists)
4177 const entityKey = entity + '#' + data.wpcEName;
4178
4179 if(typeof wpcFilterJsonData.wpcFilterEntitiesWithoutSlug[entityKey] !== "undefined"){
4180
4181 let val = $currentEl.val();
4182
4183 if(typeof data.wpcTempFrom !== 'undefined' || typeof data.wpcTempTo !== 'undefined'){
4184 let wpcTempFrom = (typeof data.wpcTempFrom !== 'undefined');
4185 let wpcTempTo= (typeof data.wpcTempTo !== 'undefined');
4186 let wpcAbsFrom= (typeof data.wpcAbsFrom !== 'undefined');
4187 let wpcAbsTo= (typeof data.wpcAbsTo !== 'undefined');
4188 let dateTimeStr = false;
4189
4190 if(wpcAbsFrom && data.wpcAbsFrom === val){
4191 return;
4192 }
4193
4194 if(wpcTempFrom && wpcAbsFrom && data.wpcTempFrom === data.wpcAbsFrom){
4195 return;
4196 }
4197
4198 if(wpcAbsTo && data.wpcAbsTo === val){
4199 return;
4200 }
4201
4202
4203 if(wpcTempTo && wpcAbsTo && data.wpcTempTo === data.wpcAbsTo){
4204 return;
4205 }
4206
4207 if(wpcTempFrom){
4208 dateTimeStr = $("#wpc-filters-alt-date-from-"+ data.fid)[wpcPickerFn()]('getDate');
4209 }
4210
4211 if(wpcTempTo){
4212 dateTimeStr = $("#wpc-filters-alt-date-to-"+ data.fid)[wpcPickerFn()]('getDate');
4213 }
4214
4215 if(dateTimeStr !== false){
4216 val = formatDate(dateTimeStr, data.wpcDateType);
4217 }
4218 }
4219
4220 if(typeof data.min !== 'undefined' || typeof data.max !== 'undefined'){
4221 val = Number($currentEl.val());
4222 }
4223
4224 if(typeof data.absMin !== 'undefined' && data.absMin === val){
4225 return;
4226 }
4227 if(typeof data.min !== 'undefined' && data.min === val){
4228 return;
4229 }
4230 if(typeof data.absMax !== 'undefined' && data.absMax === val){
4231 return;
4232 }
4233 if(typeof data.max !== 'undefined' && data.max === val){
4234 return;
4235 }
4236 const sortKey = Object.keys(wpcFilterJsonData.wpcFilterPermalinksNum)
4237 .find(k => wpcFilterJsonData.wpcFilterPermalinksNum[k] === entityKey);
4238
4239 if (!sortKey) return;
4240
4241 urlParamsWithoutSlug[sortKey] ??= [];
4242 const wpcEName = wpcFilterJsonData.wpcFilterPermalinks[entityKey];
4243 urlParamsWithoutSlug[sortKey][wpcEName] ??= [];
4244 urlParamsWithoutSlug[sortKey][wpcEName][data.wpcSlug] = val;
4245 return;
4246 }
4247
4248 const sortKey = Object.keys(wpcFilterJsonData.wpcFilterPermalinksNum)
4249 .find(k => wpcFilterJsonData.wpcFilterPermalinksNum[k] === entityKey);
4250
4251 if (!sortKey) return;
4252
4253 urlParams[sortKey] ??= [];
4254 const wpcEName = wpcFilterJsonData.wpcFilterPermalinks[entityKey];
4255 urlParams[sortKey][wpcEName] ??= [];
4256 if (!urlParams[sortKey][wpcEName].includes(wpcTermSlug(data.wpcSlug))) {
4257 urlParams[sortKey][wpcEName].push(wpcTermSlug(data.wpcSlug));
4258 }
4259 });
4260 };
4261
4262 processElements($inputs, setId);
4263 processElements($options, setId);
4264 processElements($rangeForms, setId);
4265 processElements($dateForms, setId);
4266
4267 const $searchInput = $('.wpc-filter-search-form input.wpc-search-field', $el);
4268 if($searchInput !== undefined && $searchInput.length){
4269
4270 let sortKey = Object.values(wpcFilterJsonData.wpcFilterPermalinksNum).length + 1;
4271 const searchSlug = $searchInput.attr('name');
4272 const searchVal = $searchInput.val();
4273
4274 if (searchVal !== undefined && searchVal !== '') {
4275 if(wpcFilterJsonData.wpcFilterEntitiesWithoutSlug === undefined){
4276 wpcFilterJsonData.wpcFilterEntitiesWithoutSlug = {}
4277 }
4278 wpcFilterJsonData.wpcFilterEntitiesWithoutSlug.search = 'search';
4279
4280 if (urlParamsWithoutSlug[sortKey]=== undefined) {
4281 urlParamsWithoutSlug[sortKey] = {};
4282 }
4283 if (urlParamsWithoutSlug[sortKey]['search'] === undefined) {
4284 urlParamsWithoutSlug[sortKey]['search'] = {};
4285 }
4286 urlParamsWithoutSlug[sortKey]['search'][searchSlug] = searchVal;
4287 }
4288 }
4289
4290 let urlArray = [];
4291
4292 let wpcFilterPermalinksKeys = {};
4293 for (const [key, value] of Object.entries(wpcFilterJsonData.wpcFilterPermalinks)) {
4294 wpcFilterPermalinksKeys[value] = key;
4295 }
4296
4297 if(wpcIsPro) {
4298 let url = new URL(baseUrl);
4299 if(permalinksEnabled){
4300 if (Object.keys(urlParams).length > 0) {
4301 Object.entries(urlParams).forEach(([key, value]) => {
4302 Object.entries(value).forEach(([filterName, arr]) => {
4303 let urlKey = String(wpcFilterPermalinksKeys[filterName] || '').split('#').pop()
4304 const sorted = arr.sort((a, b) =>
4305 Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(a) - Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(b));
4306 let logic = filterSetData['allEntities'][urlKey]['filter']['logic'];
4307 let tempUrlString = filterName + "-" + sorted.join('-' + logic + '-');
4308 urlArray.push(tempUrlString)
4309 });
4310 });
4311 }
4312 }
4313 if(!permalinksEnabled){
4314 if (Object.keys(urlParams).length > 0) {
4315 if (Object.keys(urlParams).length > 0) {
4316 Object.entries(urlParams).forEach(([key, value]) => {
4317 Object.entries(value).forEach(([filterName, arr]) => {
4318 let urlKey = String(wpcFilterPermalinksKeys[filterName] || '').split('#').pop()
4319 const sorted = arr.sort((a, b) =>
4320 Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(a) - Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(b));
4321 url.searchParams.set(filterName, sorted.join(';'));
4322 });
4323 });
4324 }
4325 }
4326 }
4327
4328 if(permalinksEnabled){
4329 if (urlArray.length) {
4330 // Relative resolution against baseUrl (the clean page URL
4331 // without filter segments, always slash-terminated) appends
4332 // the filter path: /shop/ + color-blue -> /shop/color-blue
4333 url = new URL(urlArray.join('/'), baseUrl);
4334 } else {
4335 // No path filters selected (e.g. only a numeric range, which
4336 // travels as a GET param): keep the page base URL as is.
4337 // new URL("/", baseUrl) wiped the path and produced links
4338 // like http://site.test/?max_price=84 instead of
4339 // http://site.test/shop/?max_price=84
4340 url = new URL(baseUrl);
4341 }
4342 }
4343
4344
4345 if (Object.keys(urlParamsWithoutSlug).length > 0) {
4346 Object.entries(urlParamsWithoutSlug).forEach(([key, value]) => {
4347 Object.entries(value).forEach(([filterName, arr]) => {
4348 Object.entries(arr).forEach(([item_key, item]) => {
4349 url.searchParams.set(item_key, item);
4350 });
4351 });
4352 });
4353 }
4354 if(url.pathname === '/' && url.search === '' && applyButtonPage) {
4355 return baseUrl;
4356 }
4357 wpcNormalizeTrailingSlash(url);
4358 changeSearchUrl(url, $el, setId);
4359 return url.toString();
4360 }
4361
4362
4363 if(!wpcIsPro){
4364 if (Object.keys(urlParams).length > 0) {
4365 Object.entries(urlParams).forEach(([key, value]) => {
4366 Object.entries(value).forEach(([filterName, arr]) => {
4367 let urlKey = String(wpcFilterPermalinksKeys[filterName] || '').split('#').pop()
4368 const sorted = arr.sort((a, b) =>
4369 Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(a) - Object.values(filterSetData['allEntities'][urlKey].items_sort).indexOf(b));
4370 let filterPositon = Number(filterSetData['allEntities'][urlKey]['filter']['menu_order']);
4371 urlParamsWithoutPermalinks[filterPositon] = {[filterName]: sorted.join(';')}
4372
4373 });
4374 });
4375 }
4376
4377 if (Object.keys(urlParamsWithoutSlug).length > 0) {
4378 Object.entries(urlParamsWithoutSlug).forEach(([key, value]) => {
4379 Object.entries(value).forEach(([filterName, arr]) => {
4380 let urlKey = String(wpcFilterPermalinksKeys[filterName] || '').split('#').pop()
4381 let filterPositon = Number(filterSetData['allEntities'][urlKey]['filter']['menu_order']);
4382 urlParamsWithoutPermalinks[filterPositon] = arr;
4383 });
4384 });
4385 }
4386 let url = new URL(baseUrl);
4387 if (Object.keys(urlParamsWithoutPermalinks).length > 0) {
4388 Object.values(urlParamsWithoutPermalinks).forEach((item) => {
4389 Object.entries(item).forEach(([item_key, item]) => {
4390 url.searchParams.set(item_key, item);
4391 });
4392 });
4393 }
4394 if(url.pathname === '/' && url.search === '' && applyButtonPage) {
4395 return baseUrl;
4396 }
4397 wpcNormalizeTrailingSlash(url);
4398 changeSearchUrl(url, $el, setId);
4399 return url.toString();
4400 }
4401 }
4402
4403 function formatDate(value, type) {
4404 const d = new Date(value);
4405
4406 const pad = n => String(n).padStart(2, '0');
4407
4408 const date = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
4409 const time = `${pad(d.getHours())}.${pad(d.getMinutes())}.${pad(d.getSeconds())}`;
4410 const timeColon = `${pad(d.getHours())}.${pad(d.getMinutes())}.${pad(d.getSeconds())}`;
4411
4412 switch (type) {
4413 case 'date': return date;
4414 case 'datetime': return `${date}t${time}`;
4415 case 'time': return timeColon;
4416 default: return date;
4417 }
4418 }
4419
4420 function compareInputWithRangeList(form, applyButtonMode){
4421 if(!applyButtonMode) return;
4422 let inputDataMin = $('input.wpc-filters-range-min', form);
4423
4424 if(!inputDataMin.length) return;
4425
4426 let inputDataMax = $('input.wpc-filters-range-max', form);
4427 let inputMinVal = Number(inputDataMin.val());
4428 let inputMaxVal = Number(inputDataMax.val());
4429 let $rangeList = form.parents().find('.wpc-filter-' + inputDataMin.data('wpcEName') + ' .wpc-filters-range-inputs .wpc-filters-radio input');
4430
4431 if (!$rangeList.length) return;
4432 $rangeList.each(function (i, radioEl){
4433 let data = $(radioEl).data();
4434 let elMin = Number(data.min)
4435 let elMax = Number(data.max)
4436
4437
4438 if((elMin !== inputMinVal || elMax !== inputMaxVal)){
4439 $(radioEl).prop('checked', false).data('wpc-was-checked', false);
4440 }
4441
4442 if((elMin === inputMinVal && elMax === inputMaxVal)){
4443 $(radioEl).prop('checked', true).data('wpc-was-checked', true);
4444 }
4445 });
4446 }
4447
4448 function compareWithOtherFilterSets($el, setId){
4449 let $otherFilterSetsOnPage;
4450 $otherFilterSetsOnPage = $(wpcWidgetContainer).not(`.wpc-filter-set-${setId}`);
4451 let changedFilterSets = new Set();
4452 if ($otherFilterSetsOnPage) {
4453 $otherFilterSetsOnPage.each((index, filterSetWidget) => {
4454 const $filterSetWidget = $(filterSetWidget);
4455 const changedSetId = $filterSetWidget.data('set');
4456 const $allInputData = $('input, option', $filterSetWidget);
4457 const $allInputDataThisWidget = $('input, option', `.wpc-filter-set-${setId}`);
4458
4459 $allInputData.each((index, inputEl) => {
4460 const $inputEl = $(inputEl);
4461 const data = $inputEl.data();
4462 const wpcEName = data.wpcEName;
4463 const wpcSlug = data.wpcSlug;
4464
4465 if(wpcEName !== undefined && wpcSlug !== undefined){
4466 const $input = $allInputDataThisWidget.filter(function () {
4467 return $(this).data('wpc-e-name') === data.wpcEName &&
4468 $(this).data('wpc-slug') === data.wpcSlug
4469 });
4470
4471 if($input.length){
4472 const isChecked = $input.is(':checked') || $input.is(':selected')
4473 const $inputForChange = $filterSetWidget.find($inputEl);
4474 const isRatingStar = $inputForChange.hasClass('flrt-star-input');
4475 if ($inputEl.prop('tagName') === 'OPTION') {
4476 if($input.prop('selected') !== $inputEl.prop('selected')){
4477 $inputForChange.prop('selected', isChecked);
4478 }
4479 }
4480
4481 if ($inputEl.prop('tagName') === 'INPUT') {
4482 if ($inputEl.attr('type') === 'checkbox' || $inputEl.attr('type') === 'radio') {
4483 if($input.prop('checked') !== $inputEl.prop('checked')){
4484 $inputForChange.prop('checked', isChecked);
4485 }
4486 }
4487 if ($inputEl.attr('type') === 'number' || $inputEl.attr('type') === 'text') {
4488 const isRange = $input.hasClass('wpc-filters-range-min') && $inputEl.hasClass('wpc-filters-range-min') || $input.hasClass('wpc-filters-range-max') && $inputEl.hasClass('wpc-filters-range-max')
4489 if(isRange){
4490 if ($input.val() !== $inputEl.val()) {
4491 $inputForChange.val($input.val());
4492 if(data.min !== undefined){
4493 $inputForChange.data('min', $input.data('min')).attr('data-min', $input.data('min'))
4494 }
4495 if(data.max !== undefined){
4496 $inputForChange.data('max', $input.data('max')).attr('data-max', $input.data('max'));
4497 }
4498 }
4499 }
4500 const isDate = ($input.hasClass('wpc-filters-range-from') && $inputEl.hasClass('wpc-filters-range-from') || $input.hasClass('wpc-filters-range-to') && $inputEl.hasClass('wpc-filters-range-to'))
4501 && $input.data('wpc-date-type') === data.wpcDateType;
4502 if(isDate){
4503 if ($input.val() !== $inputEl.val()) {
4504 const newDate = $input[wpcPickerFn()]('getDate');
4505 $inputForChange[wpcPickerFn()]('setDate', newDate);
4506 }
4507 }
4508 }
4509 }
4510 changedFilterSets.add(changedSetId)
4511 }
4512 }
4513
4514 if(wpcEName !== undefined && data.wpcSlugMin !== undefined && $inputEl.hasClass('wpc-range-list-item')){
4515 const $input = $allInputDataThisWidget.filter(function () {
4516 return $(this).data('wpc-slug-min') !== undefined &&
4517 $(this).prop('id') === $inputEl.prop('id') &&
4518 $(this).data('wpc-e-name') === data.wpcEName &&
4519 $(this).data('termId') === data.termId
4520 });
4521
4522 if($input.length) {
4523 const isChecked = $input.is(':checked') || $input.is(':selected')
4524 const $inputForChange = $filterSetWidget.find($inputEl);
4525 if ($inputEl.prop('tagName') === 'INPUT') {
4526 if ($inputEl.attr('type') === 'checkbox' || $inputEl.attr('type') === 'radio') {
4527 if (isChecked) {
4528 $inputForChange.prop('checked', isChecked).data('wpc-was-checked', true).addClass('wpc-range-list-item-checked');
4529 }else{
4530 $inputForChange.prop('checked', isChecked).data('wpc-was-checked', false).removeClass('wpc-range-list-item-checked');
4531 }
4532 }
4533 }
4534 changedFilterSets.add(changedSetId)
4535 }
4536 }
4537 });
4538 });
4539 }
4540 return changedFilterSets;
4541 }
4542
4543 function applyJsMode($el, setId){
4544 updateCounters($el, setId)
4545 const hasMultipleWidgets = $(wpcWidgetContainer).length > 1;
4546
4547 if (hasMultipleWidgets) {
4548 const changedFilterSets = compareWithOtherFilterSets($el, setId);
4549 if (changedFilterSets.size > 0) {
4550 for (const elementSetId of changedFilterSets) {
4551 updateCounters($('.wpc-filter-set-' + elementSetId), elementSetId)
4552 }
4553 }
4554 }
4555
4556 const newUrl = buildUrlForApplyButton($el, setId);
4557 if( wpcUseSelect2 === 'yes' && typeof $.fn.select2 !== 'undefined' ){
4558 $('.wpc-filters-widget-select, .wpc-orderby-select').each(function() {
4559 if ($(this).hasClass('select2-hidden-accessible')) {
4560 $(this).select2('close');
4561 }
4562 });
4563 }
4564 wpcInitiateAll();
4565 const resetButton = $('.wpc-sticky-buttons .wpc-filters-reset-button', $el).attr('href');
4566 const applyDataUrl = $('.wpc-sticky-buttons .wpc-filters-submit-button', $el).data('wpcApplyUrl');
4567 const applyButtonUrl = $('.wpc-sticky-buttons .wpc-filters-submit-button', $el).attr('href');
4568 let isSticky = false;
4569 const isSameUrl = (newUrl + '/' === resetButton || newUrl === resetButton + '/');
4570 const urlNow = window.location.href;
4571 if(applyButtonUrl !== newUrl){
4572 isSticky = true;
4573 if(hasMultipleWidgets){
4574 let $otherFilterSetsOnPage;
4575 $otherFilterSetsOnPage = $(wpcWidgetContainer).not(`.wpc-filter-set-${setId}`);
4576 if($otherFilterSetsOnPage){
4577 $otherFilterSetsOnPage.each((index, filterSetWidget) => {
4578 const $filterSetWidget = $(filterSetWidget);
4579 $('.wpc-sticky-buttons .wpc-filters-submit-button', $filterSetWidget).attr('href', newUrl)
4580 $('.wpc-filters-widget-controls-item .wpc-filters-apply-button', $filterSetWidget).attr('href', newUrl)
4581 });
4582 }
4583 }
4584 $('.wpc-sticky-buttons .wpc-filters-submit-button', $el).attr('href', newUrl)
4585 $('.wpc-filters-widget-controls-item .wpc-filters-apply-button', $el).attr('href', newUrl)
4586 }
4587 if (applyDataUrl === newUrl){
4588 isSticky = false;
4589 }
4590 wpcEnableStickyButtons(isSticky);
4591 wpcUpdateStickyButtons();
4592 }
4593
4594 // Mirrors flrt_chips_labels() in PHP: "Labels for Chips" filter option,
4595 // {value} replaced with the input value, or the value appended.
4596 function flrtChipLabelFromTemplate(template, value) {
4597 template = String(template);
4598 if (template.indexOf('{value}') !== -1) {
4599 return template.split('{value}').join(value);
4600 }
4601 return template + ' ' + value;
4602 }
4603
4604 function collectChips($currentEl, setId) {
4605 const data = $currentEl.data();
4606 const chips = wpcFilterJsonData[setId]['chips'];
4607
4608
4609 const sortData = {};
4610 $('.wpc-filters-section').each((index, el) => {
4611 const fid = $(el).data('fid');
4612 if (fid !== undefined) sortData[fid] = index;
4613 });
4614
4615
4616 const pushChip = (slotIndex, chip, isSearch = false) => {
4617 chips[slotIndex] = chips[slotIndex] || [];
4618
4619 if (!chips[slotIndex].some(c => c.entityClass === chip.entityClass)) {
4620 chips[slotIndex].push(chip);
4621 }
4622
4623 if (isSearch) {
4624 const idx = chips[slotIndex].findIndex(c => c.entityClass === chip.entityClass);
4625 if (idx !== -1 && idx !== chips[slotIndex].length - 1) {
4626 chips[slotIndex].push(chips[slotIndex].splice(idx, 1)[0]);
4627 }
4628 }
4629 };
4630
4631
4632 const makeChip = (entityClass, chipLabel, isSearch = false) => ({
4633 entityClass,
4634 chipLabel,
4635 wpcEName: (isSearch) ? '' : data.wpcEName,
4636 wpcSlug: (isSearch) ? '' : data.wpcSlug,
4637 });
4638
4639 const slotIndex = sortData[data.fid];
4640
4641 if (data.min !== undefined || data.max !== undefined) {
4642
4643 if (slotIndex === undefined) return;
4644
4645 const rangeName = $currentEl
4646 .closest('.wpc-filters-section-' + data.fid)
4647 .find('.wpc-filter-title')
4648 .text();
4649 const chipLabel = data.wpcChipLabel
4650 ? flrtChipLabelFromTemplate(data.wpcChipLabel, $currentEl.val())
4651 : `${data.wpcChipsText} ${rangeName} ${$currentEl.val()}`;
4652 const entityClass = `wpc-chip-${data.wpcEName}-${data.wpcSlug}`;
4653 const chip = makeChip(entityClass, chipLabel);
4654
4655 // A chip only for a bound the user actually set: a value equal to
4656 // the catalog-wide bound (data.absMin/absMax) OR to the current
4657 // intersection placeholder (data.min/max, refreshed by
4658 // updateRangeInput) is untouched — the same criterion
4659 // buildUrlForApplyButton uses, so chips always match the built URL
4660 const numVal = Number($currentEl.val());
4661 if (data.absMin !== undefined && numVal !== data.absMin && numVal !== data.min) pushChip(slotIndex, chip);
4662 if (data.absMax !== undefined && numVal !== data.absMax && numVal !== data.max) pushChip(slotIndex, chip);
4663
4664 } else if (data.wpcTempFrom !== undefined || data.wpcTempTo !== undefined) {
4665
4666 if (slotIndex === undefined) return;
4667
4668 const chipLabel = data.wpcChipLabel
4669 ? flrtChipLabelFromTemplate(data.wpcChipLabel, $currentEl.val())
4670 : `${data.wpcChipsText} ${$currentEl.val()}`;
4671 const entityClass = `wpc-chip-${data.wpcEName}-${data.wpcSlug}`;
4672 const chip = makeChip(entityClass, chipLabel);
4673
4674 // Untouched date fields keep data-wpc-temp-* equal to the abs bound
4675 // (updateInputDateData rewrites it only on a real picker pick) —
4676 // mirror buildUrlForApplyButton so no chip appears for a field the
4677 // user never opened, even after updateRangeInput narrowed its value
4678 if (data.wpcAbsFrom !== undefined && $currentEl.val() != data.wpcAbsFrom && data.wpcTempFrom !== data.wpcAbsFrom) pushChip(slotIndex, chip);
4679 if (data.wpcAbsTo !== undefined && $currentEl.val() != data.wpcAbsTo && data.wpcTempTo !== data.wpcAbsTo) pushChip(slotIndex, chip);
4680
4681 } else {
4682
4683 const $wpcSection = $currentEl.closest(`.wpc-filters-section.wpc-filter-${data.wpcEName}`);
4684 const fid = $wpcSection.data('fid');
4685 if (sortData[fid] === undefined) return;
4686
4687 const sectionSlot = sortData[fid];
4688 const entityClass = `wpc-chip-${data.wpcEName}-${data.termId}`;
4689
4690 let chipLabel = $(`label .wpc-filter-link`, $currentEl.closest(`.wpc-term-id-${data.termId}`)).text();
4691
4692 const $select = $('select', $wpcSection);
4693 if ($select.length) {
4694 const isSelect2 = $select.hasClass('select2-hidden-accessible') && wpcUseSelect2 === 'yes';
4695 const selector = isSelect2 ? 'select.select2-hidden-accessible option:selected' : 'select option:selected';
4696 chipLabel = $(selector, $wpcSection).data('wpcChip');
4697 }
4698
4699 chips[sectionSlot] = chips[sectionSlot] || [];
4700 if (!chips[sectionSlot].some(c => c.entityClass === entityClass)) {
4701 chips[sectionSlot][data.termId] = makeChip(entityClass, chipLabel);
4702 }
4703 }
4704
4705 const searchInput = $('.wpc-filter-set-' + setId + ' form.wpc-filter-search-form input');
4706 if(searchInput !== undefined){
4707 let searchVal = searchInput.val();
4708 if(searchVal !== undefined && searchVal !== ''){
4709 const chipLabel = wpcSearchChipsText.replace("%s", searchVal);
4710 const entityClass = `wpc-chip-search`;
4711 const chip = makeChip(entityClass, chipLabel, true);
4712 pushChip(Object.values(sortData).length + 1, chip, true);
4713 }
4714
4715 }
4716 }
4717 function updateChipsList(setId) {
4718 const chips = wpcFilterJsonData[setId]['chips'];
4719 flrtClearChips(setId);
4720
4721 const entries = Object.entries(chips);
4722 if (entries.length === 0) return;
4723
4724
4725 const $container = $('.wpc-filter-chips-' + setId);
4726 $container.find('li').remove();
4727 $container.removeClass('wpc-filter-chips-empty');
4728
4729 const fragment = [chipsResetButtonTemplate()];
4730
4731 entries.forEach(([, chipGroup]) => {
4732 Object.values(chipGroup).forEach(chip => {
4733 fragment.push(
4734 chip.wpcEName === 'product_visibility'
4735 ? chipsStarsTemplate(chip, setId)
4736 : chipsTemplate(chip)
4737 );
4738 });
4739 });
4740
4741 $container.append(fragment.join(''));
4742 }
4743
4744 function chipsResetButtonTemplate(){
4745 // Client-rebuilt chips describe the PENDING selection and never navigate —
4746 // render them as spans so no crawlable link appears from client state
4747 return `<li class="wpc-filter-chip wpc-chip-reset-all"><span class="wpc-apply-button-chip wpc-apply-button-chips-reset" title=""><span class="wpc-chip-content"><span class="wpc-filter-chip-name">${chipsReset}</span><span class="wpc-chip-remove-icon">×</span></span></span></li>`;
4748 }
4749
4750 function resetButtonInApplyButtonMode($this, e){
4751
4752 const $el = $this.closest(wpcWidgetContainer);
4753 const setId = $el.data('set');
4754
4755
4756 if (!setId || !wpcApplyButtonSets.length || !wpcApplyButtonSets.includes(setId)) return;
4757
4758 e.preventDefault();
4759 $('html, body').css("cursor", "wait");
4760 wpcShowSpinner();
4761 setTimeout(function() {
4762 $el.find('.wpc-filters-widget-select').each(function () {
4763 const sel = this;
4764 const opts = sel.options;
4765 if (opts.length && sel.selectedIndex !== -1) {
4766 sel.value = opts[0].value;
4767 }
4768 });
4769
4770 $('input:checked', $el).each(function () {
4771 this.checked = false;
4772 $(this).data('wpc-was-checked', false);
4773 });
4774
4775 $('input[type=text], input[type=number]', $el).each(function () {
4776 resetInputElement($(this));
4777 });
4778
4779 applyJsMode($el, setId);
4780
4781 delete wpcFilterJsonData[setId]['chips'];
4782 delete wpcFilterJsonData[setId]['tempFilteredAllPostsIds'];
4783 flrtClearChips(setId);
4784 wpcHideSpinner();
4785 $('html, body').css("cursor", "auto");
4786 }, 0);
4787 }
4788
4789 function flrt_rating_star()
4790 {
4791 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25">
4792 <polygon class="cls-1" points="19.89 24.5 12.48 19.8 5.06 24.48 7.03 15.62 0.5 9.64 9.12 8.87 12.51 0.5 15.88 8.88 24.5 9.68 17.96 15.63 19.89 24.5"/>
4793 </svg>`;
4794 }
4795
4796 function chipsTemplate(chip){
4797 let title = wpcSprintf(
4798 chipsTitle, chip.chipLabel
4799 )
4800 return `<li class="wpc-filter-chip ${chip.entityClass}"><span data-wpc-e-name="${chip.wpcEName}" data-wpc-slug="${chip.wpcSlug}" class="wpc-apply-button-chip" title="${title}"><span class="wpc-chip-content"><span class="wpc-filter-chip-name">${chip.chipLabel} </span><span class="wpc-chip-remove-icon">×</span></span></span></li>`;
4801 }
4802
4803 function chipsStarsTemplate(chip, setId) {
4804 let title = wpcSprintf(
4805 chipsTitle, chip.chipLabel
4806 )
4807 let $starsNumber = $(`input[data-wpc-e-name="${chip.wpcEName}"][data-wpc-slug="${chip.wpcSlug}"]:checked`, $(`.wpc-filter-set-${setId}`));
4808 if(!$starsNumber.length){
4809 $starsNumber = $(`option[data-wpc-e-name="${chip.wpcEName}"][data-wpc-slug="${chip.wpcSlug}"]:selected`, $(`.wpc-filter-set-${setId}`));
4810 }
4811
4812
4813 let ratingNum = $starsNumber.data('ratingNum');
4814 let tempHtml = '';
4815 for (let i = 1; i <= ratingNum; i++) {
4816 tempHtml += `<span>${flrt_rating_star()}</span>`;
4817 }
4818 return `<li class="wpc-filter-chip ${chip.entityClass}"><span data-wpc-e-name="${chip.wpcEName}" data-wpc-slug="${chip.wpcSlug}" class="wpc-apply-button-chip" title="${title}"><span class="wpc-chip-content"><span class="wpc-chip-stars">${tempHtml} </span><span class="wpc-chip-remove-icon">×</span></span></span></li>`;
4819
4820 }
4821
4822 function flrtClearChips(setId){
4823 $('.wpc-filter-chips-'+setId + ' li').remove().addClass('wpc-empty-chips-container');
4824 }
4825
4826 function wpcSprintf(format, ...args) {
4827 let index = 0;
4828 return format.replace(/%(\.\d+)?([sdf%])/g, (match, precision, type) => {
4829 if (type === '%') return '%';
4830 const arg = args[index++];
4831 switch (type) {
4832 case 's': return String(arg ?? '');
4833 case 'd': return parseInt(arg ?? 0, 10);
4834 case 'f': return precision
4835 ? parseFloat(arg ?? 0).toFixed(parseInt(precision.slice(1)))
4836 : parseFloat(arg ?? 0).toString();
4837 default: return match;
4838 }
4839 });
4840 }
4841
4842 function unsetChip($chipElement){
4843 const wpcEName = $chipElement.data('wpcEName');
4844 const wpcSlug = $chipElement.data('wpcSlug');
4845 const setId = $chipElement.closest('.wpc-filter-chips-list').data('set')
4846 const $el = $(`.widget_wpc_filters_widget .wpc-filter-set-${setId}`);
4847 if(typeof wpcEName !== 'undefined' && typeof wpcSlug !== 'undefined'){
4848 const $inputElement = $el.find('[data-wpc-e-name="' + wpcEName + '"][data-wpc-slug="' + wpcSlug + '"]').not('.wpc-apply-button-chip');
4849 if($inputElement.length > 0){
4850 resetInputElement($inputElement)
4851 applyJsMode($el, setId)
4852 }
4853 }
4854 }
4855 function resetInputElement($inputElement) {
4856 const inputData = $inputElement.data();
4857 const type = $inputElement.prop('type');
4858 const isOption = ($inputElement.prop('tagName') === 'OPTION');
4859
4860 const { absMin, absMax, wpcAbsFromRaw, wpcAbsToRaw, wpcAbsFrom, wpcAbsTo } = inputData;
4861
4862 if (absMin !== undefined) {
4863 $inputElement.val(absMin).data('min', absMin).change();
4864 } else if (absMax !== undefined) {
4865 $inputElement.val(absMax).data('max', absMax).change();
4866 } else if (wpcAbsFromRaw !== undefined) {
4867 $inputElement.val(wpcAbsFrom)[wpcPickerFn()]('setDate', new Date(wpcAbsFromRaw));
4868 } else if (wpcAbsToRaw !== undefined) {
4869 $inputElement.val(wpcAbsTo)[wpcPickerFn()]('setDate', new Date(wpcAbsToRaw));
4870 } else if (type === 'checkbox' || type === 'radio') {
4871 // Clear BOTH the attribute and jQuery data: updateCounters selects
4872 // radios by the [data-wpc-was-checked=true] ATTRIBUTE, and .data()
4873 // alone does not update it — the term would stay "selected"
4874 $inputElement.prop('checked', false)
4875 .attr('data-wpc-was-checked', false)
4876 .data('wpc-was-checked', false);
4877 // The old guard compared type to 'react-radio', which never occurs —
4878 // dead code — and the fill classes span the WHOLE star group, not one li
4879 if ($inputElement.hasClass('flrt-star-input')) {
4880 $inputElement
4881 .closest('.flrt-stars-wpc-filter-content')
4882 .find('label.flrt-star-label')
4883 .removeClass('flrt-star-label-hover flrt-star-label-checked flrt-star-label-not-checked')
4884 .data('wpc-was-checked', false);
4885 }
4886 }else if (isOption) {
4887 const select = $inputElement.closest('select');
4888 const defaultValue = select.find('.wpc-dropdown-default').val();
4889 select.val(defaultValue).trigger('change')
4890 $inputElement.prop('selected', false);
4891
4892 }
4893 }
4894
4895 // Merges the inline page part (filteredAllPostsIds, domain, permalinks…)
4896 // into a blob: per-set keys are object-assigned, root keys replaced
4897 function wpcMergePagePart(target, pagePart) {
4898 for (const k in pagePart) {
4899 if (k === 'blobUrl') continue;
4900 if (target[k] && typeof target[k] === 'object' && !Array.isArray(target[k]) &&
4901 pagePart[k] && typeof pagePart[k] === 'object' && !Array.isArray(pagePart[k])) {
4902 Object.assign(target[k], pagePart[k]);
4903 } else {
4904 target[k] = pagePart[k];
4905 }
4906 }
4907 return target;
4908 }
4909
4910 function updateWpcFilterJsonData(responseHtml) {
4911 const parser = new DOMParser();
4912 const doc = parser.parseFromString(responseHtml, 'text/html');
4913
4914 // Inert JSON block (Plugin::inlineScriptJsonData): JSON.parse is
4915 // orders of magnitude faster than eval'ing a JS object literal
4916 const jsonEl = doc.querySelector('#wpc-filter-json-data');
4917 if (jsonEl) {
4918 try {
4919 const pagePart = JSON.parse(jsonEl.textContent);
4920
4921 if (pagePart.blobUrl) {
4922 if (pagePart.blobUrl === window.wpcFilterJsonBlobUrl && typeof window.wpcFilterJsonData !== 'undefined') {
4923 // Same blob — refresh only the page-scoped part
4924 wpcMergePagePart(window.wpcFilterJsonData, pagePart);
4925 } else {
4926 window.wpcFilterJsonBlobUrl = pagePart.blobUrl;
4927 delete window.wpcFilterJsonData;
4928 window.wpcFilterJsonDataPromise = fetch(pagePart.blobUrl, { credentials: 'same-origin' })
4929 .then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
4930 .then((blob) => {
4931 window.wpcFilterJsonData = wpcMergePagePart(blob, pagePart);
4932 return blob;
4933 })
4934 .catch((e) => { console.error('Filter Everything: filter data fetch failed', e); });
4935 }
4936 } else {
4937 window.wpcFilterJsonData = pagePart;
4938 }
4939 } catch (e) {
4940 console.error('Filter Everything: filter data JSON parse failed', e);
4941 }
4942 return;
4943 }
4944
4945 // Legacy responses carry the data as an executable literal
4946 const scriptEl = doc.querySelector('#wpc-filter-everything-js-before');
4947
4948 if (scriptEl) {
4949 let code = scriptEl.textContent;
4950 (0, eval)(code);
4951 }
4952 }
4953
4954 function wpcIsSearchUrl(){
4955 const url = new URL(window.location.href);
4956 return url.searchParams.has('srch');
4957 }
4958
4959 return {
4960 applyJsMode: function ($el, setId) { wpcWhenDataReady(function () { applyJsMode($el, setId); }); },
4961 compareInputWithRangeList: function (form, applyButtonMode) { wpcWhenDataReady(function () { compareInputWithRangeList(form, applyButtonMode); }); },
4962 unsetChip: function ($chipElement) { wpcWhenDataReady(function () { unsetChip($chipElement); }); },
4963 updateWpcFilterJsonData: updateWpcFilterJsonData
4964 };
4965
4966 })(); // end of the Apply-button recount engine (wpcApplyEngine)
4967
4968 })(jQuery);