PluginProbe
Filter Everything — WordPress & WooCommerce Filters / trunk
Filter Everything — WordPress & WooCommerce Filters vtrunk
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 1.6.3 All 51 releases
filter-everything / assets / js / filter-everything.js

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

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