PageSourceSearch

https://www.wineo.de/bundles/wineotheme/js/jquery.listing-actions.js?1790073358

js wineo.de collected 2026-10-07 08:11:03 UTC 55,944 bytes, 1,539 lines download raw bytes

1;(function ($, window, StateManager, undefined) {
2    'use strict';
3
4    var $body = $('body');
5
6    /**
7     * Plugin for handling the filter functionality and
8     * all other actions for changing the product listing.
9     * It handles the current set of category parameters and applies
10     * them to the current top location url when something was
11     * changed by the user over the filter form, action forms or
12     * the action links.
13     *
14     * ** Filter Form **
15     * The filter form exists of different filter components,
16     * the filter submit button and the labels for active filters.
17     * Each component is rendered in a single panel and has its own functionality.
18     * All single components are handled by the "filterComponent" plugin.
19     * The plugin for the components fires correct change events for each type
20     * of component, so the "listingActions" plugin can listen on the changes
21     * of the user. A filter form has to be a normal form with the selector,
22     * which is set in the plugin options, so the form can be found by the plugin.
23     * The actual submitting of the form will always be prevented to build the complex
24     * category parameters out of the serialized form data.
25     *
26     * Example:
27     * <form id="filter" method="get" data-filter-form="true">
28     *
29     *
30     * ** Action Forms **
31     * You can apply different category parameters over additional action forms.
32     * In most cases these forms are auto submitting forms using the "autoSubmit" plugin,
33     * which change just one parameter via a combo- or checkbox. So with these
34     * action forms you have the possibility to apply all kind of category parameters
35     * like sorting, layout type, number of products per page etc.
36     *
37     * Example:
38     * <form method="get" data-action-form="true">
39     *  <select name="{$shortParameters.sSort}" data-auto-submit="true">
40     *      {...}
41     *  </select>
42     * </form>
43     *
44     *
45     * ** Action Links **
46     * You can also apply different category parameter via direct links.
47     * Just use the corresponding get parameters in the href attribute of the link.
48     * The new parameter will be added to the existing category parameters.
49     * If the parameter already exists the value will be updated with the new one.
50     *
51     * Example:
52     * <a href="?p=1&l=list" data-action-link="true">list view</a>
53     *
54     */
55    $.plugin('swListingActions', {
56
57        defaults: {
58
59            /**
60             * The selector for the filter panel form.
61             */
62            filterFormSelector: '*[data-filter-form="true"]',
63
64            /**
65             * The selector for the single filter components.
66             */
67            filterComponentSelector: '*[data-filter-type]',
68
69            /**
70             * The selector for the button which shows and hides the filter panel.
71             */
72            filterTriggerSelector: '*[data-filter-trigger="true"]',
73
74            /**
75             * The selector for the icon inside the filter trigger button.
76             */
77            filterTriggerIconSelector: '.action--collapse-icon',
78
79            /**
80             * The selector for the filter panel element.
81             */
82            filterContainerSelector: '.action--filter-options',
83
84            /**
85             * The selector for the inner filter container which used to for the loading indicator
86             * if the off canvas menu is active
87             */
88            filterInnerContainerSelector: '.filter--container',
89
90            /**
91             * The selector for additional listing action forms.
92             */
93            actionFormSelector: '*[data-action-form="true"]',
94
95            /**
96             * The selector for additional listing action links.
97             */
98            actionLinkSelector: '*[data-action-link="true"]',
99
100            /**
101             * The selector for the container where the active filters are shown.
102             */
103            activeFilterContSelector: '.filter--active-container',
104
105            /**
106             * The selector for the button which applies the filter changes.
107             */
108            applyFilterBtnSelector: '.filter--btn-apply',
109
110            /**
111             * The css class for active filter labels.
112             */
113            activeFilterCls: 'filter--active',
114
115            /**
116             * The close icon element which is used for the active filter labels.
117             */
118            activeFilterIconCls: 'filter--active-icon',
119
120            /**
121             * The css class for the filter panel when it is completely collapsed.
122             */
123            collapsedCls: 'is--collapsed',
124
125            /**
126             * The css class for the filter container when it shows only the preview of the active filters.
127             */
128            hasActiveFilterCls: 'is--active-filter',
129
130            /**
131             * The css class for active states.
132             */
133            activeCls: 'is--active',
134
135            /**
136             * The css class for disabled states.
137             */
138            disabledCls: 'is--disabled',
139
140            /**
141             * Selector for the element that contains the found product count.
142             */
143            filterCountSelector: '.filter--count',
144
145            /**
146             * Class that will be added to the apply filter button
147             * when loading the results.
148             */
149            loadingClass: 'is--loading',
150
151            /**
152             * The characters used as a prefix to identify property field names.
153             * The properties will be merged in one GET parameter.
154             * For example properties with field names beginning with __f__"ID"
155             * will be merged to &f=ID1|ID2|ID3|ID4 etc.
156             *
157             */
158            propertyPrefixChar: '__',
159
160            /**
161             * The buffer time in ms to wait between each action before firing the ajax call.
162             */
163            bufferTime: 850,
164
165            /**
166             * The time in ms for animations.
167             */
168            animationSpeed: 400,
169
170            /** Css class which will be added when the user uses instant filter results */
171            instantFilterActiveCls: 'is--instant-filter-active',
172
173            /**
174             * class to select the listing div
175             */
176            listingSelector: '.listing--container > .listing',
177
178            /**
179             * class to select the pagination bars
180             */
181            paginationSelector: '.listing--paging.panel--paging',
182
183            /**
184             * data attribute which indicates whether infinite scrolling is used or not
185             */
186            infiniteScrollingAttribute: 'data-infinite-scrolling',
187
188            /**
189             * selector for the page size select box
190             */
191            paginationBarPerPageSelector: '.per-page--field.action--field',
192
193            /**
194             * selector for the hidden input field of the filter form which stores the current page
195             */
196            pageInputSelector: 'input[name=p]',
197
198            /**
199             * selector for the hidden input field of the filter form which stores the current sorting
200             */
201            sortInputSelector: 'input[name=o]',
202
203            /**
204             * selector for the hidden input field of the filter form which stores the current amount of products per page
205             */
206            perPageInputSelector: 'input[name=n]',
207
208            /**
209             * selector for the sorting select box
210             */
211            sortActionFormSelector: '.action--sort',
212
213            /**
214             * selector for the products per page select box
215             */
216            perPageActionFormSelector: '.action--per-page',
217
218            /**
219             * selector for the wrapper of the whole listing
220             */
221            listingWrapperSelector: '.listing--wrapper',
222
223            /**
224             * The selector for the element which get the loading indicator after customer activates a filter
225             */
226            loadingIndSelector: '.listing--wrapper',
227
228            /**
229             * The selector for "no filter result found" container
230             */
231            noResultContainerSelector: '.listing-no-filter-result .alert',
232
233            /**
234             * Class for loading indicator, added and removed on the configurable `listingSelector` element
235             */
236            isLoadingCls: 'is--loading',
237
238            /**
239             * Configuration for the loading indicator
240             */
241            loadingIndConfig: {
242                theme: 'light',
243                animationSpeed: 100,
244                closeOnClick: false
245            },
246
247            /**
248             * selector for the filter close button, which is only visible in off canvas
249             */
250            filterCloseBtnSelector: '.filter--close-btn',
251
252            /**
253             * icon for the filter close button
254             */
255            closeFilterOffCanvasBtnIcon: '<i class="icon--arrow-right"></i>',
256
257            /**
258             * selector for the search page headline
259             */
260            searchHeadlineProductCountSelector: '.search--headline .headline--product-count',
261
262            /**
263             * selector for the filter facet container
264             */
265            filterFacetContainerSelector: '.filter--facet-container',
266
267            /**
268             * selector for the filter action button bottom
269             */
270            filterActionButtonBottomSelector: '.filter--actions.filter--actions-bottom',
271
272            /**
273             * selector for the parent of the loading indicator in if the filters in sidebar mode
274             */
275            sidebarLoadingIndicatorParentSelector: '.content-main--inner',
276
277            /**
278             * selector for the jquery.add-article plugin to enable support for the off canvas cart
279             */
280            addArticleSelector: '*[data-add-article="true"]',
281
282            /**
283             * Threshold for the scroll position when the user switches pages (in both modes e.g. infinite scrolling & page change)
284             */
285            listingScrollThreshold: -10
286        },
287
288        /**
289         * Initializes the plugin.
290         */
291        init: function () {
292            var me = this,
293                filterCount;
294
295            me.applyDataAttributes();
296
297            $('.sidebar-filter--loader').appendTo('.sidebar-filter--content');
298            me.$filterForm = $(me.opts.filterFormSelector);
299            me.$filterComponents = me.$filterForm.find(me.opts.filterComponentSelector);
300            me.$filterTrigger = me.$el.find(me.opts.filterTriggerSelector);
301            me.$filterTriggerIcon = me.$filterTrigger.find(me.opts.filterTriggerIconSelector);
302            me.$filterCont = $(me.opts.filterContainerSelector);
303            me.$actionForms = $(me.opts.actionFormSelector);
304            me.$actionLinks = $(me.opts.actionLinkSelector);
305            me.$activeFilterCont = me.$filterForm.find(me.opts.activeFilterContSelector);
306            me.$applyFilterBtn = me.$filterForm.find(me.opts.applyFilterBtnSelector);
307            me.$listing = $(me.opts.listingSelector);
308            me.$pageInput = $(me.$filterForm.find(me.opts.pageInputSelector));
309            me.$sortInput = $(me.$filterForm.find(me.opts.sortInputSelector));
310            me.$perPageInput = $(me.$filterForm.find(me.opts.perPageInputSelector));
311            me.$listingWrapper = me.$el.parent(me.opts.listingWrapperSelector);
312            me.$closeFilterOffCanvasBtn = $(me.opts.filterCloseBtnSelector);
313            me.$filterFacetContainer = me.$filterForm.find(me.opts.filterFacetContainerSelector);
314            me.$filterActionButtonBottom = me.$filterForm.find(me.opts.filterActionButtonBottomSelector);
315            me.$sidebarModeLoadionIndicator = $(me.opts.sidebarLoadingIndicatorParentSelector);
316            me.$noFilterResultContainer = $(me.opts.noResultContainerSelector);
317
318            me.searchHeadlineProductCount = $(me.opts.searchHeadlineProductCountSelector);
319            me.listingUrl = me.$filterForm.attr('data-listing-url');
320            me.loadFacets = me.$filterForm.attr('data-load-facets') === 'true';
321            me.showInstantFilterResult = me.$filterForm.attr('data-instant-filter-result') === 'true';
322            me.isInfiniteScrolling = me.$listing.attr(me.opts.infiniteScrollingAttribute);
323            me.isFilterpanelInSidebar = me.$filterForm.attr('data-is-in-sidebar') === 'true';
324
325            me.controllerURL = window.location.href.split('?')[0];
326            me.resetLabel = me.$activeFilterCont.attr('data-reset-label');
327            me.propertyFieldNames = [];
328            me.activeFilterElements = {};
329            me.categoryParams = {};
330            me.urlParams = '';
331            me.bufferTimeout = 0;
332            me.closeFilterOffCanvasBtnText = me.$closeFilterOffCanvasBtn.html();
333            me.closeFilterOffCanvasBtnTextWithProducts = me.$closeFilterOffCanvasBtn.attr('data-show-products-text');
334
335            me.getPropertyFieldNames();
336            me.setCategoryParamsFromTopLocation();
337            me.createActiveFiltersFromCategoryParams();
338            me.createUrlParams();
339
340            filterCount = Object.keys(me.activeFilterElements).length;
341
342            me.updateFilterTriggerButton(filterCount > 1 ? filterCount - 1 : filterCount);
343            me.initStateHandling();
344            me.registerEvents();
345
346            me.$loadingIndicatorElement = $(me.opts.loadingIndSelector);
347            me.$offCanvasLoadingIndicator = $(me.opts.filterInnerContainerSelector);
348
349            $.subscribe('action/fetchListing', $.proxy(me.onSendListingRequest, me));
350
351            me.disableActiveFilterContainer(true);
352
353            var isFiltered = me.$filterForm.attr('data-is-filtered');
354            if (isFiltered > 0 && me.loadFacets) {
355                me.getFilterResult(me.urlParams, true, false);
356            }
357        },
358
359        /**
360         * Initializes the state manager for specific device options.
361         */
362        initStateHandling: function () {
363            var me = this,
364                enterFn = $.proxy(me.onEnterMobile, me),
365                exitFn = $.proxy(me.onExitMobile, me);
366
367            StateManager.registerListener([
368                {
369                    state: 'xs',
370                    enter: enterFn,
371                    exit: exitFn
372                },
373                {
374                    state: 's',
375                    enter: enterFn,
376                    exit: exitFn
377                }
378            ]);
379
380            $.publish('plugin/swListingActions/onInitStateHandling', [me]);
381        },
382
383        /**
384         * Called when entering the xs or s viewport.
385         * Removes/Clears style attributes that were set in higher viewports.
386         */
387        onEnterMobile: function () {
388            var me = this,
389                opts = me.opts;
390
391            me.$filterForm.removeAttr('style');
392            me.$filterFacetContainer.removeAttr('style');
393            me.$filterActionButtonBottom.removeAttr('style');
394
395            me.disableActiveFilterContainer(false);
396
397            me.$filterCont.removeClass(opts.collapsedCls);
398
399            me.$filterTrigger.removeClass(opts.activeCls);
400
401            $.publish('plugin/swListingActions/onEnterMobile', [me]);
402        },
403
404        /**
405         * @param {boolean} disabled
406         */
407        disableActiveFilterContainer: function (disabled) {
408            if (this.showInstantFilterResult || this.isFilterpanelInSidebar) {
409                return;
410            }
411
412            if (disabled) {
413                this.$activeFilterCont.addClass(this.opts.disabledCls);
414            } else if (this.$activeFilterCont.hasClass(this.opts.disabledCls)) {
415                this.$activeFilterCont.removeClass(this.opts.disabledCls);
416            }
417        },
418
419        /**
420         * Called when exiting the xs or s viewport.
421         * Add the disabled class to the active filter container
422         * when it has active filter elements.
423         */
424        onExitMobile: function () {
425            if (StateManager.isCurrentState(['xs', 's'])) {
426                return;
427            }
428
429            if (Object.keys(this.activeFilterElements).length && !this.isFilterpanelInSidebar) {
430                this.disableActiveFilterContainer(true);
431            }
432
433            $.publish('plugin/swListingActions/onExitMobile', [this]);
434        },
435
436        /**
437         * Registers all necessary events.
438         */
439        registerEvents: function () {
440            this._on(this.$filterForm, 'submit', $.proxy(this.onFilterSubmit, this));
441            this._on(this.$actionForms, 'submit', $.proxy(this.onActionSubmit, this));
442            this._on(this.$actionLinks, 'click', $.proxy(this.onActionLink, this));
443            this._on(this.$filterComponents, 'onChange', $.proxy(this.onComponentChange, this));
444            this._on(this.$filterTrigger, 'click', $.proxy(this.onFilterTriggerClick, this));
445
446            this._on($body, 'click', $.proxy(this.onBodyClick, this));
447
448            this.$activeFilterCont.on(this.getEventName('click'), '.' + this.opts.activeFilterCls, $.proxy(this.onActiveFilterClick, this));
449            this.$listingWrapper.on(this.getEventName('submit'), this.opts.actionFormSelector, $.proxy(this.onActionSubmit, this));
450            this.$listingWrapper.on(this.getEventName('click'), this.opts.actionLinkSelector, $.proxy(this.onActionLink, this));
451
452            $.publish('plugin/swListingActions/onRegisterEvents', [this]);
453        },
454
455        /**
456         * Called by event listener on submitting the filter form.
457         * Gets the serialized form data and applies it to the category params.
458         *
459         * @param {Event} event
460         */
461        onFilterSubmit: function (event) {
462            event.preventDefault();
463
464            var formData = this.$filterForm.serializeArray(),
465                categoryParams = this.setCategoryParamsFromData(formData, false);
466
467            this.applyCategoryParams(categoryParams);
468
469            $.publish('plugin/swListingActions/onFilterSubmit', [this, event]);
470        },
471
472        /**
473         * Called by event listener on submitting an action form.
474         * Gets the serialized form data and applies it to the category params.
475         *
476         * Depending on which action is submitted the hidden input fields
477         * of the form filter are set to the new value if instantFilterResult is active.
478         *
479         * @param {Event} event
480         */
481        onActionSubmit: function (event) {
482            event.preventDefault();
483
484            var $form = $(event.currentTarget),
485                formData = $form.serializeArray(),
486                categoryParams = this.setCategoryParamsFromData(formData, true);
487
488            if (this.showInstantFilterResult) {
489                // first array element is always page number
490                this.setPageInput(this.getFormValue(formData, 'p'));
491
492                // second array element is always whether sorting or products per pages
493                if (this.isSortAction($form)) {
494                    this.setSortInput(this.getFormValue(formData, 'o'));
495                } else if (this.isPerPageAction($form)) {
496                    this.setPerPageInput(this.getFormValue(formData, 'n'));
497                }
498            }
499            this.applyCategoryParams(categoryParams);
500
501            $.publish('plugin/swListingActions/onActionSubmit', [this, event]);
502        },
503
504        /**
505         * @param {Object} data
506         * @param {string} key
507         * @returns {string}
508         */
509        getFormValue: function (data, key) {
510            var value = '';
511            $.each(data, function (index, item) {
512                if (item.name === key) {
513                    value = item.value;
514                }
515            });
516            return value;
517        },
518
519        /**
520         * @param {Object} $form
521         * @return {boolean}
522         */
523        isSortAction: function ($form) {
524            return $form.is(this.opts.sortActionFormSelector);
525        },
526
527        /**
528         *
529         * @param {Object} $form
530         * @return {boolean}
531         */
532        isPerPageAction: function ($form) {
533            return $form.is(this.opts.perPageActionFormSelector);
534        },
535
536        /**
537         * Helper method to set the hidden input field for the current page of the filter form
538         *
539         * @param {int} value
540         */
541        setPageInput: function (value) {
542            this.$pageInput.val(value);
543        },
544
545        /**
546         * Helper method to set the hidden input field for the current sorting of the filter form
547         *
548         * @param {int} value
549         */
550        setSortInput: function (value) {
551            this.$sortInput.val(value);
552        },
553
554        /**
555         * Helper method to set the hidden input field for products per page of the filter form
556         *
557         * @param {int} value
558         */
559        setPerPageInput: function (value) {
560            this.$perPageInput.val(value);
561        },
562
563        /**
564         * Called by event listener on clicking on an action link.
565         * Reads the parameter in the href attribute and adds it to the
566         * category params.
567         *
568         * @param {Event} event
569         */
570        onActionLink: function (event) {
571            event.preventDefault();
572
573            var me = this,
574                $link = $(event.currentTarget),
575                linkParams = $link.attr('href').split('?')[1],
576                linkParamsArray = linkParams.split('&'),
577                paramValue;
578
579            if (me.showInstantFilterResult) {
580                // Update page number in web form
581                $.each(linkParamsArray, function(index, param) {
582                    paramValue = param.split('=');
583
584                    if (paramValue[0] === 'p') {
585                        me.setPageInput(paramValue[1]);
586                    }
587                });
588            }
589
590            this.applyCategoryParams(
591                this.setCategoryParamsFromUrlParams(linkParams)
592            );
593
594            $.publish('plugin/swListingActions/onActionLink', [this, event]);
595        },
596
597        /**
598         * Called by event listener on clicking the filter trigger button.
599         * Opens and closes the filter form panel.
600         *
601         * @param {Event} event
602         */
603        onFilterTriggerClick: function (event) {
604            event.preventDefault();
605
606            if (StateManager.isCurrentState(['xs', 's'])) {
607                return;
608            }
609
610            if (this.$filterCont.hasClass(this.opts.collapsedCls)) {
611                this.closeFilterPanel();
612            } else {
613                this.openFilterPanel();
614            }
615
616            $.publish('plugin/swListingActions/onFilterTriggerClick', [this, event]);
617        },
618
619        /**
620         * Closes all filter panels if the user clicks anywhere else.
621         *
622         * @param {Event} event
623         */
624        onBodyClick: function (event) {
625            var $target = $(event.target);
626
627            if (!$target.is(this.opts.filterComponentSelector + ', ' + this.opts.filterComponentSelector + ' *')) {
628                $.each(this.$filterComponents, function (index, item) {
629                    $(item).data('plugin_swFilterComponent').close();
630                });
631            }
632
633            $.publish('plugin/swListingActions/onBodyClick', [this, event]);
634        },
635
636        /**
637         * Called by event listener on the change event of the
638         * single filter components. Applies the changes of the
639         * component values to the category params.
640         *
641         * @param {Event} event
642         */
643        onComponentChange: function (event) {
644            var urlParams,
645                formData,
646                categoryParams;
647
648            if (this.showInstantFilterResult) {
649                this.setPageInput(1);
650            }
651
652            formData = this.$filterForm.serializeArray();
653            categoryParams = this.setCategoryParamsFromData(formData);
654
655            urlParams = this.createUrlParams(categoryParams);
656
657            this.createActiveFiltersFromCategoryParams(categoryParams);
658
659            this.enableButtonLoading();
660            this.buffer($.proxy(this.getFilterResult, this, urlParams, this.loadFacets, this.showInstantFilterResult), this.opts.bufferTime);
661
662            $.publish('plugin/swListingActions/onComponentChange', [this, event]);
663        },
664
665        /**
666         * Called by event listener on clicking an active filter label.
667         * It removes the clicked filter param form the set of active filters
668         * and updates the specific component.
669         *
670         * @param {Event} event
671         */
672        onActiveFilterClick: function (event) {
673            var me = this,
674                $activeFilter = $(event.currentTarget),
675                param = $activeFilter.attr('data-filter-param'),
676                isMobile = StateManager.isCurrentState(['xs', 's']);
677
678            if (param === 'reset') {
679                // Reset all facets
680                $.each(me.activeFilterElements, function (key) {
681                    me.removeActiveFilter(key);
682                    me.resetFilterProperty(key);
683                });
684
685                // Reset all options inside the facets
686                $.each(me.$filterComponents, function (i, component) {
687                    var $component = $(component),
688                        componentPlugin = $component.data('plugin_swFilterComponent');
689
690                    $.each(componentPlugin.$inputs, function(i, item) {
691                        componentPlugin.disable($(item), false);
692                        componentPlugin.disableComponent(false);
693                    });
694
695                    $component
696                        .removeClass(me.opts.disabledCls)
697                        .find('.' + me.opts.disabledCls)
698                        .removeClass(me.opts.disabledCls);
699                });
700
701                if (!isMobile && !me.$filterCont.hasClass(me.opts.collapsedCls)) {
702                    me.applyCategoryParams();
703                }
704            } else if (!me.$activeFilterCont.hasClass(me.opts.disabledCls)) {
705                me.removeActiveFilter(param);
706                me.resetFilterProperty(param);
707            }
708
709            $.publish('plugin/swListingActions/onActiveFilterClick', [me, event]);
710        },
711
712        /**
713         * @returns {Array}
714         */
715        getPropertyFieldNames: function () {
716            var me = this;
717
718            $.each(me.$filterComponents, function (index, item) {
719                var $comp = $(item),
720                    types = ['value-list', 'value-list-single', 'value-tree', 'media', 'value-tree-single', 'date'],
721                    type = $comp.attr('data-filter-type'),
722                    fieldName = $comp.attr('data-field-name');
723
724                if (types.indexOf(type) >= 0 && me.propertyFieldNames.indexOf(fieldName) === -1) {
725                    me.propertyFieldNames.push(fieldName);
726                }
727            });
728
729            $.publish('plugin/swListingActions/onGetPropertyFieldNames', [me, me.propertyFieldNames]);
730
731            return me.propertyFieldNames;
732        },
733
734        /**
735         * Converts given form data to the category parameter object.
736         * You can choose to either extend or override the existing object.
737         *
738         * @param {Object} formData
739         * @param {boolean} extend
740         * @returns {Object}
741         */
742        setCategoryParamsFromData: function (formData, extend) {
743            var tempParams = {};
744
745            $.each(formData, function (index, item) {
746                if (item['value']) {
747                    tempParams[item['name']] = item['value'];
748                }
749            });
750
751            if (extend) {
752                return $.extend(this.categoryParams, tempParams);
753            }
754
755            this.categoryParams = tempParams;
756
757            $.publish('plugin/swListingActions/onSetCategoryParamsFromData', [this, tempParams]);
758
759            return tempParams;
760        },
761
762        /**
763         * Converts top location parameters to the category parameter object.
764         *
765         * @returns {Object}
766         */
767        setCategoryParamsFromTopLocation: function () {
768            var urlParams = window.location.search.substr(1),
769                categoryParams = this.setCategoryParamsFromUrlParams(urlParams);
770
771            $.publish('plugin/swListingActions/onSetCategoryParamsFromData', [this, categoryParams]);
772
773            return categoryParams;
774        },
775
776        /**
777         * Converts url parameters to the category parameter object.
778         *
779         * @param urlParamString
780         * @returns {Object}
781         */
782        setCategoryParamsFromUrlParams: function (urlParamString) {
783            var me = this,
784                categoryParams,
785                params;
786
787            if (urlParamString.length <= 0) {
788                categoryParams = {};
789
790                $.publish('plugin/swListingActions/onSetCategoryParamsFromUrlParams', [me, categoryParams]);
791
792                return categoryParams;
793            }
794
795            categoryParams = me.categoryParams;
796            params = urlParamString.split('&');
797
798            $.each(params, function (index, item) {
799                var param = item.split('=');
800
801                param = $.map(param, function (val) {
802                    val = val.replace(/\+/g, '%20');
803                    return decodeURIComponent(val);
804                });
805
806                if (param[1] === 'reset') {
807                    delete categoryParams[param[0]];
808                } else if (me.propertyFieldNames.indexOf(param[0]) !== -1) {
809                    var properties = param[1].split('|');
810
811                    $.each(properties, function (index, property) {
812                        categoryParams[me.opts.propertyPrefixChar + param[0] + me.opts.propertyPrefixChar + property] = property;
813                    });
814                } else {
815                    categoryParams[param[0]] = param[1];
816                }
817            });
818
819            $.publish('plugin/swListingActions/onSetCategoryParamsFromUrlParams', [me, categoryParams]);
820
821            return categoryParams;
822        },
823
824        /**
825         * Converts the category parameter object to url parameters
826         * and applies the url parameters to the current top location.
827         *
828         * @param {Object} categoryParams
829         */
830        applyCategoryParams: function (categoryParams) {
831            var params = categoryParams || this.categoryParams,
832                urlParams = this.createUrlParams(params);
833
834            this.applyUrlParams(urlParams);
835
836            $.publish('plugin/swListingActions/onApplyCategoryParams', [this, categoryParams]);
837        },
838
839        /**
840         * Converts the category parameter object to url parameters.
841         *
842         * @param {Object} categoryParams
843         * @returns {string}
844         */
845        createUrlParams: function (categoryParams) {
846            var catParams = categoryParams || this.categoryParams,
847                params = this.cleanParams(catParams),
848                filterList = [];
849
850            $.each(params, function (key, value) {
851                filterList.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
852            });
853
854            this.urlParams = '?' + filterList.join('&');
855
856            $.publish('plugin/swListingActions/onCreateUrlParams', [this, this.urlParams]);
857
858            return this.urlParams;
859        },
860
861        /**
862         * @param {Object} params
863         * @returns {Object}
864         */
865        cleanParams: function (params) {
866            var me = this,
867                propertyParams = {};
868
869            $.each(params, function (key, value) {
870                if (key.substr(0, 2) === me.opts.propertyPrefixChar) {
871                    var propertyKey = key.split(me.opts.propertyPrefixChar)[1];
872
873                    if (propertyParams[propertyKey] !== undefined) {
874                        propertyParams[propertyKey] += '|' + value;
875                    } else {
876                        propertyParams[propertyKey] = value;
877                    }
878                } else {
879                    propertyParams[key] = value;
880                }
881            });
882
883            return propertyParams;
884        },
885
886        /**
887         * Applies given url params to the top location.
888         *
889         * @param {string}
889 urlParams
890         */
891        applyUrlParams: function (urlParams) {
892            var params = urlParams || this.urlParams,
893                formData,
894                categoryParams,
895                paramsForFilterResult;
896
897            if (this.showInstantFilterResult) {
898                formData = this.$filterForm.serializeArray();
899
900                categoryParams = this.setCategoryParamsFromData(formData);
901
902                paramsForFilterResult = this.createUrlParams(categoryParams);
903
904                this.enableButtonLoading();
905                this.buffer($.proxy(this.getFilterResult, this, paramsForFilterResult, false, this.showInstantFilterResult), this.opts.bufferTime);
906            } else {
907                window.location.href = this.getListingUrl(params, false);
908            }
909
910            $.publish('plugin/swListingActions/onApplyUrlParams', [this, urlParams]);
911        },
912
913        /**
914         * Returns the full url path to the listing
915         * including all current url params.
916         *
917         * @param {string} urlParams
918         * @param {boolean} encode
919         * @returns {string}
920         */
921        getListingUrl: function (urlParams, encode) {
922            var params = urlParams || this.urlParams,
923                url;
924
925            if (encode) {
926                url = encodeURI(this.controllerURL + params);
927            } else {
928                url = this.controllerURL + params;
929            }
930
931            $.publish('plugin/swListingActions/onGetListingUrl', [this, url, urlParams, encode]);
932
933            return url;
934        },
935
936        /**
937         * Buffers a function by the given buffer time.
938         *
939         * @param {function} func
940         * @param {int} bufferTime
941         */
942        buffer: function (func, bufferTime) {
943            if (this.bufferTimeout) {
944                clearTimeout(this.bufferTimeout);
945            }
946
947            this.bufferTimeout = setTimeout(func, bufferTime);
948
949            $.publish('plugin/swListingActions/onBuffer', [this, this.bufferTimeout, func, bufferTime]);
950        },
951
952        /**
953         * Resets the current buffer timeout.
954         */
955        resetBuffer: function () {
956            this.bufferTimeout = 0;
957
958            $.publish('plugin/swListingActions/onResetBuffer', [this, this.bufferTimeout]);
959        },
960
961        /**
962         * Event listener which allows to send listing ajax request to load facets, total count and/or listings
963         *
964         * @param {object} event
965         * @param {object} params
966         * @param {boolean} loadFacets
967         * @param {boolean} loadProducts
968         * @param {function} callback
969         */
970        onSendListingRequest: function (event, params, loadFacets, loadProducts, callback) {
971            var formData = this.$filterForm.serializeArray();
972
973            $.each(formData, function (index, item) {
974                if (!params.hasOwnProperty(item.name)) {
975                    if (!item.value || (typeof item.value === 'string' && item.value.length <= 0)) {
976                        return;
977                    }
978
979                    params[item.name] = item.value;
980                }
981            });
982
983            this.sendListingRequest(params, loadFacets, loadProducts, callback, true);
984        },
985
986        /**
987         * @param {string|object} params
988         * @param {boolean} loadFacets
989         * @param {boolean} loadProducts
990         * @param {function} callback
991         * @param {boolean} appendDefaults
992         */
993        sendListingRequest: function (params, loadFacets, loadProducts, callback, appendDefaults) {
994            if (typeof params === 'object') {
995                params = '?' + $.param(params);
996            }
997
998            this.resetBuffer();
999
1000            $.ajax({
1001                type: 'get',
1002                url: this.buildListingUrl(params, loadFacets, loadProducts),
1003                success: $.proxy(callback, this)
1004            });
1005            $.publish('plugin/swListingActions/onGetFilterResult', [this, params]);
1006        },
1007
1008        /**
1009         * Gets the counted result of found products
1010         * with the current applied category parameters.
1011         * Updates the filter submit button on success.
1012         *
1013         * @param {string}
1013 urlParams
1014         * @param {boolean} loadFacets
1015         * @param {boolean} loadProducts
1016         */
1017        getFilterResult: function (urlParams, loadFacets, loadProducts) {
1018            var me = this,
1019                params = urlParams || me.urlParams,
1020                loadingIndicator = me.$loadingIndicatorElement;
1021
1022            if (me.$filterCont.is('.off-canvas.is--open')) {
1023                loadingIndicator = me.$offCanvasLoadingIndicator;
1024            } else if (me.isFilterpanelInSidebar) {
1025                loadingIndicator = me.$sidebarModeLoadionIndicator;
1026            }
1027
1028            me.resetBuffer();
1029            me.enableLoading(loadingIndicator, loadProducts, function () {
1030                // send ajax request to load products and facets
1031                me.sendListingRequest(params, loadFacets, loadProducts, function (response) {
1032                    me.disableLoading(loadingIndicator, loadProducts, response, function () {
1033                        me.updateListing(response);
1034                        // publish finish event to update filter panels
1035                        $.publish('plugin/swListingActions/onGetFilterResultFinished', [me, response, params]);
1036                    });
1037                });
1038            });
1039        },
1040
1041        /**
1042         * Enables the loading animation in the listing
1043         *
1044         * @param {Object} loadingIndicator
1045         * @param {boolean} loadProducts
1046         * @param {function} callback
1047         */
1048        enableLoading: function (loadingIndicator, loadProducts, callback) {
1049            callback = $.isFunction(callback) ? callback : $.noop;
1050
1051            if (loadProducts) {
1052                this.$listing.addClass(this.opts.isLoadingCls);
1053
1054                loadingIndicator.setLoading(
1055                    true,
1056                    this.opts.loadingIndConfig
1057                ).then(
1058                    $.proxy(callback, this)
1059                );
1060            } else {
1061                this.enableButtonLoading();
1062                callback.call(this);
1063            }
1064        },
1065
1066        /**
1067         * Enables the button reload animation
1068         */
1069        enableButtonLoading: function () {
1070            if (!this.showInstantFilterResult) {
1071                this.$applyFilterBtn.addClass(this.opts.loadingClass);
1072            }
1073        },
1074
1075        /**
1076         * Disables the loading animation for the listing
1077         *
1078         * @param {Object} loadingIndicator
1079         * @param {boolean} loadProducts
1080         * @param {Object} response
1081         * @param {function} callback
1082         */
1083        disableLoading: function (loadingIndicator, loadProducts, response, callback) {
1084            callback = $.isFunction(callback) ? callback : $.noop;
1085
1086            if (loadProducts) {
1087                // disable loading indicator
1088                loadingIndicator.setLoading(false).then(
1089                    $.proxy(callback, this)
1090                );
1091            } else {
1092                this.$applyFilterBtn.removeClass(this.opts.loadingClass);
1093                this.updateFilterButton(response.totalCount);
1094                callback.call(this);
1095            }
1096        },
1097
1098        /**
1099         * Builds the URL by taking the basic path and adding parameters to it.
1100         *
1101         * @param {string} formParams
1102         * @param {boolean} loadProducts
1103         * @param {boolean} loadFacets
1104         * @returns {string}
1105         */
1106        buildListingUrl: function (formParams, loadFacets, loadProducts) {
1107            var url = this.listingUrl + formParams;
1108
1109            if (loadProducts) {
1110                url += '&loadProducts=1';
1111            }
1112            if (loadFacets) {
1113                url += '&loadFacets=1';
1114            }
1115
1116            return url;
1117        },
1118
1119        /**
1120         * Updates the listing with new products
1121         *
1122         * @param {Object} response
1123         */
1124        updateListing: function (response) {
1125            var html,
1126                pages;
1127
1128            if (!response.hasOwnProperty('listing')) {
1129                this.$listing.removeClass(this.opts.isLoadingCls);
1130                return;
1131            }
1132
1133            this.updateFilterCloseButton(response.totalCount);
1134            this.updateSearchHeadline(response.totalCount);
1135            this.updateNoResultContainer(response.totalCount);
1136
1137            html = response.listing.trim();
1138
1139            this.$listing.html(html);
1140            this.$listing.removeClass(this.opts.isLoadingCls);
1141
1142            window.history.pushState('data', '', window.location.href.split('?')[0] + this.urlParam
1142s);
1143
1144            $.publish('plugin/swListingActions/updateListing', [this, html]);
1145
1146            StateManager.updatePlugin(this.opts.addArticleSelector, 'swAddArticle');
1147
1148            if (this.isInfiniteScrolling) {
1149                pages = Math.ceil(response.totalCount / this.$perPageInput.val());
1150
1151                // update infinite scrolling plugin and data attributes for infinite scrolling
1152                this.$listing.attr('data-pages', pages);
1153                this.$listing.data('plugin_swInfiniteScrolling').destroy();
1154                StateManager.addPlugin(this.opts.listingSelector, 'swInfiniteScrolling');
1155                $.publish('plugin/swListingActions/updateInfiniteScrolling', [this, html, pages]);
1156            } else {
1157                this.updatePagination(response);
1158                this.scrollToTopPagination();
1159            }
1160        },
1161
1162        /**
1163         * Scrolls to the top paging bar
1164         */
1165        scrollToTopPagination: function () {
1166            var $htmlBodyCt = $('html, body'),
1167                listingScrollThreshold = this.opts.listingScrollThreshold,
1168                listingActionPos = this.$el.offset().top + listingScrollThreshold,
1169                scrollTop = $htmlBodyCt.scrollTop();
1170
1171            // Browser compatibility
1172            if (scrollTop === 0) {
1173                scrollTop = $('body').scrollTop();
1174            }
1175
1176            if (scrollTop > listingActionPos) {
1177                $htmlBodyCt.animate({
1178                    scrollTop: listingActionPos
1179                }, this.opts.animationSpeed);
1180            }
1181        },
1182
1183        /**
1184         * Updates the off canvas filter close button with the amount of products
1185         *
1186         * @param {int} totalCount
1187         */
1188        updateFilterCloseButton: function (totalCount) {
1189            var filterCount = Object.keys(this.activeFilterElements).length;
1190
1191            if (filterCount > 0) {
1192                this.$closeFilterOffCanvasBtn.html(this.closeFilterOffCanvasBtnTextWithProducts.replace('%s', totalCount) + this.opts.closeFilterOffCanvasBtnIcon);
1193
1194                $.publish('plugin/swListingActions/updateFilterCloseBtnWithProductsCount', [this, totalCount]);
1195            } else {
1196                this.$closeFilterOffCanvasBtn.html(this.closeFilterOffCanvasBtnText);
1197
1198                $.publish('plugin/swListingActions/updateFilterCloseBtnDefault', [this]);
1199            }
1200
1201            this.updateFilterTriggerButton(filterCount > 1 ? filterCount - 1 : filterCount);
1202        },
1203
1204        /**
1205         * Updates the headline of the search page with the new total count
1206         *
1207         * @param {int} totalCount
1208         */
1209        updateSearchHeadline: function (totalCount) {
1210            if (this.searchHeadlineProductCount.length > 0) {
1211                this.searchHeadlineProductCount.html(totalCount);
1212            }
1213        },
1214
1215        /**
1216         * @param {int} totalCount
1217         */
1218        updateNoResultContainer: function (totalCount) {
1219            if (totalCount > 0) {
1220                if (!this.$noFilterResultContainer.hasClass('is--hidden')) {
1221                    this.$noFilterResultContainer.addClass('is--hidden');
1222                }
1223                return;
1224            }
1225            if (this.$noFilterResultContainer.hasClass('is--hidden')) {
1226                this.$noFilterResultContainer.removeClass('is--hidden');
1227            }
1228        },
1229
1230        /**
1231         * Updates the html for the listing pagination in case infinite scrolling is disabled
1232         *
1233         * @param {Object} response
1234         */
1235        updatePagination: function (response) {
1236            var html = response.pagination.trim();
1237
1238            $(this.opts.paginationSelector).replaceWith(html);
1239            StateManager.updatePlugin(this.opts.paginationBarPerPageSelector, 'swAutoSubmit');
1240
1241            $.publish('plugin/swListingActions/updatePagination', [this, html]);
1242        },
1243
1244        /**
1245         * Updates the layout of the filter submit button
1246         * with the new count of found products.
1247         *
1248         * @param {int} count
1249         */
1250        updateFilterButton: function (count) {
1251            this.$applyFilterBtn.find(this.opts.filterCountSelector).html(count);
1252
1253            if (count <= 0) {
1254                this.$applyFilterBtn.attr('disabled', 'disabled');
1255            } else {
1256                this.$applyFilterBtn.removeAttr('disabled');
1257            }
1258
1259            $.publish('plugin/swListingActions/onUpdateFilterButton', [this, c
1259ount]);
1260        },
1261
1262        /**
1263         * Updates the layout of the filter trigger button
1264         * on mobile viewports with the current count of active filters.
1265         *
1266         * @param activeFilterCount
1267         */
1268        updateFilterTriggerButton: function (activeFilterCount) {
1269            this.$filterTriggerIcon.html(activeFilterCount || '');
1270
1271            $.publish('plugin/swListingActions/onUpdateFilterTriggerButton', [this, activeFilterCount]);
1272        },
1273
1274        /**
1275         * Creates the labels for active filters from the category params.
1276         *
1277         * @param categoryParams
1278         */
1279        createActiveFiltersFromCategoryParams: function (categoryParams) {
1280            var me = this,
1281                count = 0,
1282                params = categoryParams || this.categoryParams;
1283
1284            $.each(this.activeFilterElements, function (key) {
1285                if (params[key] === undefined || params[key] === 0) {
1286                    me.removeActiveFilter(key);
1287                }
1288            });
1289
1290            $.each(params, function (key, value) {
1291                me.createActiveFilter(key, value);
1292            });
1293
1294            $.each(this.activeFilterElements, function () {
1295                count++;
1296            });
1297
1298            if (count > 1) {
1299                this.createActiveFilterElement('reset', this.resetLabel);
1300            }
1301
1302            this.$filterCont.toggleClass(this.opts.hasActiveFilterCls, (count > 0));
1303            if (this.showInstantFilterResult && count > 0) {
1304                this.$filterCont.addClass(this.opts.instantFilterActiveCls);
1305            }
1306
1307            if (!this.opts.isFilterpanelInSidebar) {
1308                this.$activeFilterCont.toggleClass(
1309                    this.opts.collapsedCls,
1310                    this.$filterCont.hasClass(this.opts.collapsedCls)
1311                );
1312            }
1313
1314            $.publish('plugin/swListingActions/onCreateActiveFiltersFromCategoryParams', [this, categoryParams]);
1315        },
1316
1317        /**
1318         * Creates an active filter label for the given parameter.
1319         * If the label for the given parameter already
1320         * exists it will be updated.
1321         *
1322         * @param {string} param
1323         * @param {string} value
1324         */
1325        createActiveFilter: function (param, value) {
1326            var label = this.createActiveFilterLabel(param, value);
1327
1328            if (label !== undefined && label.length) {
1329                if (this.activeFilterElements[param] !== undefined) {
1330                    this.updateActiveFilterElement(param, label);
1331                } else {
1332                    this.createActiveFilterElement(param, label);
1333                }
1334            }
1335
1336            $.publish('plugin/swListingActions/onCreateActiveFilter', [this, param, value]);
1337        },
1338
1339        /**
1340         * Creates the DOM element for an active filter label.
1341         *
1342         * @param {string} param
1343         * @param {string} label
1344         */
1345        createActiveFilterElement: function (param, label) {
1346            this.activeFilterElements[param] = $('<span>', {
1347                'class': this.opts.activeFilterCls,
1348                'html': this.getLabelIcon() + label,
1349                'data-filter-param': param
1350            }).appendTo(this.$activeFilterCont);
1351
1352            $.publish('plugin/swListingActions/onCreateActiveFilterElement', [this, param, label]);
1353        },
1354
1355        /**
1356         * Updates the layout of an existing filter label element.
1357         *
1358         * @param param
1359         * @param {string} label
1360         */
1361        updateActiveFilterElement: function (param, label) {
1362            this.activeFilterElements[param].html(this.getLabelIcon() + label);
1363
1364            $.publish('plugin/swListingActions/onUpdateActiveFilterElement', [this, param, label]);
1365        },
1366
1367        /**
1368         * Removes an active filter label from the set and from the DOM.
1369         *
1370         * @param param
1371         */
1372        removeActiveFilter: function (param) {
1373            this.activeFilterElements[param].remove();
1374
1375            delete this.activeFilterElements[param];
1376
1377            $.publish('plugin/swListingActions/onRemoveActiveFilter', [this, param]);
1378        },
1379
1380        /**
1381         * Resets a filter parameter and updates
1382         * the component based on the component type.
1383         *
1384         * @param {string} param
1385         */
1386        resetFilterProperty: function (param) {
1387            var $input,
1388                rangeSlider;
1389
1390            if (param === 'rating') {
1391                $input = this.$filterForm.find('.filter--rating .
1391is--active input[name="rating"]');
1392                $input.removeAttr('checked').trigger('change');
1393            } else {
1394                $input = this.$filterForm.find('[name="' + this.escapeDoubleQuotes(param) + '"]');
1395                if ($input.is('[data-range-input]')) {
1396                    rangeSlider = $input.parents('[data-range-slider="true"]').data('plugin_swRangeSlider');
1397                    rangeSlider.reset($input.attr('data-range-input'));
1398                } else if ($input.is('[data-datepicker="true"]') || $input.is('[data-date-range-input]')) {
1399                    $input.trigger('clear');
1400                } else {
1401                    $input.removeAttr('checked').trigger('change');
1402                }
1403            }
1404
1405            $.publish('plugin/swListingActions/onResetFilterProperty', [this, param]);
1406        },
1407
1408        /**
1409         * Creates the correct label content for an active
1410         * filter label based on the component type.
1411         *
1412         * @param {string} param
1413         * @param {string} value
1414         * @returns {string}
1415         */
1416        createActiveFilterLabel: function (param, value) {
1417            var $label,
1418                labelText = '',
1419                valueString = value + '';
1420
1421            if (param === 'rating' && value > 0) {
1422                labelText = this.createStarLabel(value);
1423            } else {
1424                $label = this.$filterForm.find('label[for="' + this.escapeDoubleQuotes(param) + '"]');
1425
1426                if ($label.is('[data-range-label]')) {
1427                    labelText = $label.prev('span').html() + $label.html();
1428                } else if ($label.is('[data-date-range-label]')) {
1429                    labelText = $label.html() + ' ' + $label.next('[data-date-range-input]').attr('data-display-value');
1430                } else if ($label.find('img').length) {
1431                    labelText = $label.find('img').attr('alt');
1432                } else if ($label.closest(this.opts.filterComponentSelector).is('[data-filter-type="radio"]')) {
1433                    var activeRadioId = $label.closest(this.opts.filterComponentSelector).find('input:checked').attr('id');
1434                    labelText = this.$filterForm.find('label[for="' + this.escapeDoubleQuotes(activeRadioId) + '"]').html();
1435                } else if (value > 0 || valueString.length > 0) {
1436                    labelText = $label.html();
1437                }
1438            }
1439
1440            $.publish('plugin/swListingActions/onCreateActiveFilterLabel', [this, labelText, param, value]);
1441
1442            return labelText;
1443        },
1444
1445        /**
1446         * Only escapes a " if it's not already escaped
1447         *
1448         * @param {string} str
1449         * @returns string
1450         */
1451        escapeDoubleQuotes: function (str) {
1452            return str.replace(/\\([\s\S])|(")/g, '\\$1$2');
1453        },
1454
1455        /**
1456         * Creates the label content for the special rating component.
1457         *
1458         * @param {int} stars
1459         * @returns {string}
1460         */
1461        createStarLabel: function (stars) {
1462            var label = '',
1463                i = 0;
1464
1465            for (i; i < 5; i++) {
1466                if (i < stars) {
1467                    label += '<i class="icon--star"></i>';
1468                } else {
1469                    label += '<i class="icon--star-empty"></i>';
1470                }
1471            }
1472
1473            $.publish('plugin/swListingActions/onCreateStarLabel', [this, label, stars]);
1474
1475            return label;
1476        },
1477
1478        /**
1479         * Returns the html string of the delete icon
1480         * for an active filter label.
1481         *
1482         * @returns {string}
1483         */
1484        getLabelIcon: function () {
1485            var icon = '<span class="' + this.opts.activeFilterIconCls + '"></span>';
1486
1487            $.publish('plugin/swListingActions/onCreateStarLabel', [this, icon]);
1488
1489            return icon;
1490        },
1491
1492        /**
1493         * Opens the filter form panel based on the current state.
1494         */
1495        openFilterPanel: function () {
1496            if (!this.$filterCont.hasClass(this.opts.hasActiveFilterCls)) {
1497                this.$activeFilterCont.slideDown(this.opts.animationSpeed);
1498            }
1499
1500            this.$filterFacetContainer.slideDown(this.opts.animationSpeed);
1501            this.$filterActionButtonBottom.slideDown(this.opts.animationSpeed);
1502
1503            this.disableActiveFilterContainer(false);
1504            this.$filterCont.addClass(this.opts.collapsedCls);
1505            this.$filterTrigger.addClass(this.opts.activeCls);
1506
1507            $.publish('plugin/swListingActions/onOpenFilterPanel', [this]);
1508        },
1509
1510        /**
1511         * Closes the filter form panel based on the current state.
1512         */
1513        closeFilterPanel: function () {
1514            if (!this.$filterCont.hasClass(this.opts.hasActiveFilterCls)) {
1515                this.$activeFilterCont.slideUp(this.opts.animationSpeed);
1516            }
1517
1518            this.$filterFacetContainer.slideUp(this.opts.animationSpeed);
1519            this.$filterActionButtonBottom.slideUp(this.opts.animationSpeed);
1520
1521            this.disableActiveFilterContainer(true);
1522            this.$filterCont.removeClass(this.opts.collapsedCls);
1523            this.$filterTrigger.removeClass(this.opts.activeCls);
1524
1525            $.publish('plugin/swListingActions/onCloseFilterPanel', [this]);
1526        },
1527
1528        /**
1529         * Destroys the plugin.
1530         */
1531        destroy: function () {
1532            this.$el.off(this.getEventName('click'), '.' + this.opts.activeFilterCls);
1533            this.$listingWrapper.off(this.getEventName('submit'), this.opts.actionFormSelector);
1534            this.$listingWrapper.off(this.getEventName('click'), this.opts.actionLinkSelector);
1535
1536            this._destroy();
1537        }
1538    });
1539})(jQuery, window, StateManager, undefined);

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.