PageSourceSearch

https://dechent.de/wp-content/plugins/WPCarSync/js/wpcs-core.js?ver=3.8.2

js dechent.de collected 2026-10-06 19:12:10 UTC 90,168 bytes, 2,401 lines download raw bytes

1var wpcs_lightbox = null;
2var wpcs_form = null;
3var wpcs_orderbar = null;
4var wpcs_contact_form = null;
5var wpcs_compare = null;
6var wpcs_park = null;
7var wpcs_search_request = null;
8var wpcs_infinite_scroll = null;
9var wpcs_textsearch = null;
10var wpcs_form_elements = null;
11var wpcs_2slides = null;
12
13(function($) {
14
15    $(function() {
16
17        // init Filter Form
18        wpcs_form = new WPCSForm();
19
20        // init Order Bar
21        if ($('.dxim_ff_order_bar').length > 0) wpcs_orderbar = new WPCSOrderbar();
22
23        // init Contact Form
24        if ($('.wpcs_contact_form').length > 0) wpcs_contact_form = new WPCSContactForm();
25
26        // init compare table
27        wpcs_compare = new WPCSCompare();
28
29        // textsearch
30        //wpcs_textsearch = new WPCSTextSearch();
31
32        //
33        wpcs_2slides = new WPCS2Slides();
34
35        // init Search Request
36        wpcs_search_request = new WPCSSearchRequest();
37
38        // lightbox
39        wpcs_lightbox = new WPCSLightbox();
40        wpcsInitLightbox();
41
42        // Park
43        wpcs_park = new WPCSPark();
44
45        //
46        wpcs_infinite_scroll = new WPCSInfiniteScroll();
47
48        //
49        wpcs_form_elements = new WPCSFormElements();
50
51        // WPCSSticky init
52        $('.wpcs_sticky').each(function() {
53            new WPCSSticky(this);
54        });
55
56        // WPCSScroller init
57        $('.wpcs_scroller').each(function() {
58            new WPCSScroller(this);
59        });
60
61        // FilterSet buttons
62        new WPCSFilterSet();
63
64
65        $('.dxim_vehicle_carousel').each(function() {
66            let carousel = this;
67            let title = 0;
68            let data = 0;
69
70            $('.vehicle-on-archive', carousel).each(function() {
71                if ($('.dxim_archive_specifics', this).height() > data) data = $('.dxim_archive_specifics', this).height();
72                if ($('.title', this).height() > title) title = $('.title', this).height();
73            });
74            $('.vehicle-on-archive .title', carousel).css('height', title);
75            $('.vehicle-on-archive .dxim_archive_specifics', carousel).css('height', data);
76
77        });
78
79        $('a.wpcs_toggle')
80            .click(function(e) {
81                e.preventDefault();
82                let selector = $(this).data('div');
83                $(selector).toggleClass('wpcs_hidden');
84            });
85
86        $('a.wpcs_toggle_visibility')
87            .click(function(e) {
88                e.preventDefault();
89                let a = $(this);
90                let selector = a.data('div');
91                a.toggleClass('active');
92                if ($(selector).hasClass('wpcs_not_visible_mobile')) {
93                    $(selector).removeClass('wpcs_not_visible_mobile');
94                    if ($('body').hasClass('wpcs_mobile')) $(selector).addClass('wpcs_not_visible');
95                }
96
97                $(selector).toggleClass('wpcs_not_visible');
98            });
99
100        // init text searches
101        $('.wpcs_textsearch').each(function() {
102            new WPCSTextSearch(this);
103        });
104
105        $('[data-wpcs-bind]').each(function() {
106            let selector = $(this).data('wpcs-bind');
107            let amount = parseInt($(selector).data('amount'));
108            if (amount > 0) {
109                $(this)
110                    .html(amount + " ")
111                    .removeClass('wpcs_hidden');
112            }
113        });
114
115        //
116        add_body_classes()
117
118    }); // domready
119
120    /**
121     *
122     * @constructor
123     */
124    function WPCS2Slides() {
125
126        this.__construct = function() {
127            $('.wpcs_2slides_container').each(function() {
128                let area_selector = $(this).data('area-selector');
129                $(area_selector, this).hide();
130            });
131
132            $(document).on('click', '.wpcs_2slides_activate', function(e) {
133                e.preventDefault();
134                let area = $(this).data('area');
135                let slider = $(this).closest('.wpcs_2slides_container');
136                let area_selector = slider.data('area-selector');
137                slider.addClass('active');
138                $(area_selector + '[data-area="' + area + '"]', slider).show();
139            });
140
141            $(document).on('click', '.wpcs_2slides_disable', function(e) {
142                e.preventDefault();
143                let slider = $(this).closest('.wpcs_2slides_container');
144                let area_selector = slider.data('area-selector');
145                slider.removeClass('active');
146                $(area_selector, slider).hide();
147            });
148        }
149
150        this.initItem = function(item) {
151            let area_selector = $(item).data('area-selector');
152            $(area_selector, item).hide();
153        }
154
155        this.__construct();
156
157    }
158
159
160    /**
161     *
162     * @constructor
163     */
164    function WPCSFormElements() {
165
166        var that = this;
167
168        this._construct = function() {
169            this.initWPCSSelect();
170        }
171
172        this.initWPCSSelect = function() {
173            $('.wpcsselect.custom').each(function() {
174                let select = $(this);
175                select.wpcsselect({});
176            });
177        }
178
179        this._construct();
180    }
181
182
183    function WPCSTextSearch(textsearch) {
184
185        var that = this;
186
187        this.textsearch = $(textsearch);
188        this.timeout = null;
189        this.loader = $('.wpcs_ajax_loader', this.textsearch);
190        this.textinput = $('.searchInput', this.textsearch);
191        this.close_btn = $('.wpcs_textsearch_reset_btn', this.textsearch);
192        this.results_div = null;
193
194        this._construct = function() {
195
196            let h = this.loader.height();
197            this.loader.css('margin-top', Math.round(h / 2) * -1);
198
199            let h2 = this.close_btn.height();
200            this.close_btn.css('margin-top', Math.round(h2 / 2) * -1);
201
202            let target = that.textsearch.data('target');
203
204            // standard result div
205            this.results_div = $('.' + target, that.textsearch);
206            // target angabe für externen div => muss ID sein
207            if (this.results_div.length < 1) this.results_div = $('#' + target);
208
209            $(that.textinput).keyup(function() {
210                if (that.timeout !== null) window.clearTimeout(that.timeout);
211
212                let val = $(this).val();
213                if (val.length < 3) return;
214
215                that.setCloseBtnState();
216
217                that.timeout = window.setTimeout(that._doSearch, 700, val);
218            });
219
220            this.close_btn.click(function(e) {
221                e.preventDefault();
222                that.emptyList();
223                that.textinput.val("");
224                that.setCloseBtnState();
225
226            });
227        }
228
229        /**
230         *
231         * @param val
232         */
233        this._doSearch = function(val) {
234            that.loader.show();
235            that.close_btn.hide();
236            let action = that.textsearch.data('type');
237            let args = that.textsearch.data('params');
238
239            $.ajax({
240                method: 'POST',
241                url: wpcs_ajax_url,
242                data: { action: action, searchstring: val, args: args },
243                success: function(data) {
244                    that.loader.hide();
245                    that.setCloseBtnState();
246
247                    if (data.status == "1") {
248                        console.log(that.results_div);
249                        that.results_div.html(data.html).fadeIn(100);
250                    } else if (data.status == "0") {
251                        that.emptyList();
252                    }
253                }
254            });
255        }
256
257        this.emptyList = function() {
258            that.results_div.html(null).fadeOut(150);
259        }
260
261        this.setCloseBtnState = function() {
262            let val = that.textinput.val();
263            if (val != "") that.close_btn.show();
264            else that.close_btn.hide();
265        }
266
267        this._construct();
268    }
269
270    /**
271     *
272     * @constructor
273     */
274    function WPCSInfiniteScroll() {
275        var that = this;
276
277        this.__construct = function() {
278            that._initButton();
279            that._initInfiniteLoader();
280            $(document).on("wpcs_ajax_form_submitted", that._checkButtonVisibility);
281        }
282
283
284        this._checkButtonVisibility = function() {
285            if ($('#wpcs_pagination_button').length < 1) return;
286
287            if (that._canLoadMore()) $('#wpcs_pagination_button').show();
288            else $('#wpcs_pagination_button').hide();
289        }
290
291        /**
292         * prüft ob nachlade button angezeigt werden darf
293         * @private
294         */
295        this._canLoadMore = function() {
296            let current_page = parseInt($('#wpcs_hidden_wpcsp').val());
297            if (current_page >= vehicle_list_properties.max_pages) return false;
298            else return true;
299        }
300
301
302        /**
303         * initialisiert button zum nachladen der ergebnisse
304         * @private
305         */
306        this._initButton = function() {
307            $('#wpcs_pagination_button').click(function(e) {
308                e.preventDefault();
309                that._loadVehicles();
310            });
311        }
312
313        /**
314         *
315         * @private
316         */
317        this._initInfiniteLoader = function() {
318            if ($('#wpcs_vehicle_archive').data('infinite') != "1") return;
319
320            let timeout = null;
321            $(window).on("scroll", function() {
322                if (timeout != null) window.clearTimeout(timeout);
323                timeout = window.setTimeout(function() {
324                    if ($('#wpcs_vehicle_archive .vehicle-on-archive').length < 1) return;
325
326                    let element_pos = parseInt($('#wpcs_vehicle_archive .vehicle-on-archive').last().offset().top);
327                    if (wpcs_form.is_loading || !that._canLoadMore()) return;
328                    var scrollHeight = $(document).height();
329                    var scrollPos = $(window).height() + $(window).scrollTop();
330                    if (scrollPos >= element_pos) that._loadVehicles();
331                }, 400);
332            });
333        }
334
335        /**
336         *
337         * @private
338         */
339        this._loadVehicles = function() {
340            let current_page = parseInt($('#wpcs_hidden_wpcsp').val());
341            let new_page = current_page + 1;
342            $('#wpcs_hidden_wpcsp').val(new_page);
343
344            wpcs_form.submitAjax(1);
345        }
346
347        this.__construct();
348    }
349
350    /**
351     *
352     * @constructor
353     */
354    function WPCSSearchRequest() {
355
356        var that = this;
357
358        this.loader = $('.wpcs_ajax_loader[data-category=searchRequest]');
359
360        this.__construct = function() {
361            this._initSaveSearch();
362            this._initList();
363        }
364
365        /**
366         *
367         * @param wrapper
368         * @private
369         */
370        this._initSearch = function(sid) {
371            let wrapper = $('.wpcs_save_search_request_wrapper[data-sid="' + sid + '"]');
372            $('.wpcs_save_search_request_btn', wrapper).click(function(e) {
373                e.preventDefault();
374                var btn = $(this);
375                var spinner = $('.wpcs_icon_spinner', btn);
376                spinner.css('display', 'inline-block');
377
378                let email = $('.wpcs_sr_email_input', wrapper);
379                let email_val = email.val();
380                let privacy = $('input[name=wpcs_sr_privacy]', wrapper).is(':checked') ? 1 : 0;
381
382                $.ajax({
383                    method: 'POST',
384                    url: wpcs_ajax_url,
385                    data: { action: 'wpcs_search_request', todo: 'addSearchRequest', email: email_val, privacy: privacy, formData: wpcs_form.serialize() },
386                    success: function(data) {
387                        spinner.hide();
388
389                        if (data.error == "1") {
390                            $('.status', wrapper).addClass('error').removeClass('success').html(data.msg);
391                        } else {
392                            $('.status', wrapper).addClass('success').removeClass('error').html(data.msg);
393                            email.val('');
394                        }
395                    }
396                });
397            });
398
399            $('.privacy_btn', wrapper).click(function(e) {
400                e.preventDefault();
401                $('.wpcs_vsr_privacy_text', wrapper).slideToggle();
402            })
403        }
404
405
406        this._initSaveSearch = function() {
407            $('.wpcs_save_search_request_wrapper').each(function() {
408                let wrapper = this;
409                let sid = $(wrapper).data('sid');
410                that._initSearch(sid);
411            });
412        }
413
414        this._initList = function() {
415            $('#wpcs_vsq_list .wpcs_search_request').each(function() {
416                let wrapper = this;
417                $('.delete_btn', wrapper).click(function(e) {
418                    e.preventDefault();
419                    let button = this;
420                    let hash = $('#wpcs_vsq_list').data('hash');
421                    let time = $(wrapper).data('time');
422
423                    var spinner = $('.wpcs_icon_spinner', button);
424                    if (spinner.length) spinner.css('display', 'inline-block');
425                    else that.loader.show();
426                    $('.delete_icon', button).hide();
427
428                    $.ajax({
429                        method: 'POST',
430                        url: wpcs_ajax_url,
431                        data: { action: 'wpcs_search_request', todo: 'removeSearchRequest', hash: hash, time: time },
432                        success: function(data) {
433                            if (spinner.length) spinner.hide();
434                            else that.loader.hide();
435                            $('.delete_icon', button).show();
436
437                            if (data.error == "1") {
438                                alert(data.msg);
439                            } else {
440                                $(wrapper).remove();
441                            }
442                        }
443                    });
444                });
445            });
446        }
447
448        this.__construct();
449
450    }
451
452    /**
453     *
454     * @constructor
455     */
456    function WPCSPark() {
457
458        var that = this;
459
460        this.vehicles_list_html = null;
461        this.vehicle_count = 0;
462
463        this.__construct = function() {
464            this._initToggleButtons()
465            this._initList();
466        }
467
468        this._initList = function() {
469            $('#wpcs_vehicle_parked .vehicle').each(function() {
470
471                let vehicle = this;
472                let vehicle_id = $(this).attr('data-vehicle-id');
473
474                $('.wpcs_park_remove_btn', vehicle).click(function(e) {
475                    e.preventDefault();
476                    $('.wpcs_ajax_loader', vehicle).show();
477                    $.ajax({
478                        method: 'POST',
479                        url: wpcs_ajax_url,
480                        data: { action: 'wpcs_park', todo: 'removeFromList', vehicle_id: vehicle_id },
481                        success: function(data) {
482                            if (!data.error) {
483                                that.vehicles_list_html = data.list;
484                                that.vehicle_count = data.count;
485                                that._setList();
486                            }
487                        }
488                    });
489                });
490
491            });
492        }
493
494        this._initToggleButtons = function() {
495            $('.wpcs_park_toggle_btn').click(function(e) {
496                e.preventDefault();
497                var btn = $(this);
498                var todo = $(this).hasClass('wpcs_is_parked') ? 'removeFromList' : 'addToList';
499                var vehicle_id = $(this).attr('data-vehicle-id');
500                var spinner = $('.wpcs_icon_spinner', btn);
501                spinner.css('display', 'inline-block');
502                $.ajax({
503                    method: 'POST',
504                    url: wpcs_ajax_url,
505                    data: { action: 'wpcs_park', todo: todo, vehicle_id: vehicle_id },
506                    success: function(data) {
507                        spinner.hide();
508                        if (!data.error) {
509                            btn.toggleClass('wpcs_is_parked');
510                            that.vehicles_list_html = data.list;
511                            that.vehicle_count = data.count;
512                            that._setList();
513                        }
514                    }
515                });
516            });
517        }
518
519        /**
520         *
521         * @param vehicles
522         * @private
523         */
524        this._setList = function() {
525
526            if (that.vehicles_list_html !== null) {
527                $('#wpcs_vehicle_parked .vehicles_wrapper').html(that.vehicles_list_html);
528            }
529
530            that._setButtonState();
531            that._refreshCounter();
532            that._initList();
533
534        }
535
536        /**
537         *
538         * @param count
539         */
540        this._refreshCounter = function() {
541            let element = $('.wpcs_count_vehicles_parked');
542            element.html(that.vehicle_count);
543
544            // animation
545            element.each(function() {
546                this.classList.remove("wpcs_animated");
547                this.offsetWidth;
548                this.classList.add('wpcs_animated');
549            });
550
551        }
552
553        /**
554         *
555         * @private
556         */
557        this._setButtonState = function() {
558            if ($('#wpcs_vehicle_parked').length < 1) return;
559
560            $('.wpcs_park_toggle_btn').each(function() {
561                $(this).removeClass('wpcs_is_parked');
562            });
563
564            $('#wpcs_vehicle_parked .vehicle').each(function() {
565                let id = $(this).data('vehicle-id');
566                $('.wpcs_park_toggle_btn[data-vehicle-id=' + id + ']').addClass('wpcs_is_parked');
567            });
568        }
569
570        /**
571         *
572         * @param vehicle_id
573         * @private
574         */
575        this._removeFromList = function(vehicle_id) {
576            $('#wpcs_vehicle_parked .vehicle[data-vehicle-id=' + vehicle_id + ']').remove();
577        }
578
579        this.__construct();
580    }
581
582
583    /**
584     *
585     * @constructor
586     */
587    function WPCSLightbox() {
588        var that = this;
589        this.gallery = $('#wpcs_vehicle_images_wrapper');
590        this.current = 0;
591        this.lightbox = null;
592
593        /**
594         *
595         * @private
596         */
597        this.__construct = function() {
598
599            if ($('#wpcs_vehicle_images_wrapper.wpcs_lightbox_enabled').length < 1) return;
600
601
602            $('#wpcs_vehicle_single_image_wrapper a.lightbox_opener').click(function(e) {
603                e.preventDefault();
604                e.stopPropagation();
605                that._open($('#wpcs_vehicle_single_image').attr('src'));
606            });
607        };
608
609        this._create = function() {
610            var html = '';
611
612            html += '<div id="wpcs_lightbox">';
613            html += '<div class="close"><span class="wpcs_icon dashicons dashicons-no-alt"></span></div>';
614            html += '<div class="image_wrapper">';
615            html += '<div class="arrow arrow_left"><span class="wpcs_icon dashicons dashicons-arrow-left-alt2"></span></div>';
616            html += '<img id="wpcs_lightbox_image" alt="' + wpcs_lightbox_alt + '" src="" />';
617            html += '<div class="arrow arrow_right"><span class="wpcs_icon dashicons dashicons-arrow-right-alt2"></span></div>';
618            html += '</div>';
619            html += '</div>';
620
621            this.lightbox = $(html);
622
623            $('body').prepend(this.lightbox);
624
625            $('.arrow_right', this.lightbox).click(function() { that._next(); });
626            $('.arrow_left', this.lightbox).click(function() { that._prev(); });
627            $('.close', this.lightbox).click(function() { that._close(); });
628
629        };
630
631
632        this._next = function() {
633            this.current++;
634            if (this.current > wpcs_lightbox_sources.length - 1) this.current = 0;
635            this._change(wpcs_lightbox_sources[this.current]);
636        };
637
638        this._prev = function() {
639            this.current--;
640            if (this.current < 0) this.current = wpcs_lightbox_sources.length - 1;
641            this._change(wpcs_lightbox_sources[this.current]);
642        };
643
644        this._change = function(src) {
645            $('#wpcs_lightbox_image', this.lightbox).attr('src', src);
646        };
647
648
649        this._open = function(src) {
650            if (this.lightbox == null) this._create();
651            this._change(src);
652            this.lightbox.fadeIn();
653        };
654
655
656        this._close = function() {
657            this.lightbox.fadeOut();
658        };
659
660        if (this.gallery.length > 0)
661            this.__construct();
662
663    }
664
665    /**
666     * Global lightbox — works on any page via data attributes.
667     *
668     * Trigger: any element with data-lightbox-source="<CSS selector>"
669     *   data-lightbox-index="N"        optional start index (default 0)
670     *   data-lightbox-sync="gallery"   sync with window._wpcsGallery while navigating
671     *
672     * The overlay (#wpcs-lightbox) is created once on first open if it doesn't exist yet.
673     */
674    function wpcsInitLightbox() {
675
676        var overlay     = null;
677        var lbSwiper    = null;
678        var currentImgs = [];
679        var syncGallery = false;
680
681        function ensureOverlay() {
682            if (document.getElementById('wpcs-lightbox')) {
683                overlay = document.getElementById('wpcs-lightbox');
684                return;
685            }
686            overlay = document.createElement('div');
687            overlay.id        = 'wpcs-lightbox';
688            overlay.className = 'wpcs-lightbox';
689            overlay.innerHTML =
690                '<div class="wpcs-lightbox__inner">' +
691                    '<button class="wpcs-lightbox__close" aria-label="Schließen">✕</button>' +
692                    '<div class="wpcs-lightbox__swiper swiper">' +
693                        '<div class="swiper-wrapper"></div>' +
694                    '</div>' +
695                    '<div class="wpcs-lightbox__pagination swiper-pagination"></div>' +
696                    '<button class="wpcs-lightbox__nav wpcs-lightbox__nav--prev" aria-label="Vorheriges Bild">' +
697                        '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>' +
698                    '</button>' +
699                    '<button class="wpcs-lightbox__nav wpcs-lightbox__nav--next" aria-label="Nächstes Bild">' +
700                        '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>' +
701                    '</button>' +
702                '</div>';
703            document.body.appendChild(overlay);
704            overlay.querySelector('.wpcs-lightbox__close').addEventListener('click', closeLightbox);
705            overlay.addEventListener('click', function(e) { if (e.target === overlay) closeLightbox(); });
706        }
707
708        function buildSlides() {
709            var wrapper = overlay.querySelector('.swiper-wrapper');
710            wrapper.innerHTML = '';
711            currentImgs.forEach(function(el) {
712                var src   = el.dataset.lightboxSrc || el.getAttribute('src');
713                var slide = document.createElement('div');
714                slide.className   = 'swiper-slide';
715                slide.innerHTML   = '<img src="' + src + '" alt="">';
716                wrapper.appendChild(slide);
717            });
718        }
719
720        function openLightbox(imgs, index, sync) {
721            ensureOverlay();
722            currentImgs = imgs;
723            syncGallery = !!sync;
724
725            buildSlides();
726
727            if (lbSwiper) { lbSwiper.destroy(true, true); lbSwiper = null; }
728
729            overlay.classList.add('open');
730            document.body.style.overflow = 'hidden';
731
732            lbSwiper = new Swiper(overlay.querySelector('.swiper'), {
733                initialSlide : index || 0,
734                loop         : currentImgs.length > 1,
735                keyboard     : { enabled: true },
736                navigation   : {
737                    nextEl: overlay.querySelector('.wpcs-lightbox__nav--next'),
738                    prevEl: overlay.querySelector('.wpcs-lightbox__nav--prev'),
739                },
740                pagination   : {
741                    el   : overlay.querySelector('.wpcs-lightbox__pagination'),
742                    type : 'fraction',
743                },
744                on: {
745                    slideChange: function() {
746                        if (syncGallery && window._wpcsGallery) {
747                            window._wpcsGallery.goTo(lbSwiper.realIndex);
748                        }
749                    }
750                }
751            });
752        }
753
754        function closeLightbox() {
755            if (overlay) overlay.classList.remove('open');
756            document.body.style.overflow = '';
757            if (lbSwiper) { lbSwiper.destroy(true, true); lbSwiper = null; }
758        }
759
760        // Event delegation — works for dynamically rendered galleries too
761        document.addEventListener('click', function(e) {
762            var trigger = e.target.closest('[data-lightbox-source]');
763            if (!trigger) return;
764            e.preventDefault();
765            var imgs  = Array.from(document.querySelectorAll(trigger.dataset.lightboxSource))
766                            .filter(function(img) { return !img.closest('.slick-cloned'); });
767            if (!imgs.length) return;
768            var sync  = trigger.dataset.lightboxSync === 'gallery';
769            var index = sync && window._wpcsGallery
770                ? window._wpcsGallery.getCurrent()
771                : parseInt(trigger.dataset.lightboxIndex || '0', 10);
772            openLightbox(imgs, index, sync);
773        });
774
775        document.addEventListener('keydown', function(e) {
776            if (!overlay || !overlay.classList.contains('open')) return;
777            if (e.key === 'Escape') closeLightbox();
778        });
779    }
780
781    /**
782     *
783     * @constructor
784     */
785    function WPCSCompare() {
786
787        var that = this;
788        var table = null;
789
790        /**
791         *
792         * @private
793         */
794        this.__construct = function() {
795            this.table = $('#wpcs_compare_table');
796            this.vehicle_count = null;
797
798            // init compare table
799            if (this.table.length > 0) {
800                this.initSectionLinks();
801                this.initRemoveBtn();
802                //this._setWidth();
803            }
804
805            this._init_compare_buttons();
806        };
807
808        this._init_compare_buttons = function(collection) {
809            $('.wpcs_compare_toggle_btn', collection).click(function(e) {
810                e.preventDefault();
811                var btn = $(this);
812                var todo = $(this).hasClass('wpcs_in_comparison') ? 'removeFromList' : 'addToList';
813                var vehicle_id = $(this).attr('data-vehicle-id');
814                var spinner = $('.wpcs_icon_spinner', btn);
815                spinner.css('display', 'inline-block');
816                $.ajax({
817                    method: 'POST',
818                    url: wpcs_ajax_url,
819                    data: { action: 'wpcs_compare', todo: todo, vehicle_id: vehicle_id },
820                    success: function(data) {
821                        spinner.hide();
822                        if (!data.error) {
823                            btn.toggleClass('wpcs_in_comparison');
824                            that.vehicle_count = data.count;
825                            that._refreshCounter();
826                        }
827                    }
828                });
829            });
830        }
831
832        this._refreshCounter = function() {
833            let element = $('.wpcs_count_vehicles_compared');
834            element.html(that.vehicle_count);
835
836            // animation
837            element.each(function() {
838                this.classList.remove("wpcs_animated");
839                this.offsetWidth;
840                this.classList.add('wpcs_animated');
841            });
842        }
843
844        /**
845         *
846         */
847        this.initSectionLinks = function() {
848            $('a.section_link', that.table).click(function(e) {
849                e.preventDefault();
850                var rel = $(this).attr('rel');
851
852                //$(this).parent().toggleClass('opened');
853                $('tr[rel=' + rel + ']').toggleClass('wpcs_hidden');
854                $(this).toggleClass('closed');
855            });
856        };
857
858        /**
859         *
860         */
861        this.initRemoveBtn = function() {
862            $('.wpcs_compare_table_remove_btn').click(function(e) {
863                e.preventDefault();
864                var vehicle_id = $(this).attr('data-vehicle-id');
865                var btn = this;
866                var spinner = $('.wpcs_icon_spinner', btn);
867                spinner.css('display', 'inline-block');
868
869                $.ajax({
870                    method: 'POST',
871                    url: wpcs_ajax_url,
872                    data: { action: 'wpcs_compare', todo: 'removeFromList', vehicle_id: vehicle_id },
873                    success: function(data) {
874                        spinner.hide();
875                        if (!data.error) {
876                            that._removeFromTable(vehicle_id);
877                        }
878                    }
879                });
880
881            });
882        };
883
884        /**
885         *
886         * @private
887         */
888        this._setWidth = function() {
889            var count = this._countCars() + 1;
890            var percent = 100 / count;
891            $('td', this.table).not('.header').css('width', percent + '%');
892        };
893
894        /**
895         *
896         * @param vehicle_id
897         * @private
898         */
899        this._removeFromTable = function(vehicle_id) {
900            jQuery('td[data-vehicle-id=' + vehicle_id + ']', this.table).remove();
901
902            var count = this._countCars();
903            $('td.header', this.table).attr('colspan', count);
904
905            //this._setWidth();
906
907            if (count == 1) {
908                //this.table.remove();
909                $('#wpcs_vehicle_compare').html('<h3>Keine Fahrzeuge zum Vergleichen ausgewählt.</h3>');
910            }
911        };
912
913        /**
914         *
915         * @returns {number}
916         * @private
917         */
918        this._countCars = function() {
919            var cars = $('td.header', this.table).first().attr('colspan') - 1;
920            return cars;
921        };
922
923        /**
924         *
925         */
926        this.__construct();
927    }
928
929
930    /**
931     *
932     * @constructor
933     */
934    function WPCSOrderbar() {
935
936        var that = this;
937
938        this.__construct = function() {
939            this.initOrderBar();
940        };
941
942        this.initOrderBar = function() {
943            $('.dxim_ff_order_bar a.dxim_set_order').click(function(e) {
944                e.preventDefault();
945
946                var orderby = $(this).attr('data-orderby');
947                var order = $('.dxim_ff_order_bar').attr('data-order');
948                var new_order = (order == 'asc') ? 'desc' : 'asc';
949
950
951                $('#dxim_orderby_hidden').val(orderby);
952                $('#dxim_order_hidden').val(new_order);
953
954                that.refresh(new_order, orderby);
955
956                wpcs_form.submit();
957            });
958        };
959
960        this.refresh = function(order, orderby) {
961            $('.dxim_ff_order_bar').each(function() {
962                let bar = $(this);
963                bar.attr('data-order', order);
964
965                $('a.dxim_set_order', bar).each(function() {
966                    var tmp_orderby = $(this).attr('data-orderby');
967                    if (tmp_orderby == orderby) $(this).addClass('current');
968                    else $(this).removeClass('current');
969                });
970            });
971
972        }
973
974        this.__construct();
975
976    }
977
978    function animateValue(obj, start, end, duration) {
979        let startTimestamp = null;
980        const step = (timestamp) => {
981            if (!startTimestamp) startTimestamp = timestamp;
982            const progress = Math.min((timestamp - startTimestamp) / duration, 1);
983            let value = Math.floor(progress * (end - start) + start);
984            $(obj).attr('data-value', value);
985            obj.innerHTML = value.toLocaleString('de-DE');
986            if (progress < 1) {
987                window.requestAnimationFrame(step);
988            }
989        };
990        window.requestAnimationFrame(step);
991    }
992
993    /** ----------------------------------------------------------------------------------
994     * Filterform
995     * @constructor
996     */
997    function WPCSForm() {
998        var that = this;
999        var ajax = false;
1000
1001        this.is_loading = false;
1002        this.isResetting = false;
1003        this.preFilterTimeout = null;
1004        this.scrollTopAfterSubmit = false;
1005
1006        this.keyUpDelay = null;
1007
1008        /**
1009         *
1010         * @returns {*|jQuery}
1011         */
1012        this.serialize = function() {
1013            that._serializeMultipleCheckbox();
1014            let formdata = $('.dxim_filter :input').serialize();
1015            return formdata;
1016        }
1017
1018        /**
1019         *
1020         * @private
1021         */
1022        this.__construct = function() {
1023
1024            if ($('#wpcs_vehicle_archive[data-ajax="1"]').length > 0) this.ajax = 1;
1025
1026            this.initWpcsSelect();
1027            this.initCheckboxes();
1028            this.initSlider();
1029            this.initActiveFilters();
1030            this.initRange();
1031            this.initSubmit();
1032            this.initColorSelect();
1033            this.initPagination();
1034            this.initMultipleCheckbox();
1035            this.initTextField();
1036            this.initLayoutSelector();
1037
1038            $(document).on('wpcs_ajax_form_submitted', this.scrollTop);
1039            $(document).on('wpcs_reset_all_filters', function() { that.resetAllFilters(); });
1040            $(document).on('click', '.wpcs_reset_all_filters_btn', function(e) { e.preventDefault(); that.resetAllFilters(); });
1041        };
1042
1043        this.scrollTop = function() {
1044            if (!that.scrollTopAfterSubmit) return;
1045            $([document.documentElement, document.body]).animate({
1046                scrollTop: $(".wpcs_vehicles").offset().top
1047            }, 1000);
1048            that.scrollTopAfterSubmit = false;
1049        }
1050
1051        this._serializeMultipleCheckbox = function() {
1052            $('.wpcs_multiple_checkbox').each(function() {
1053                let wrapper = this;
1054                let hidden = $('.wpcs_element_value', wrapper);
1055
1056                let valueStr = '';
1057                $('.wpcs_selectable.selected', wrapper).each(function(z) {
1058                    if (z != 0) valueStr += ',';
1059                    valueStr += $(this).data('value');
1060                });
1061                hidden.val(valueStr);
1062            });
1063        }
1064
1065        this.resetMultipleCheckbox = function(filter) {
1066            $('.wpcs_multiple_checkbox[data-filter="' + filter + '"] .wpcs_selectable.selected').removeClass('selected');
1067        }
1068
1069        this.initMultipleCheckbox = function() {
1070            $('.wpcs_multiple_checkbox').each(function() {
1071                let wrapper = this;
1072                $('.wpcs_selectable', wrapper).click(function() {
1073                    $(this).toggleClass('selected');
1074                    that._preFilterAmount();
1075                });
1076            });
1077        }
1078
1079        this.initRange = function() {
1080            if (typeof noUiSlider === 'undefined') return;
1081            $('.dxim_filter_range').each(function() {
1082                var slider_timeout = null;
1083                let slider_wrapper = $(this);
1084                let key = slider_wrapper.data('key');
1085                let filter_1 = slider_wrapper.data('filter-1');
1086                let filter_2 = slider_wrapper.data('filter-2');
1087                let start_val = slider_wrapper.data('start');
1088                let step = slider_wrapper.data('step');
1089                let end_val = slider_wrapper.data('end');
1090                let slider = document.getElementById('wpcs_fs_' + key);
1091                let range = wpcs_slider_ranges[key];
1092                let unit = slider_wrapper.data('unit');
1093                let label = slider_wrapper.data('label');
1094                let formatting = parseInt(slider_wrapper.data('formatting'));
1095
1096                let input_1 = jQuery('#wpcs_fv_' + filter_1);
1097                let label_1 = jQuery('#wpcs_fl_' + filter_1);
1098                let input_2 = jQuery('#wpcs_fv_' + filter_2);
1099                let label_2 = jQuery('#wpcs_fl_' + filter_2);
1100
1101                let min_input = jQuery('.min_input', slider_wrapper);
1102                let max_input = jQuery('.max_input', slider_wrapper);
1103
1104                noUiSlider.create(slider, {
1105                    start: [start_val, end_val],
1106                    format: {
1107                        to: function(value) { return Math.round(value); },
1108                        from: function(value) { return Math.round(value); }
1109                    },
1110                    step: step,
1111                    connect: true,
1112                    range: range
1113                });
1114
1115                slider.noUiSlider.on('update', function(values) {
1116                    let val_min = values[0];
1117                    let val_max = values[1];
1118                    let html_min = val_min;
1119                    let html_max = val_max;
1120                    min_input.val(val_min);
1121                    max_input.val(val_max);
1122
1123                    if (val_min == range.min && val_max == range.max) {
1124                        val_min = val_max = '#';
1125                        html_min = range.min;
1126                        html_max = range.max;
1127                    }
1128
1129                    if (val_min == '#' && val_max == '#') slider_wrapper.addClass('inactive');
1130                    else slider_wrapper.removeClass('inactive');
1131
1132
1133                    input_1.val(val_min);
1134                    input_2.val(val_max);
1135
1136                    if (formatting > 0) {
1137                        html_min = html_min.toLocaleString('de-DE');
1138                        html_max = html_max.toLocaleString('de-DE');
1139                    }
1140
1141                    label_1.html(html_min + " " + unit);
1142                    label_2.html(html_max + " " + unit);
1143                });
1144
1145                slider.noUiSlider.on('set', function() {
1146                    that._preFilterAmount();
1147                });
1148
1149                $('.min_input, .max_input', slider_wrapper).focus(function() {
1150                    $(this).data('current-value', $(this).val());
1151                    $(this).val("");
1152                });
1153
1154                $('.min_input, .max_input', slider_wrapper).focusout(function() {
1155                    let val = $(this).val();
1156                    if (val === "") $(this).val($(this).data('current-value'));
1157                    slider.noUiSlider.set([min_input.val(), max_input.val()]);
1158                });
1159
1160                $('.noUi-handle-upper', slider_wrapper).attr('aria-label', label + " größter Wert");
1161                $('.noUi-handle-lower', slider_wrapper).attr('aria-label', label + " kleinster Wert");
1162
1163            });
1164
1165        };
1166
1167        this.initSlider = function() {
1168            if (typeof noUiSlider === 'undefined') return;
1169            $('.dxim_filter_slider').each(function() {
1170                let slider_timeout = null;
1171                let slider_wrapper = $(this);
1172                let key = slider_wrapper.attr('data-key');
1173                let start_val = slider_wrapper.attr('data-start');
1174                let default_value = slider_wrapper.data('default');
1175                let unit = slider_wrapper.data('unit');
1176                let formatting = parseInt(slider_wrapper.data('formatting'));
1177                let step = slider_wrapper.data('step');
1178                let label = slider_wrapper.data('label');
1179                let slider = document.getElementById('wpcs_fs_' + key);
1180                let range = wpcs_slider_ranges[key];
1181                let input = jQuery('#wpcs_fv_' + key);
1182                let label_current = jQuery('#wpcs_fl_' + key + '_current');
1183                let input_label = jQuery('.input_label', slider_wrapper);
1184
1185                noUiSlider.create(slider, {
1186                    start: [start_val],
1187                    connect: 'lower',
1188                    range: range,
1189                    step: step,
1190                });
1191
1192                slider.noUiSlider.on('update', function(value) {
1193                    let val = Math.round(value);
1194                    let label = val;
1195
1196                    input_label.val(val);
1197
1198                    if (value == default_value) {
1199                        val = '#';
1200                        label = default_value;
1201                    }
1202
1203                    if (formatting > 0) {
1204                        label = label.toLocaleString('de-DE');
1205                    }
1206
1207                    input.val(val);
1208                    label_current.html(label + " " + unit);
1209                });
1210
1211                slider.noUiSlider.on('set', function(value) {
1212                    that._preFilterAmount();
1213                });
1214
1215                $('.input_label', slider_wrapper).focus(function() {
1216                    $(this).data('current-value', $(this).val());
1217                    $(this).val("");
1218                });
1219
1220                $('.input_label', slider_wrapper).focusout(function() {
1221                    let val = $(this).val();
1222                    //if(val === "" || val < range.min || val > range.max) {
1223                    if (val === "") {
1224                        $(this).val($(this).data('current-value'));
1225                        val = $(this).val();
1226                    }
1227                    slider.noUiSlider.set(val);
1228                });
1229
1230                $('.noUi-handle-lower', slider_wrapper).attr('aria-label', "setze Wert für " + label);
1231
1232            });
1233        };
1234
1235        this.initColorSelect = function() {
1236            $('.wpcs_color_select').each(function() {
1237                let c = this;
1238                $('.color', c)
1239                    .click(function() {
1240                        let i = 0;
1241                        let values = []
1242                        $(this).toggleClass('active');
1243                        $('.color', c).each(function() {
1244                            if ($(this).hasClass('active')) {
1245                                values[i] = $(this).data('key');
1246                                i++;
1247                            }
1248                        });
1249
1250                        $('.color_value', c).val(values.join(","));
1251                        that._preFilterAmount();
1252                    })
1253                    .keydown(function(e) {
1254                        if (e.key === "Enter" || e.key === " ") {
1255                            $(this).trigger('click');
1256                        }
1257                    });
1258            });
1259        }
1260
1261
1262        this.initTextField = function() {
1263            $('.dxim_filter input[type=text]').keyup(function() {
1264                if (that.keyUpDelay !== null) window.clearTimeout(that.keyUpDelay);
1265                that.keyUpDelay = window.setTimeout(function() {
1266                    that._preFilterAmount();
1267                }, 800);
1268            });
1269        }
1270
1271
1272        this.initLayoutSelector = function() {
1273            $('#wpcs_archive_layout_selector .button')
1274                .click(function() {
1275                    $('#wpcs_archive_layout_selector .button').removeClass('active');
1276
1277                    let mode = $(this).data('type');
1278
1279                    $('#wpcs_archive_layout_selector input').val(mode);
1280                    vehicle_list_properties.layout = mode;
1281
1282                    $(this).addClass('active');
1283                    that.submit();
1284                })
1285                .keydown(function(e) {
1286                    if (e.key === "Enter" || e.key === " ") $(this).trigger('click');
1287                });
1288        }
1289
1290
1291        this.initPagination = function() {
1292            // Vehicle-Archive-Pagination: Links mit data-wpcsp Attribut sind eindeutig
1293            // dem Fahrzeugarchiv zugeordnet (Hidden-Input #wpcs_hidden_wpcsp).
1294            // Theme-Archive (Angebote, Jobs) nutzen href="/page/N/" ohne data-wpcsp und
1295            // werden hier nicht abgefangen — sie navigieren normal.
1296            // Pagination steht im Markup ausserhalb von #wpcs_vehicle_ar
1296chive, daher
1297            // kein Container-Scope (sonst greift der Handler nie).
1298            $('.wpcs-pagination a[data-wpcsp]').off('click.wpcs-pagination').on('click.wpcs-pagination', function(e) {
1299                if ($('#wpcs_hidden_wpcsp').length === 0) return; // kein Vehicle-Filter-Form auf Seite
1300                e.preventDefault();
1301                let wpcsp = $(this).data('wpcsp');
1302                $('#wpcs_hidden_wpcsp').val(wpcsp);
1303
1304                that.scrollTopAfterSubmit = true;
1305                that.submit();
1306            });
1307        }
1308
1309        /**
1310         *
1311         */
1312        this.initWpcsSelect = function() {
1313            $('.wpcsselect', '.dxim_filter').each(function() {
1314                let select = $(this);
1315                let field = select.data('field');
1316                let dxim_filter = $('.dxim_filter[data-filter-name="' + field + '"]');
1317                let load = dxim_filter.data('load');
1318                let values = dxim_filter.data('values');
1319
1320                if (select.hasClass('dxim_order_select')) {
1321                    select.wpcsselect({
1322                        onSelect: function() {
1323                            let val = $('.wpcs_select_value', select).val();
1324                            $('#dxim_order_hidden').val(val);
1325                            if (that.ajax) that._preFilterAmount();
1326                        }
1327                    });
1328                } else if (select.hasClass('dxim_orderby_select')) {
1329                    select.wpcsselect({
1330                        onSelect: function() {
1331                            let val = $('.wpcs_select_value', select).val();
1332                            $('#dxim_orderby_hidden').val(val);
1333                            if (that.ajax) that._preFilterAmount();
1334                        }
1335                    });
1336                } else {
1337                    select.wpcsselect({
1338                        ajax: {
1339                            url: wpcs_ajax_url,
1340                            dataType: 'json',
1341                            method: 'POST',
1342                            data: function() {
1343                                var query = {
1344                                    action: 'getData',
1345                                    field: field,
1346                                    load: load,
1347                                    values: values,
1348                                    init_ajax_data: 1,
1349                                    filterFormData: that.serialize(),
1350                                };
1351                                return query;
1352                            },
1353                        },
1354                        onSelect: function() {
1355                            if (field === "make") {
1356                                that.resetSelect("model");
1357                            }
1358
1359                            if (that.preFilterTimeout !== null) window.clearTimeout(that.preFilterTimeout);
1360                            that.preFilterTimeout = window.setTimeout(that._preFilterAmount, 800);
1361                        }
1362                    });
1363                }
1364
1365            });
1366        };
1367
1368        this.initActiveFilters = function() {
1369            $('.wpcs_filterform_active_filters .active_filter').each(function() {
1370                let filter = this;
1371                let key = $(filter).data('key');
1372                let type = $(filter).data('type');
1373
1374                $('.reset', filter)
1375                    .click(function() {
1376                        if (type == "select") {
1377                            that.resetSelect(key);
1378                            that._resetPageNr();
1379                            that.submit();
1380                        }
1381                        if (type == "checkbox") {
1382                            that.resetCheckbox(key);
1383                            that._resetPageNr();
1384                            that.submit();
1385                        }
1386                        if (type == "multiple_checkbox") {
1387                            that.resetMultipleCheckbox(key);
1388                            that._resetPageNr();
1389                            that.submit();
1390                        }
1391                        if (type == "text") {
1392                            that.resetText(key);
1393                            that._resetPageNr();
1394                            that.submit();
1395                        }
1396                        if (type == "slider") {
1397                            that.resetSlider(key);
1398                            that._resetPageNr();
1399                            that.submit();
1400                        }
1401                        if (type == "color") {
1402                            that.resetColor(key);
1403                            that._resetPageNr();
1404                            that.submit();
1405                        }
1406
1407                        if (type == "custom") { $(document).trigger("wpcs_ajax_remove_filter", [key]); }
1408
1409                        filter.remove();
1410                    })
1411                    .keydown(function(e) {
1412                        if (e.key === "Enter" || e.key === " ") $(this).trigger('click');
1413                    });
1414
1415            });
1416        }
1417
1418
1419        /**
1420         * Vorschau beim Klick der Checkbox aktivieren
1421         */
1422        this.initCheckboxes = function() {
1423            $('.dxim_filter input[type=checkbox]').click(function() {
1424                that._preFilterAmount();
1425            });
1426        };
1427
1428        /**
1429         * Anzahl der Treffer Vorschau
1430         * @private
1431         */
1432        this._preFilterAmount = function() {
1433            if (that.isResetting) return;
1434
1435            that._resetPageNr();
1436
1437            if (that.ajax) {
1438                that.submitAjax();
1439            } else {
1440                $.ajax({
1441                    method: 'POST',
1442                    url: wpcs_ajax_url,
1443                    data: { action: 'getPreFilteredAmount', filterFormData: that.serialize(), init_ajax_data: 1 },
1444                    success: function(data) {
1445                        that._refreshCounterValues(data);
1446                    }
1447                });
1448            }
1449
1450        };
1451
1452        //
1453        this._refreshCounterValues = function(data) {
1454            let old_val = Math.round($('.wpcs_search_count').eq(0).attr('data-value'));
1455            $('.wpcs_search_count').each(function() {
1456                animateValue(this, old_val, data, 500);
1457            });
1458        }
1459
1460        /**
1461         * Submit Form
1462         */
1463        this.initSubmit = function() {
1464
1465            $('.dxim_filter_form_submit').click(function(e) {
1466                e.preventDefault();
1467
1468                let target = $('#wpcs_vehicle_archive');
1469                let redirect_url = $(this).attr('data-redirect');
1470                let url = '';
1471
1472                if (redirect_url != undefined && redirect_url != "") {
1473                    url = redirect_url;
1474                    that.ajax = false;
1475                } else {
1476                    // zum archiv umleiten
1477                    if (target.length < 1) url = wpcs_archive_url;
1478                    // aktuelle Seite reload
1479                    else url = wpcs_current_url;
1480                }
1481
1482                that.submit(url);
1483            });
1484
1485        };
1486
1487        /**
1488         *
1489         */
1490        this.submit = function(force_url) {
1491
1492            if (that.ajax) {
1493                that.submitAjax();
1494                return;
1495            }
1496
1497            let url = '';
1498            if (!force_url) {
1499                //let target = $('.wpcs_vehicle_list.wpcs_vehicles');
1500                let target = $('#wpcs_vehicle_archive');
1501                // zum archiv umleiten
1502                if (target.length < 1) url = wpcs_archive_url;
1503                // aktuelle Seite reload
1504                else url = wpcs_current_url;
1505            } else url = force_url;
1506
1507            let data = that.serialize();
1508            url += "?" + data;
1509            url += '#wpcs_vehicles';
1510            window.location.href = url;
1511        }
1512
1513        /**
1514         *
1515         * @private
1516         */
1517        this._resetPageNr = function() {
1518            // reset page id to 1
1519            $('#wpcs_hidden_wpcsp').val(1);
1520        }
1521
1522        /**
1523         *
1524         */
1525        this.submitAjax = function(append = 0) {
1526
1527            vehicle_list_properties.append = append;
1528
1529            that.setLoadingState();
1530
1531            $.ajax({
1532                method: 'POST',
1533                url: wpcs_ajax_url,
1534                data: { action: 'wpcs_fetchResults', filterFormData: that.serialize(), init_ajax_data: 1, vehicle_list_data: vehicle_list_properties },
1535                success: function(json) {
1536
1537                    let new_vehicles = $(json.vehicle_list_html);
1538
1539                    // vehicles
1540                    if (append > 0) {
1541                        $('#wpcs_vehicle_archive .wpcs_vehicles_wrapper').append(new_vehicles);
1542                    } else {
1543                        $('#wpcs_vehicle_archive').replaceWith(new_vehicles);
1544
1545                        // Seitenzeiger und max_pages explizit zurücksetzen — nicht auf Script-Ausführung
1546                        // im ersetzten HTML verlassen (in manchen jQuery-Versionen unzuverlässig)
1547                        $('#wpcs_hidden_wpcsp').val(1);
1548                        if (json.max_pages !== undefined) vehicle_list_properties.max_pages = json.max_pages;
1549
1550                        // pagination
1551                        $('.wpcs-pagination, .dxim_pagination').replaceWith(json.pagination);
1552                        that.initPagination();
1553
1554                        // active filters
1555                        $('.wpcs_filterform_active_filters').replaceWith(json.active_filters);
1556                        that.initActiveFilters();
1557
1558                        // refresh counters
1559                        // $('.vehicle_count').html(json.count);
1560                        that._refreshCounterValues(json.count);
1561                    }
1562
1563                    // disable loading state
1564                    that.hideLoadingState();
1565
1566                    //
1567                    wpcs_compare._init_compare_buttons(new_vehicles);
1568
1569                    //
1570                    wpcs_park._initToggleButtons(new_vehicles);
1571
1572                    $(document).trigger("wpcs_ajax_form_submitted");
1573                }
1574            });
1575
1576        }
1577
1578        this.setLoadingState = function() {
1579            this.is_loading = true;
1580            $('.wpcs_ajax_loader[data-category=sync]').addClass('active');
1581        }
1582
1583        this.hideLoadingState = function() {
1584            this.is_loading = false;
1585            $('.wpcs_ajax_loader[data-category=sync]').removeClass('active');
1586        }
1587
1588
1589        this.resetSelect = function(key) {
1590            let obj = $('.wpcsselect[data-field=' + key + ']');
1591            if (obj.length < 1) return;
1592            $('.wpcs_select_handle', obj).html(obj.data('nothing-selected'));
1593            $('.wpcs_select_value', obj).val('');
1594        }
1595
1596        this.resetCheckbox = function(key) {
1597            let input = $('input[name=' + key + ']');
1598            input.prop('checked', false);
1599        }
1600
1601
1602        this.resetText = function(key) {
1603            $('input[name=' + key + ']').val('');
1604        }
1605
1606        this.resetSlider = function(key) {
1607            // Modern-Pfad: .wpcs-range-wrapper per data-key / data-filter-1 / data-filter-2 finden.
1608            // Im Modern-Mode existiert noUiSlider nicht — Range/Slider werden über native
1609            // range-Inputs gerendert. form-elements.js hängt Listener an, die beim input/change
1610            // die hidden-Inputs + visuelle Anzeige nachziehen. Reset → auf Min/Max setzen + Events.
1611            let $modernWrapper = $(
1612                '.wpcs-range-wrapper[data-key="' + key + '"], ' +
1613                '.wpcs-range-wrapper[data-filter-1="' + key + '"], ' +
1614                '.wpcs-range-wrapper[data-filter-2="' + key + '"]'
1615            );
1616            if ($modernWrapper.length) {
1617                $modernWrapper.each(function() {
1618                    let wrapper = this;
1619                    let minInput = wrapper.querySelector('.wpcs-range-min');
1620                    let maxInput = wrapper.querySelector('.wpcs-range-max');
1621                    let numMin   = wrapper.querySelector('.wpcs-range-number-min');
1622                    let numMax   = wrapper.querySelector('.wpcs-range-number-max');
1623                    let rangeMin = parseInt(wrapper.getAttribute('data-min'));
1624                    let rangeMax = parseInt(wrapper.getAttribute('data-max'));
1625
1626                    if (minInput && maxInput) {
1627                        minInput.value = minInput.min;
1628                        maxInput.value = maxInput.max;
1629                    } else if (minInput) {
1630                        let wKey = wrapper.getAttribute('data-key') || '';
1631                        minInput.value = wKey.indexOf('_max') !== -1 ? minInput.max : minInput.min;
1632                    }
1633                    if (numMin) numMin.value = rangeMin;
1634                    if (numMax) numMax.value = rangeMax;
1635
1636                    // Events triggern, damit form-elements.js onInput (visuelles Update)
1637                    // + onChange (Hidden-Inputs + preFilterAmount) ausführt.
1638                    ['input', 'change'].forEach(function(evt) {
1639                        if (minInput) minInput.dispatchEvent(new Event(evt, {bubbles: true}));
1640                        if (maxInput) maxInput.dispatchEvent(new Event(evt, {bubbles: true}));
1641                    });
1642                });
1643                return;
1644            }
1645
1646            // Legacy-Pfad (noUiSlider)
1647            if (typeof noUiSlider === 'undefined') return;
1648            let obj = $('#wpcs_fs_' + key);
1649
1650            // RANGE slider
1651            if (obj.length < 1) {
1652                let key2 = key.replace('_max', '').replace('_min', '');
1653                let obj2 = $('#wpcs_fs_' + key2);
1654                if (obj2.length < 1) return;
1655
1656                const slider = obj2[0].noUiSlider;
1657                if (!slider) return;
1658
1659                if (window.wpcs_slider_ranges && window.wpcs_slider_ranges[key2]) {
1660                    const { min, max } = window.wpcs_slider_ranges[key2];
1661                    slider.set([min, max]);
1662                }
1663
1664                return;
1665            }
1666
1667            // SINGLE slider (min or max)
1668            const slider = obj[0].noUiSlider;
1669            if (!slider) return;
1670            const range = window.wpcs_slider_ranges[key];
1671            if (!range) return;
1672
1673            let value = key.endsWith('_min') ? range.min : range.max;
1674            slider.set(value);
1675        };
1676
1677
1678
1679
1680        this.resetColor = function(key) {
1681            let obj = $('.dxim_filter[data-filter-name=' + key + '] .wpcs_color_select');
1682            $('.color_value', obj).val('');
1683            $('.color.active', obj).removeClass('active');
1684
1685        }
1686
1687        this.resetAllFilters = function() {
1688            that.isResetting = true;
1689
1690            $('.dxim_filter[data-filter-name]').each(function() {
1691                let wrapper = $(this);
1692                // .attr() statt .data() – liest direkt aus dem HTML-Attribut, kein jQuery-Cache
1693                let key  = wrapper.attr('data-filter-name');
1694                let type = wrapper.attr('data-filter-type');
1695
1696                if (type === 'select') {
1697                    that.resetSelect(key);
1698                } else if (type === 'checkbox') {
1699                    that.resetCheckbox(key);
1700                } else if (type === 'multiple_checkbox') {
1701                    that.resetMultipleCheckbox(key);
1702                } else if (type === 'text') {
1703                    that.resetText(key);
1704                } else if (type === 'color') {
1705                    // direkt über wrapper – kein zweiter Selector-Lookup
1706                    wrapper.find('.color_value').val('');
1707                    wrapper.find('.color.active').removeClass('active');
1708                } else if (type === 'slider') {
1709                    that.resetSlider(key);
1710                } else if (type === 'range') {
1711                    let rangeEl = wrapper.find('.dxim_filter_range');
1712                    let filter1 = rangeEl.attr('data-filter-1');
1713                    let filter2 = rangeEl.attr('data-filter-2');
1714                    // direkt setzen – nicht auf noUiSlider update-Event warten
1715                    if (filter1) $('#wpcs_fv_' + filter1).val('#');
1716                    if (filter2) $('#wpcs_fv_' + filter2).val('#');
1717                    // visuellen Reset trotzdem durchführen
1718                    that.resetSlider(key + '_min');
1719                }
1720            });
1721
1722            that.isResetting = false;
1723            that._resetPageNr();
1724            that.submit();
1725        };
1726
1727        // ----------------------------------------------------------
1728        // setXxx — programmatic filter value setters (mirror of resetXxx)
1729        // Used by WPCSFilterSet AJAX mode to apply a filterset without page reload.
1730        // Call with isResetting = true around noUiSlider set() calls to suppress
1731        // intermediate _preFilterAmount triggers.
1732        // ----------------------------------------------------------
1733
1734        // filterFormData: optional serialized filter string passed from applyFilterset so that
1735        // depends-fields (e.g. class=Car for category) are available server-side for label lookup.
1736        this.setSelect = function(key, value, filterFormData) {
1737            let obj = $('.dxim_filter[data-filter-name=' + key + '] .wpcsselect');
1738            if (obj.length < 1) return;
1739
1740            if (!value) { that.resetSelect(key); return; }
1741
1742            // Set hidden value immediately so serialize() picks it up
1743            $('.wpcs_select_value', obj).val(value);
1744            $('.wpcs_select_delete', obj).show();
1745
1746            // Show raw value immediately as fallback while fetching label
1747            $('.wpcs_select_handle', obj).text(value);
1748
1749            // Fetch human-readable label(s). load:'all' uses _getAll() callbacks which resolve
1750            // depends (e.g. class→category). filterFormData lets the server find the depends value.
1751            $.ajax({
1752                method: 'POST',
1753                url: wpcs_ajax_url,
1754                dataType: 'json',
1755                data: { action: 'getData', field: key, load: 'all', filterFormData: filterFormData || '' }
1756            }).done(function(data) {
1757                let selectedValues = value.split(',');
1758                let labels         = [];
1759
1760                for (let index in data) {
1761                    if (selectedValues.indexOf(String(data[index]['id'])) !== -1) {
1762                        labels.push(data[index]['text']);
1763                    }
1764                }
1765
1766                // Only update if the value hasn't changed in the meantime
1767                if ($('.wpcs_select_value', obj).val() !== value) return;
1768
1769                let handleText;
1770                if      (labels.length === 0) handleText = value; // fallback: raw value
1771                else if (labels.length === 1) handleText = labels[0];
1772                else                          handleText = labels.length + ' ausgewählt';
1773
1774                $('.wpcs_select_handle', obj).html(handleText);
1775            });
1776        };
1777
1778        this.setCheckbox = function(key, value) {
1779            $('input[name=' + key + ']', '.dxim_filter').prop('checked', value === '1' || value === 'on');
1780        };
1781
1782        this.setText = function(key, value) {
1783            $('input[name=' + key + ']', '.dxim_filter').val(value);
1784        };
1785
1786        this.setMultipleCheckbox = function(key, value) {
1787            let wrapper = $('.dxim_filter[data-filter-name=' + key + ']');
1788            let vals    = value ? value.split(',') : [];
1789            wrapper.find('.wpcs_element_value').val(value);
1790            wrapper.find('.wpcs_selectable').each(function() {
1791                $(this).toggleClass('selected', vals.indexOf(String($(this).data('value'))) !== -1);
1792            });
1793        };
1794
1795        this.setColor = function(key, value) {
1796            let obj  = $('.dxim_filter[data-filter-name=' + key + '] .wpcs_color_select');
1797            let vals = value ? value.split(',') : [];
1798            $('.color_value', obj).val(value);
1799            $('.color', obj).each(function() {
1800                $(this).toggleClass('active', vals.indexOf(String($(this).data('key'))) !== -1);
1801            });
1802        };
1803
1804        // Single slider: value is a number string
1805        this.setSlider = function(key, value) {
1806            let el = document.getElementById('wpcs_fs_' + key);
1807            if (!el || !el.noUiSlider) return;
1808            el.noUiSlider.set(parseFloat(value));
1809        };
1810
1811        // Range slider: key is wrapper's data-filter-name; pass null to keep current handle position
1812        this.setRange = function(key, minValue, maxValue) {
1813            let el = document.getElementById('wpcs_fs_' + key);
1814            if (!el || !el.noUiSlider) return;
1815            let cur    = el.noUiSlider.get();
1816            let newMin = (minValue !== null && minValue !== undefined) ? parseFloat(minValue) : parseFloat(cur[0]);
1817            let newMax = (maxValue !== null && maxValue !== undefined) ? parseFloat(maxValue) : parseFloat(cur[1]);
1818            el.noUiSlider.set([newMin, newMax]);
1819        };
1820
1821        // Apply a full set of filterset params { key: value } to the filter form.
1822        // Resets all filters first, then sets each param, then submits via AJAX.
1823        this.applyFilterset = function(params) {
1824            // 1. Reset all filters silently
1825            that.isResetting = true;
1826            $('.dxim_filter[data-filter-name]').each(function() {
1827                let key  = $(this).attr('data-filter-name');
1828                let type = $(this).attr('data-filter-type');
1829                if      (type === 'select')            that.resetSelect(key);
1830                else if (type === 'checkbox')          that.resetCheckbox(key);
1831                else if (type === 'multiple_checkbox') that.resetMultipleCheckbox(key);
1832                else if (type === 'text')              that.resetText(key);
1833                else if (type === 'color')             that.resetColor(key);
1834                else if (type === 'slider')            that.resetSlider(key);
1835                else if (type === 'range')             that.resetSlider(key + '_min');
1836            });
1837            that._resetPageNr();
1838
1839            // 2. Range sliders: params use filter-1/filter-2 keys (e.g. price_min, price_max),
1840            //    not the wrapper's data-filter-name — match them to the right slider.
1841            let rangeHandled = {};
1842            $('.dxim_filter[data-filter-type="range"]').each(function() {
1843                let key     = $(this).attr('data-filter-name');
1844                let rangeEl = $(this).find('.dxim_filter_range');
1845                let f1      = rangeEl.attr('data-filter-1');
1846                let f2      = rangeEl.attr('data-filter-2');
1847                if (params[f1] === undefined && params[f2] === undefined) return;
1848                that.setRange(
1849                    key,
1850                    params[f1] !== undefined ? params[f1] : null,
1851                    params[f2] !== undefined ? params[f2] : null
1852                );
1853                if (f1) rangeHandled[f1] = true;
1854                if (f2) rangeHandled[f2] = true;
1855            });
1856
1857            // 3. All other filter types
1858            // Build a filterFormData string from all params so that depends-fields
1859            // (e.g. class=Car) are available when fetching labels for dependent selects (e.g. category).
1860            let paramsFormData = Object.keys(params).map(function(k) {
1861                return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
1862            }).join('&');
1863
1864            $.each(params, function(key, value) {
1865                if (rangeHandled[key]) return;
1866                let w    = $('.dxim_filter[data-filter-name="' + key + '"]');
1867                if (w.length === 0) return;
1868                let type = w.attr('data-filter-type');
1869                if      (type === 'select')            that.setSelect(key, value, paramsFormData);
1870                else if (type === 'checkbox')          that.setCheckbox(key, value);
1871                else if (type === 'text')              that.setText(key, value);
1872                else if (type === 'multiple_checkbox') that.setMultipleCheckbox(key, value);
1873                else if (type === 'color')             that.setColor(key, value);
1874                else if (type === 'slider')            that.setSlider(key, value);
1875            });
1876
1877            that.isResetting = false;
1878
1879            // 4. Fetch results
1880            that.submitAjax();
1881        };
1882
1883        this.__construct();
1884    }
1885
1886
1887
1888    /**
1889     * -----------------------------------------------------------
1890     * Kontaktform
1891     * @constructor
1892     */
1893    function WPCSContactForm() {
1894
1895        var that = this;
1896        this.form = null;
1897        this.spinner = null;
1898
1899        /**
1900         *
1901         * @private
1902         */
1903        this.__construct = function() {
1904
1905            $('.wpcs_contact_form').each(function() {
1906                let form = this;
1907                that._initSubmit(form);
1908                that._initConditionalFields(form);
1909                that._initTimeSelects(form);
1910                that._initDateValidation(form);
1911            });
1912
1913        };
1914
1915
1916        /**
1917         *
1918         * @param form
1919         * @private
1920         */
1921        this._setForm = function(form) {
1922            this.form = $(form);
1923            this.spinner = $('.wpcs_form_submit .wpcs_icon_spin', form);
1924        }
1925
1926        /**
1927         *
1928         * @param form
1929         * @private
1930         */
1931        this._initSubmit = function(form) {
1932            $('.wpcs_form_submit', form).click(function(e) {
1933                e.preventDefault();
1934                that._setForm(form);
1935
1936                if ($(this).hasClass('ga')) {
1937                    grecaptcha.execute(wpcs_ga_public_key, { action: "submit" }).then(function(token) {
1938                        that.sendForm(token);
1939                    });
1940                } else {
1941                    that.sendForm();
1942                }
1943            });
1944        }
1945
1946        this.sendForm = function(token) {
1947            this.spinner.css('display', 'inline-block');
1948            let redirect = $(this.form).data('redirect');
1949
1950            // Disable inputs in hidden conditional fields/groups before serializing
1951            var $hiddenInputs = $('.wpcs_conditional_field.wpcs_hidden input, .wpcs_conditional_field.wpcs_hidden select, .wpcs_conditional_field.wpcs_hidden textarea', this.form);
1952            $hiddenInputs.prop('disabled', true);
1953
1954            var formData = that.form.serialize();
1955
1956            // Re-enable inputs after serializing
1957            $hiddenInputs.prop('disabled', false);
1958
1959            $.ajax({
1960                method: 'POST',
1961                url: wpcs_ajax_url,
1962                data: { action: 'wpcs_contactform', post: formData, token: token },
1963                success: function(data) {
1964                    that.spinner.hide();
1965                    that.unhighlightFormFields();
1966                    if (data.status === -1) that.highlightFormFields(data);
1967                    else {
1968                        that.form.parent().html(data.msg);
1969                        if (redirect != "") window.location.href = redirect;
1970                    }
1971                }
1972            });
1973        };
1974
1975        /**
1976         *
1977         */
1978        this.unhighlightFormFields = function() {
1979            $('label', this.form).removeClass('required');
1980            $('.wpcs-error-msg', this.form).hide();
1981        };
1982
1983        /**
1984         *
1985         * @param data
1986         */
1987        this.highlightFormFields = function(data) {
1988            $('.wpcs-error-msg', this.form).fadeIn();
1989            $(data.error).each(function() {
1990                $('label[rel=' + this + ']', this.form).addClass('required');
1991            });
1992        };
1993
1994        this._initConditionalFields = function(form) {
1995            var $form = $(form);
1996
1997            // Legacy: handle old wpcs_conditional_group elements
1998            var groups = $('.wpcs_conditional_group', $form);
1999
2000            // New: handle wpcs_conditional_field elements with multiple conditions
2001            var conditionalFields = $('.wpcs_conditional_field', $form);
2002
2003            if (groups.length < 1 && conditionalFields.length < 1) return;
2004
2005            var checkConditions = function() {
2006                // Legacy group handling
2007                groups.each(function() {
2008                    var $group = $(this);
2009                    var condField = $group.data('condition-field');
2010                    var condValue = $group.data('condition-value');
2011                    var checked = $('input[name="' + condField + '[]"][value="' + condValue + '"]', $form).is(':checked');
2012                    if (checked) $group.removeClass('wpcs_hidden');
2013                    else $group.addClass('wpcs_hidden');
2014                });
2015
2016                // New: handle fields with multiple conditions (OR logic)
2017                conditionalFields.each(function() {
2018                    var $field = $(this);
2019                    var conditions = $field.data('conditions');
2020                    if (!conditions || !Array.isArray(conditions)) return;
2021
2022                    var isVisible = false;
2023
2024                    // Check if ANY condition is met (OR logic)
2025                    for (var i = 0; i < conditions.length; i++) {
2026                        var cond = conditions[i];
2027                        var triggerField = cond.field;
2028                        var triggerValue = cond.value;
2029
2030                        // Check checkbox inputs
2031                        var checkboxChecked = $('input[name="' + triggerField + '[]"][value="' + triggerValue + '"]', $form).is(':checked');
2032
2033                        // Check select inputs (standard select)
2034                        var selectValue = $('select[name="' + triggerField + '"]', $form).val();
2035                        var selectMatches = selectValue === triggerValue;
2036
2037                        // Check hidden inputs (used by wpcsselect custom selects)
2038                        var hiddenValue = $('input[type="hidden"][name="' + triggerField + '"]', $form).val();
2039                        var hiddenMatches = hiddenValue === triggerValue;
2040
2041                        if (checkboxChecked || selectMatches || hiddenMatches) {
2042                            isVisible = true;
2043                            break;
2044                        }
2045                    }
2046
2047                    if (isVisible) {
2048                        $field.removeClass('wpcs_hidden');
2049                    } else {
2050                        $field.addClass('wpcs_hidden');
2051                    }
2052                });
2053            };
2054
2055            $form.on('change', 'input[type="checkbox"], input[type="hidden"], select', checkConditions);
2056            checkConditions();
2057        };
2058
2059        this._initTimeSelects = function(form) {
2060            var $form = $(form);
2061
2062            // Find all time picker components
2063            $('.wpcs_time_picker', $form).each(function() {
2064                var $picker = $(this);
2065                var $input = $('.wpcs_time_input', $picker);
2066                var $hiddenField = $('.wpcs_time_value', $picker);
2067                var $dropdown = $('.wpcs_time_dropdown', $picker);
2068                var $hoursList = $('.wpcs_time_hours', $picker);
2069                var $minutesList = $('.wpcs_time_minutes', $picker);
2070
2071                var selectedHour = null;
2072                var selectedMinute = null;
2073
2074                // Open dropdown when clicking on input
2075                $input.on('click', function(e) {
2076                    e.preventDefault();
2077                    e.stopPropagation();
2078                    // Close all other time pickers
2079                    $('.wpcs_time_picker').not($picker).removeClass('wpcs_open');
2080                    $picker.toggleClass('wpcs_open');
2081                });
2082
2083                // Prevent dropdown from closing when clicking inside it
2084                $dropdown.on('click', function(e) {
2085                    e.stopPropagation();
2086                });
2087
2088                // Hour selection
2089                $('.wpcs_time_option', $hoursList).on('click', function(e) {
2090                    e.stopPropagation();
2091                    selectedHour = $(this).data('value');
2092                    $('.wpcs_time_option', $hoursList).removeClass('wpcs_active');
2093                    $(this).addClass('wpcs_active');
2094                    updateDisplay();
2095                });
2096
2097                // Minute selection
2098                $('.wpcs_time_option', $minutesList).on('click', function(e) {
2099                    e.stopPropagation();
2100                    selectedMinute = $(this).data('value');
2101                    $('.wpcs_time_option', $minutesList).removeClass('wpcs_active');
2102                    $(this).addClass('wpcs_active');
2103                    updateDisplay();
2104                });
2105
2106                // Update display and hidden field
2107                var updateDisplay = function() {
2108                    var displayHour = selectedHour !== null ? selectedHour : '--';
2109                    var displayMinute = selectedMinute !== null ? selectedMinute : '--';
2110                    $input.val(displayHour + ':' + displayMinute);
2111
2112                    // Only set hidden field value if both are selected
2113                    if (selectedHour !== null && selectedMinute !== null) {
2114                        $hiddenField.val(selectedHour + ':' + selectedMinute);
2115                    } else {
2116                        $hiddenField.val('');
2117                    }
2118                };
2119            });
2120
2121            // Close all time pickers when clicking outside (only register once)
2122            $(document).off('click.timepicker').on('click.timepicker', function(e) {
2123                if (!$(e.target).closest('.wpcs_time_picker').length) {
2124                    $('.wpcs_time_picker').removeClass('wpcs_open');
2125                }
2126            });
2127        };
2128
2129        /**
2130         * iOS Safari doesn't enforce min/max on date inputs
2131         * This adds JavaScript validation as a fallback
2132         */
2133        this._initDateValidation = function(form) {
2134            var $form = $(form);
2135
2136            $('input[type="date"]', $form).on('change', function() {
2137                var $input = $(this);
2138                var value = $input.val();
2139                var min = $input.attr('min');
2140                var max = $input.attr('max');
2141
2142                if (!value) return;
2143
2144                // Validate min
2145                if (min && value < min) {
2146                    $input.val(min);
2147                }
2148
2149                // Validate max
2150                if (max && value > max) {
2151                    $input.val(max);
2152                }
2153            });
2154        };
2155
2156        this.__construct();
2157    }
2158
2159    function add_body_classes() {
2160        if (typeof wpcs_screen_width_mobile == "undefined" || typeof wpcs_screen_width_tablet == "undefined") return;
2161
2162        let w = $(this).width();
2163        if (w <= wpcs_screen_width_mobile) $('body').addClass('wpcs_mobile').removeClass('wpcs_tablet').removeClass('wpcs_desktop');
2164        else if (w <= wpcs_screen_width_tablet) $('body').removeClass('wpcs_mobile').addClass('wpcs_tablet').removeClass('wpcs_desktop');
2165        else $('body').removeClass('wpcs_mobile').removeClass('wpcs_tablet').addClass('wpcs_desktop');
2166    }
2167
2168
2169    $(window).resize(function() {
2170        add_body_classes();
2171    });
2172
2173    function WPCSScroller(scroller) {
2174        var that = this;
2175        this.scroller = scroller;
2176
2177        this.__construct = function() {
2178            $(this.scroller).on('scroll', that.updateClasses);
2179            $(window).on('resize', that.updateClasses);
2180
2181            this.updateClasses();
2182        }
2183
2184        this.updateClasses = function() {
2185
2186            if (that.scroller.scrollLeft > 0) {
2187                that.scroller.classList.add('can_scroll_left');
2188            } else {
2189                that.scroller.classList.remove('can_scroll_left');
2190            }
2191
2192            if (that.scroller.scrollLeft < that.scroller.scrollWidth - that.scroller.clientWidth) {
2193                that.scroller.classList.add('can_scroll_right');
2194            } else {
2195                that.scroller.classList.remove('can_scroll_right');
2196            }
2197        }
2198
2199        this.__construct();
2200    }
2201
2202
2203    function WPCSSticky(sticky) {
2204
2205        var that = this;
2206        this.sticky = $(sticky);
2207        this.width;
2208
2209        this.offsetStart;
2210        this.offsetStop;
2211        this.initialTop;
2212
2213        this.stickyStartObj;
2214        this.stickyStopObj;
2215
2216        this.__construct = function() {
2217
2218            this.stickyStartObj = $(this.sticky.data('sticky-start'));
2219            this.stickyStopObj = $(this.sticky.data('sticky-stop'));
2220            this.offsetStart = parseInt(this.sticky.data('sticky-start-offset')) || 0;
2221            this.offsetStop = parseInt(this.sticky.data('sticky-stop-offset')) || 0;
2222
2223            $(window).on('scroll', function() { that.update(); });
2224            $(window).on('resize', function() { that.update(); });
2225
2226            this.update();
2227        }
2228
2229        this.update = function() {
2230
2231            if ($(window).width() < 1024) {
2232                that.setSticky(false);
2233                return;
2234            }
2235
2236            let stickyStart = this.stickyStartObj.length ? this.stickyStartObj.position().top + this.stickyStartObj.outerHeight() : 0;
2237
2238            if (!this.sticky.hasClass('isSticky')) this.initialTop = this.sticky.offset().top;
2239            let position = this.initialTop;
2240            var scrollTop = $(window).scrollTop();
2241            let isSticky = false;
2242            let top = 0;
2243
2244            // calc start
2245            if ((scrollTop + stickyStart >= position)) {
2246                top = stickyStart + this.offsetStart;
2247                isSticky = true;
2248            }
2249
2250            // calc end
2251            if (isSticky && this.stickyStopObj.length > 0) {
2252                let check = this.sticky.outerHeight() + top + this.offsetStop;
2253                if ((this.stickyStopObj.offset().top - scrollTop <= check)) {
2254                    isSticky = false;
2255                }
2256            }
2257
2258            // set sticky
2259            if (isSticky) this.setSticky(true, top);
2260            else this.setSticky(false)
2261
2262        }
2263
2264        this.setSticky = function(sticky, top = 0) {
2265            if (sticky) {
2266                this.sticky.css({
2267                    position: 'fixed',
2268                    top: top + 'px',
2269                    width: that.calcWidth() + 'px',
2270                }).addClass('isSticky');
2271            } else {
2272                this.sticky.css({
2273                    position: 'initial',
2274                    top: 'initial',
2275                    width: 'auto',
2276                }).removeClass('isSticky');
2277            }
2278        }
2279
2280        this.calcWidth = function() {
2281            this.sticky.css({
2282                position: 'initial',
2283                width: 'auto'
2284            });
2285            return this.sticky.width();
2286        }
2287
2288        this.__construct();
2289    }
2290
2291    // -------------------------------------------------------
2292    // WPCSFilterSet — marks active button; AJAX mode applies filters without page reload
2293    // -------------------------------------------------------
2294    function WPCSFilterSet() {
2295
2296        $(document).on('click', '.wpcs-filterset-btn', function (e) {
2297            var $btn    = $(this);
2298            var isActive = $btn.hasClass('wpcs-filterset-active');
2299
2300            if (!(wpcs_form && wpcs_form.ajax)) {
2301                // no AJAX: normal href navigation, just toggle active class
2302                $('.wpcs-filterset-btn').removeClass('wpcs-filterset-active');
2303                if (!isActive) $btn.addClass('wpcs-filterset-active');
2304                return;
2305            }
2306
2307            e.preventDefault();
2308
2309            // Toggle: clicking an already-active filterset resets all filters
2310            if (isActive) {
2311                $('.wpcs-filterset-btn').removeClass('wpcs-filterset-active');
2312                wpcs_form.resetAllFilters();
2313                return;
2314            }
2315
2316            $('.wpcs-filterset-btn').removeClass('wpcs-filterset-active');
2317            $btn.addClass('wpcs-filterset-active');
2318
2319            // Parse query params from href into a plain object
2320            var href   = $btn.attr('href') || '';
2321            var qIdx   = href.indexOf('?');
2322            var params = {};
2323            if (qIdx !== -1) {
2324                href.substring(qIdx + 1).split('&').forEach(function (part) {
2325                    var eq = part.indexOf('=');
2326                    if (eq > -1) {
2327                        // + als %20 behandeln bevor decodeURIComponent — http_build_query kodiert Spaces als +
2328                        var decode = function(s) { return decodeURIComponent(s.replace(/\+/g, '%20')); };
2329                        params[decode(part.substring(0, eq))] = decode(part.substring(eq + 1));
2330                    }
2331                });
2332            }
2333
2334            wpcs_form.applyFilterset(params);
2335        });
2336    }
2337
2338
2339})(jQuery);
2340
2341// -----------------------------------------------------------------------
2342// Contact form file upload
2343// -----------------------------------------------------------------------
2344(function($) {
2345
2346    function wpcsUploadFile(file, fieldKey, $wrapper) {
2347        var formData = new FormData();
2348        formData.append('action', 'wpcs_contactform_file_upload');
2349        formData.append('nonce', wpcs_contactform_upload_nonce);
2350        formData.append('file', file);
2351
2352        var $list = $wrapper.find('.wpcs-upload-list');
2353        var $item = $('<li class="wpcs-upload-item">' + $('<span>').text(file.name).html() + ' <span class="wpcs-upload-status">...</span></li>');
2354        $list.append($item);
2355
2356        $.ajax({
2357            url: wpcs_ajax_url,
2358            method: 'POST',
2359            data: formData,
2360            processData: false,
2361            contentType: false,
2362            success: function(resp) {
2363                if (resp.success) {
2364                    $item.find('.wpcs-upload-status').text('✓');
2365                    $wrapper.append('<input type="hidden" name="wpcs_uploads[' + fieldKey + '][]" value="' + resp.data.filename + '">');
2366                    var $remove = $('<span class="wpcs-upload-remove" style="cursor:pointer;margin-left:8px;color:#c00;" title="Entfernen">✕</span>');
2367                    $remove.on('click', function() {
2368                        $.post(wpcs_ajax_url, { action: 'wpcs_contactform_file_delete', nonce: wpcs_contactform_upload_nonce, filename: resp.data.filename });
2369                        $wrapper.find('input[value="' + resp.data.filename + '"]').remove();
2370                        $item.remove();
2371                    });
2372                    $item.append($remove);
2373                } else {
2374                    $item.find('.wpcs-upload-status').text('Fehler: ' + (resp.data && resp.data.msg ? resp.data.msg : 'Upload fehlgeschlagen'));
2375                }
2376            },
2377            error: function() {
2378                $item.find('.wpcs-upload-status').text('Upload fehlgeschlagen.');
2379            }
2380        });
2381    }
2382
2383    $(document).on('change', '.wpcs-file-input', function() {
2384        var $input = $(this);
2385        var $wrapper = $input.closest('.wpcs-file-upload-wrapper');
2386        var fieldKey = $wrapper.data('field-key');
2387        var maxFiles = parseInt($wrapper.data('max-files')) || 5;
2388        var files = this.files;
2389
2390        for (var i = 0; i < files.length; i++) {
2391            if ($wrapper.find('input[type="hidden"]').length >= maxFiles) {
2392                alert('Maximale Anzahl Dateien erreicht (' + maxFiles + ').');
2393                break;
2394            }
2395            wpcsUploadFile(files[i], fieldKey, $wrapper);
2396        }
2397
2398        $input.val('');
2399    });
2400
2401})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.