Component: Filter Panel

Implementation Notes

Filter Configuration

  • Each filter panel must be assigned a target via the data attribute data-target (e.g., data-target="myTarget"), where the value of the data attribute corresponds to the ID of the target (e.g., <ul id="myTarget">).
  • Each filter in the filter panel must have a unique ID, which is set via the data attribute data-filter-id (e.g., data-filter-id="color"). The filter is then applied to elements that have a corresponding named data attribute in addition to the data attribute data-item (e.g., data-filter-color="Red").
  • If the data attribute data-filter-id of a filter contains multiple comma-separated names (e.g., data-filter-id="vehicle, color"), the found entries are displayed as groups in the dropdown.
  • The labels of the groups can be customized via corresponding data attributes (e.g., data-vehicle-label="Vehicles").
  • Options are automatically added to the filters according to the values found. Existing options remain (e.g., “Show all”), but are marked as non-selectable if there are no corresponding matches.
  • The options are sorted in the order that the items are found. To sort the items in a specific order, add a disabled option for each item in the desired order.
  • To display all elements unfiltered (“Show all”), the value of the predefined option must be empty.

Display of Search Results

To display the search results only after applying the filtering, the element containing the elements to be filtered can be hidden using the data attribute filterpanel-cloak.

If there is an element with the data attribute data-filter-placeholder in the target container, this element is displayed as soon as there are no search results.

Display of Number of Search Results

  • The number is output in the element with the class filterPanel-status.
  • The output of the number is defined via a template tag with the data attribute data-filter-messages, which is a child object of the filter panel.
  • The data attribute data-count is used to select the text variant matching the number.
    • The definition of plurals is based on i18next Plurals- i18next documentation.
    • Additionally, there can be a default variant that is displayed as long as no filters are selected.
  • If the status is used to display the number, no placeholder should be used.

Example:

<template data-filter-messages>
    <div data-count="default">All entries</div>
    <div data-count="zero">Nothing found</div>
    <div data-count="one"><strong>1</strong> entry found</div>
    <div data-count="other"><strong></strong> entries found</div>
</template>

Sorting

The sorting is determined by the value of the options in the select. The definition is based on CSS selectors and can describe the following types:

  • Selectors for classes and tags such as .myElement or li: The entries are sorted by the HTML content (innerHTML) of the child element selected by the selector.
  • Selectors for data attributes such as [data-date]: The entries are sorted by the values in the corresponding data attribute.
<select id="field-select">
    <option value=".myElement">By title</option>
    <option value="[data-date]">Oldest first</option>
</select>

The sorting order is ascending by default. It can be reversed with a prefixed DESC, e.g., DESC [data-date] or DESC .myElement. To better highlight the different sorting of options in the source code, the default ascending order can also be explicitly named with ASC:

<select id="field-select">
    <option value="ASC [data-date]">Oldest first</option>
    <option value="DESC [data-date]">Latest first</option>
</select>

Preferred elements in sorting

By adding the data attribute data-preferred, elements are preferred in sorting over other elements with the same value. This behavior can also be explicitly suppressed with data-preferred="false".

Elements can also be sorted by preference, for which the corresponding option must have the value [data-preferred]. Attention: The sorting order cannot be reversed when sorting by preference, as preferred elements always come first.

Displaying Filter, Status, and Sorting Separately

If the status and sorting should be displayed separately from the filters or repeated after the list to be filtered, multiple filter panels can be placed. It is important that all panels use the same ID in the data attribute data-target. In addition, both panels must each have the same template tag for defining the display for the number of search results.

<div class="doc-notes" style="margin-block: var(--sp-component);">

    <h2 class="doc-variant-title">Demos</h2>

    <h3>Preselect filters</h3>

    <p><strong>Please regard:</strong> Due to the dynamic adaptation of the URL, it is possible that the URL displayed in the address bar of the browser does not match the link called up.</p>

    <p>
        <select class="doc-select" onchange="javascript: window.location = this.value;">
            <option vtarget="_top" selected disabled>Please select</option>
            <hr>
            <option value="./filterpanel--demo-only" target="_top">Reset filters</option>
            <hr>
            <option value="./filterpanel--demo-only?fahrzeug=Autos">Fahrzeug: Auto</option>
            <option value="./filterpanel--demo-only?fahrzeug=Segways">Fahrzeug: Segways (kein Ergebnis)</option>
            <option value="./filterpanel--demo-only?farbe=Blau">Farbe: Blau</option>
            <option value="./filterpanel--demo-only?farbe=Gelb">Farbe: Gelb (kein Ergebnis)</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Blau">Fahrzeug: Auto, Farbe: Blau</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Rot">Fahrzeug: Auto, Farbe: Rot (kein Ergebnis(</option>
            <option value="./filterpanel--demo-only?fahrzeug=Segways&farbe=Rot">Fahrzeug: Segways, Farbe: Rot</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Gelb">Fahrzeug: Autos, Farbe: Gelb</option>
            <option value="./filterpanel--demo-only?farbe=Blau&fahrzeug=Autos">Farbe: Blau, Fahrzeug: Auto (umgedrehte Reihenfolge)</option>
            <option value="./filterpanel--demo-only?search=Auto">Suche nach Autos</option>
        </select>
    </p>

    <hr>

    <h3 style="margin-top: var(--sp);">Show single demo</h3>

    <p>
        <select class="doc-select" onchange="javascript: window.location = this.value;">
            <option target="_top" selected disabled>Please select</option>
            <hr />
            <option value="./filterpanel--demo-only" target="_top">Show all</option>
            <hr />
            <option value="./filterpanel--demo-only--dropdowns" target="_top">Dropdowns</option>
            <option value="./filterpanel--demo-only--dropdown-with-groups" target="_top">Dropdown with Groups</option>
            <option value="./filterpanel--demo-only--cascading-dropdowns" target="_top">Cascading Dropdowns</option>
            <option value="./filterpanel--demo-only--cascading-dropdowns-with-groups" target="_top">Cascading Dropdown with Groups</option>
            <option value="./filterpanel--demo-only--comboboxes" target="_top">Comboboxes</option>
            <option value="./filterpanel--demo-only--combobox--with-groups" target="_top">Combobox with Groups</option>
            <option value="./filterpanel--demo-only--cascading-comboboxes" target="_top">Cascading Comboboxes</option>
            <option value="./filterpanel--demo-only--live-filter" target="_top">Live Filter</option>
            <option value="./filterpanel--demo-only--live-and-cascading-dropdowns" target="_top">Live and Cascading Dropdowns</option>
            <option value="./filterpanel--demo-only--live-and-cascading-comboboxes" target="_top">Live and Cascascading Comboboxes</option>
        </select>
    </p>

</div>

<div class="doc-section">

    <h2 class="doc-variant-title">No Items given</h2>

    <div class="filterPanel" data-target="filterPanel-combo-filter-target">

        <div class="filterPanel-filters" data-cascading="true" data-theme="dark">

            <div class="filterPanel-search">
                <div class="searchField  is-required">

                    <label class="label is-required">Suche</label>

                    <span class="searchField-field">
                        <input class="field" type="text" spellcheck="false" required="" aria-required="true" />

                    </span>

                </div>

            </div>

            <hr class="filterPanel-divider" data-label="oder" />

            <div class="filterPanel-filter has-dropdown" data-filter-id="fahrzeug">
                <div class="dropdownMenu">

                    <label class="label is-optional">Fahrzeug</label>

                    <span class="dropdownMenu-select">
                        <span class="select">

                            <select>
                                <option value="">
                                    Alle Fahrzeuge
                                </option>
                                <option value="Segways">
                                    Segways
                                </option>
                            </select>

                        </span>

                    </span>

                </div>

            </div>

            <div class="filterPanel-filter has-dropdown" data-filter-id="farbe">
                <div class="dropdownMenu">

                    <label class="label is-optional">Farbe</label>

                    <span class="dropdownMenu-select">
                        <span class="select">

                            <select>
                                <option value="">
                                    Alle Farben
                                </option>
                            </select>

                        </span>

                    </span>

                </div>

            </div>

        </div>

        <div class="filterPanel-status">
        </div>

        <div class="filterPanel-sorting">

            <div class="dropdownMenu is-transparent" data-type="sorting">

                <label class="label is-optional is-transparent">Sortierung</label>

                <span class="dropdownMenu-select">
                    <span class="select">

                        <select>
                            <option value="[data-preferred]" selected="selected">
                                Bevorzugte zuerst
                            </option>
                            <option value="DESC [data-date]">
                                Neuste zuerst
                            </option>
                            <option value="[data-date]">
                                Älteste zuerst
                            </option>
                            <option value=".title--demo-only">
                                Nach Titel
                            </option>
                            <option value="DESC .title--demo-only">
                                Nach Titel (absteigend)
                            </option>
                        </select>

                    </span>

                </span>

            </div>

        </div>

        <template data-filter-messages>
            <div data-count="default">Alle Einträge</div>
            <div data-count="zero">Nichts gefunden</div>
            <div data-count="one"><strong>1</strong> Eintrag gefunden</div>
            <div data-count="other"><strong>{{count}}</strong> Einträge gefunden</div>
        </template>

    </div>

    <div id="filterPanel-combo-filter-target" class="doc-box" style="margin-top: var(--sp-large);" filterpanel-cloak>

        <div class="filterPanel-placeholder" data-filter-placeholder>
            Leider wurden mit der aktuellen Auswahl keine passenden Einträge gefunden.
        </div>

    </div>

    <div class="filterPanel" data-target="filterPanel-combo-filter-target">

        <div class="filterPanel-status">
        </div>

        <div class="filterPanel-sorting">

            <div class="dropdownMenu is-transparent" data-type="sorting">

                <label class="label is-optional is-transparent">Sortierung</label>

                <span class="dropdownMenu-select">
                    <span class="select">

                        <select>
                            <option value="[data-preferred]" selected="selected">
                                Bevorzugte zuerst
                            </option>
                            <option value="DESC [data-date]">
                                Neuste zuerst
                            </option>
                            <option value="[data-date]">
                                Älteste zuerst
                            </option>
                            <option value=".title--demo-only">
                                Nach Titel
                            </option>
                            <option value="DESC .title--demo-only">
                                Nach Titel (absteigend)
                            </option>
                        </select>

                    </span>

                </span>

            </div>

        </div>

        <template data-filter-messages>
            <div data-count="default">Alle Einträge</div>
            <div data-count="zero">Nichts gefunden</div>
            <div data-count="one"><strong>1</strong> Eintrag gefunden</div>
            <div data-count="other"><strong>{{count}}</strong> Einträge gefunden</div>
        </template>

    </div>

</div>
<div class="doc-notes" style="margin-block: var(--sp-component);">

    <h2 class="doc-variant-title">Demos</h2>

    <h3>Preselect filters</h3>

    <p><strong>Please regard:</strong> Due to the dynamic adaptation of the URL, it is possible that the URL displayed in the address bar of the browser does not match the link called up.</p>

    <p>
        <select class="doc-select" onchange="javascript: window.location = this.value;">
            <option vtarget="_top" selected disabled>Please select</option>
            <hr>
            <option value="./filterpanel--demo-only" target="_top">Reset filters</option>
            <hr>
            <option value="./filterpanel--demo-only?fahrzeug=Autos">Fahrzeug: Auto</option>
            <option value="./filterpanel--demo-only?fahrzeug=Segways">Fahrzeug: Segways (kein Ergebnis)</option>
            <option value="./filterpanel--demo-only?farbe=Blau">Farbe: Blau</option>
            <option value="./filterpanel--demo-only?farbe=Gelb">Farbe: Gelb (kein Ergebnis)</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Blau">Fahrzeug: Auto, Farbe: Blau</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Rot">Fahrzeug: Auto, Farbe: Rot (kein Ergebnis(</option>
            <option value="./filterpanel--demo-only?fahrzeug=Segways&farbe=Rot">Fahrzeug: Segways, Farbe: Rot</option>
            <option value="./filterpanel--demo-only?fahrzeug=Autos&farbe=Gelb">Fahrzeug: Autos, Farbe: Gelb</option>
            <option value="./filterpanel--demo-only?farbe=Blau&fahrzeug=Autos">Farbe: Blau, Fahrzeug: Auto (umgedrehte Reihenfolge)</option>
            <option value="./filterpanel--demo-only?search=Auto">Suche nach Autos</option>
        </select>
    </p>

    <hr>

    <h3 style="margin-top: var(--sp);">Show single demo</h3>

    <p>
        <select class="doc-select" onchange="javascript: window.location = this.value;">
            <option target="_top" selected disabled>Please select</option>
            <hr/>
            <option value="./filterpanel--demo-only" target="_top">Show all</option>
            <hr/>
            <option value="./filterpanel--demo-only--dropdowns" target="_top">Dropdowns</option>
            <option value="./filterpanel--demo-only--dropdown-with-groups" target="_top">Dropdown with Groups</option>
            <option value="./filterpanel--demo-only--cascading-dropdowns" target="_top">Cascading Dropdowns</option>
            <option value="./filterpanel--demo-only--cascading-dropdowns-with-groups" target="_top">Cascading Dropdown with Groups</option>
            <option value="./filterpanel--demo-only--comboboxes" target="_top">Comboboxes</option>
            <option value="./filterpanel--demo-only--combobox--with-groups" target="_top">Combobox with Groups</option>
            <option value="./filterpanel--demo-only--cascading-comboboxes" target="_top">Cascading Comboboxes</option>
            <option value="./filterpanel--demo-only--live-filter" target="_top">Live Filter</option>
            <option value="./filterpanel--demo-only--live-and-cascading-dropdowns" target="_top">Live and Cascading Dropdowns</option>
            <option value="./filterpanel--demo-only--live-and-cascading-comboboxes" target="_top">Live and Cascascading Comboboxes</option>
        </select>
    </p>

</div>

    {{#demo}}
        {{#unless hidden}}
<div class="doc-section">

    {{#title}}
        <h2 class="doc-variant-title">{{{.}}}</h2>
    {{/title}}

    {{#notes}}
        <div class="doc-variant-notes">{{{.}}}</div>
    {{/notes}}

    {{#filterpanel}}
        {{render '@filterpanel' (contextData '@filterpanel' this) merge=false}}
    {{/filterpanel}}

    <div id="{{filterpanel.target-id}}" class="doc-box" style="margin-top: var(--sp-large);" filterpanel-cloak>

        {{> '@filterpanel--demo-items'}}

            {{#filterpanel.placeholder}}
        <div class="filterPanel-placeholder" data-filter-placeholder>
            {{{.}}}
        </div>
            {{/filterpanel.placeholder}}

    </div>

    {{#statuspanel}}
        {{render '@filterpanel' (contextData '@filterpanel' this) merge=false}}
    {{/statuspanel}}

</div>
        {{/unless}}
    {{/demo}}
{
  "filter": {
    "items": [
      {
        "dropdownmenu": {
          "id": null,
          "label": "Auswahl 1",
          "select": {
            "id": null,
            "placeholder": null,
            "options": [
              {
                "label": "Alle anzeigen",
                "value": ""
              },
              {
                "label": "Option 1.1",
                "value": 1
              },
              {
                "label": "Option 1.2",
                "value": 2
              },
              {
                "label": "Option 1.3",
                "value": 3
              }
            ]
          }
        }
      },
      {
        "dropdownmenu": {
          "id": null,
          "label": "Auswahl 2",
          "select": {
            "id": null,
            "placeholder": null,
            "options": [
              {
                "label": "Alle anzeigen",
                "value": ""
              },
              {
                "label": "Option 2.1",
                "value": 1
              },
              {
                "label": "Option 2.2",
                "value": 2
              },
              {
                "label": "Option 2.3",
                "value": 3
              }
            ]
          }
        }
      },
      {
        "dropdownmenu": {
          "id": null,
          "label": "Auswahl 3",
          "select": {
            "id": null,
            "placeholder": null,
            "options": [
              {
                "label": "Alle anzeigen",
                "value": ""
              },
              {
                "label": "Option 3.1",
                "value": 1
              },
              {
                "label": "Option 3.2",
                "value": 2
              },
              {
                "label": "Option 3.3",
                "value": 3
              }
            ]
          }
        }
      }
    ]
  },
  "demo": {
    "title": "No Items given",
    "hidden": false,
    "items": [],
    "filter": null,
    "filterpanel": {
      "target-id": "filterPanel-combo-filter-target",
      "statuses": {
        "default": "Alle Einträge",
        "zero": "Nichts gefunden",
        "one": "<strong>1</strong> Eintrag gefunden",
        "other": "<strong>{{count}}</strong> Einträge gefunden"
      },
      "sorting": {
        "dropdownmenu": {
          "label": "Sortierung",
          "modifier": "is-transparent",
          "type": "sorting",
          "select": {
            "id": null,
            "placeholder": null,
            "options": [
              {
                "label": "Bevorzugte zuerst",
                "is-selected": true,
                "value": "[data-preferred]"
              },
              {
                "label": "Neuste zuerst",
                "value": "DESC [data-date]"
              },
              {
                "label": "Älteste zuerst",
                "value": "[data-date]"
              },
              {
                "label": "Nach Titel",
                "value": ".title--demo-only"
              },
              {
                "label": "Nach Titel (absteigend)",
                "value": "DESC .title--demo-only"
              }
            ]
          }
        }
      },
      "placeholder": "Leider wurden mit der aktuellen Auswahl keine passenden Einträge gefunden.",
      "filter": {
        "is-cascading": true,
        "items": [
          {
            "searchfield": {
              "searchfield": {
                "is-required": true,
                "field": {
                  "id": null,
                  "label": "Suche"
                }
              }
            }
          },
          {
            "divider": "oder"
          },
          {
            "filter-id": "fahrzeug",
            "dropdownmenu": {
              "id": null,
              "label": "Fahrzeug",
              "select": {
                "id": null,
                "placeholder": null,
                "options": [
                  {
                    "label": "Alle Fahrzeuge",
                    "value": null
                  },
                  {
                    "label": "Segways",
                    "value": "Segways"
                  }
                ]
              }
            }
          },
          {
            "filter-id": "farbe",
            "dropdownmenu": {
              "id": null,
              "label": "Farbe",
              "select": {
                "id": null,
                "placeholder": null,
                "options": [
                  {
                    "label": "Alle Farben",
                    "value": null
                  }
                ]
              }
            }
          }
        ]
      }
    },
    "statuspanel": {
      "target-id": "filterPanel-combo-filter-target",
      "statuses": {
        "default": "Alle Einträge",
        "zero": "Nichts gefunden",
        "one": "<strong>1</strong> Eintrag gefunden",
        "other": "<strong>{{count}}</strong> Einträge gefunden"
      },
      "sorting": {
        "dropdownmenu": {
          "label": "Sortierung",
          "modifier": "is-transparent",
          "type": "sorting",
          "select": {
            "id": null,
            "placeholder": null,
            "options": [
              {
                "label": "Bevorzugte zuerst",
                "is-selected": true,
                "value": "[data-preferred]"
              },
              {
                "label": "Neuste zuerst",
                "value": "DESC [data-date]"
              },
              {
                "label": "Älteste zuerst",
                "value": "[data-date]"
              },
              {
                "label": "Nach Titel",
                "value": ".title--demo-only"
              },
              {
                "label": "Nach Titel (absteigend)",
                "value": "DESC .title--demo-only"
              }
            ]
          }
        }
      },
      "filter": null
    }
  }
}
  • Content:
    import filterPanel from "./_filterPanel.script.js";
    
    filterPanel.init();
    
  • URL: /components/raw/filterpanel/_filterPanel.js
  • Filesystem Path: components/03-fragments/filterPanel/_filterPanel.js
  • Size: 73 Bytes
  • Content:
    export default (function (){
    
    
        const defaults = {
            selectors: {
                panel: '.filterPanel',
                filter: '.filterPanel-filter',
                search: '.filterPanel-search',
                placeholder: '[data-filter-placeholder]',
                status: '.filterPanel-status',
                sorting: '.filterPanel-sorting',
            },
            keySeparator: '\|',
            splitPattern: /\| ?/,
        };
    
        function init(options) {
    
            options = options || {};
    
            const filterPanels = document.querySelectorAll(defaults.selectors.panel);
    
            filterPanels.forEach((filterPanel) => {
    
                const settings = Object.assign({}, defaults, options);
    
                settings.isCascading = filterPanel.querySelector('[data-cascading="true"]') !== null;
    
                if (typeof filterPanel.dataset.target === "string" && filterPanel.dataset.target !== "") {
                    new FilterPanel(filterPanel, settings);
                } else if (settings.isCascading) {
                    new CascadingFilters(filterPanel, null, settings);
                }
    
            });
    
        }
    
        /************************/
        /***** Filter Panel *****/
        /************************/
    
        const FilterPanel = function(container, options) {
    
            this.settings = Object.assign({}, options);
    
            this.container = container;
    
            this.selector = "#" + this.container.dataset.target;
    
            this.settings.isCascading = this.container.querySelector('[data-cascading="true"]') !== null;
    
            // Target
    
            if (typeof this.container.dataset.target !== "string" || this.container.dataset.target === "") {
                return;
            }
    
            this.target = document.querySelector(this.selector);
    
            if (! this.target) {
                console.warn(`filterPanel: No target match selector "${this.selector}"`);
                return false;
            }
    
            // Placeholder
    
            this.placeholder = this.target.querySelector(this.settings.selectors.placeholder);
    
            // Filters
    
            this.filters = {};
            this.currentFilters = {};
    
            const filters = this.container.querySelectorAll("[data-filter-id]");
    
            filters.forEach((filter) => {
    
                const filterId = getFilterId(filter);
    
                this.filters[filterId] = new Filter(filter, filterId, this);
    
            });
    
            this.items = this.getItems();
    
            // Sorting
    
            this.sorters = [];
    
            const sorters = document.querySelectorAll(`[data-target='${this.container.dataset.target}'] ${this.settings.selectors.sorting}`);
    
            sorters.forEach((sorter) => {
    
                const select = sorter.querySelector("select");
    
                if (! select) {
                    return;
                }
    
                if (typeof this.defaultSortingString === "undefined" && select.value) {
                    this.defaultSortingString = select.value;
                }
    
                select.addEventListener(
                    "change",
                    (e) => {
                        e.preventDefault();
                        this.sort(e.target.value);
                    }
                );
    
                let values = [];
    
                select.querySelectorAll("option").forEach(option => {
    
                    if (option.value && option.value !== "") {
                        values.push(option.value);
                    }
    
                });
    
                this.sorters.push({
                    select: select,
                    values: values,
                });
    
            });
    
            // Cascading
    
            if (this.settings.isCascading) {
                this.cascadingFilters = new CascadingFilters(this.container, this, this.settings);
            }
    
            // Live filter
    
            this.currentLiveFilter;
            this.liveFilter = new LiveFilter(this.container, this, this.settings);
    
            // Statuses
    
            this.messages = {};
    
            const template = this.container.querySelector("template[data-filter-messages]");
    
            if (template) {
    
                const messages = template.content.querySelectorAll("[data-count]");
    
                messages.forEach((message) => {
                    const type = message.dataset.count;
                    this.messages[type] = message;
                });
    
            }
    
    
            this.status = new Status(this);
    
            // To prevent resetting the formerly set status,
            //   only set the statuses if filters are part of the element
    
            if (filters.length !== 0 || this.liveFilter.input !== null) {
    
                let count = "default";
    
                if (this.items.length === 0) {
                    count = 0;
                }
    
                this.status.update(count);
    
            }
    
            // Show hidden items
    
            this.target.removeAttribute("filterpanel-cloak");
    
            this.setFilters();
            this.setSorting();
    
            this.isInitialized = true;
    
        }
    
        FilterPanel.prototype.filter = function(filter, value) {
    
            this.liveFilter.reset();
    
            this.currentFilters[filter.filterId] = value;
    
            if (this.settings.isCascading && typeof filter.index === "number") {
    
                const currentIndex = filter.index;
    
                Object.keys(this.currentFilters).forEach((filterId, index) => {
                    const filter = this.filters[filterId];
    
                    if (filter.index > currentIndex) {
                        this.currentFilters[filter.filterId] = "";
                    }
    
                });
    
            }
    
            let count = 0;
    
            this.items.forEach((item) => {
    
                let isFilteredOut = false;
    
                for (let [key, value] of Object.entries(this.currentFilters)) {
    
                    const values = this.getItemValues(item, key);
    
                    values.forEach((value, index) => {
                        values[index] = value.toLowerCase();
                    });
    
                    value = value.toLowerCase();
    
                    isFilteredOut = isFilteredOut || (value !== "") && ! values.includes(value);
    
                }
    
                item.dataset.filteredOut = isFilteredOut;
    
                if (! isFilteredOut) {
                    count++;
                }
    
            });
    
            if (count !== 0 && count === this.items.length) {
                count = "default";
            }
    
            this.status.update(count);
            this.updateFilters();
            this.updateURL();
            this.scrollIntoView();
    
            return count;
    
        }
    
        FilterPanel.prototype.sort = function(sortString) {
    
            let useDefaultSorting = (typeof sortString !== "string" || sortString === this.defaultSortingString);
    
            if (useDefaultSorting) {
                sortString = this.defaultSortingString;
            }
    
            const items = Array.from(this.items);
    
            if (items.length === 0) {
                return;
            }
    
            items.sort(this.getSortFunction(sortString));
    
            const parentNode = items[0].parentNode;
    
            items.forEach((item) => {
                parentNode.appendChild(item)
            });
    
            this.currentSorting = sortString;
    
            this.synchroniseSorter(sortString);
            this.updateURL();
    
        }
    
        FilterPanel.prototype.synchroniseSorter = function(value) {
    
            this.sorters.forEach((sorter) => {
                sorter.select.value = value;
            });
    
        }
    
        FilterPanel.prototype.setSorting = function(value) {
    
            if (! window.location.search) {
                return;
            }
    
            if (typeof this.sorters !== "object" || this.sorters.length === 0) {
                return;
            }
    
            const sorter = this.sorters[0];
    
            const queryString = window.location.search.replace(/^\?/, ""),
                  params = new URLSearchParams(queryString);
    
            if (! params.get("sorting")) {
                this.sort();
                return;
            }
    
            const sortString = params.get("sorting");
    
            // Check if search string is an option of select
    
            const isValid = sorter.values.some(value => value === sortString);
    
            if (! isValid) {
                console.warn(`filterPanel: Sorting is ignored because sorting string "${sortString}" is not found an option of select.`);
                return;
            }
    
            this.sort(sortString);
    
        }
    
        FilterPanel.prototype.getSortFunction = function(sortString) {
    
            let sortFunction = () => {
                return true;
            };
    
            let direction = 1;
    
            if (typeof sortString === "string" && sortString.match(/^(ASC|DESC) /)) {
    
                let matches = sortString.match(/^(ASC|DESC) (.*)$/);
    
                direction = (matches[1] !== "DESC") ? direction : -1 * direction;
    
                sortString = matches[2];
    
            }
    
            if (typeof sortString !== "string" || sortString === "") {
    
                sortFunction = (a, b) => {
    
                    const items = [
                        {
                            value: a.innerText,
                            isPreferred: typeof a.dataset.preferred === "string" && a.dataset.preferred !== "false",
                        },
                        {
                            value: b.innerText,
                            isPreferred: typeof b.dataset.preferred === "string" && b.dataset.preferred !== "false",
                        }
                    ];
    
                    return this.compareItems(items, direction);
    
                };
    
            } else if (sortString.match(/^\[data-([^\]=]+)\]$/)) {
    
                const matches = sortString.match(/^\[data-([^\]=]+)\]$/),
                      name = matches[1];
    
                sortFunction = (a, b) => {
    
                    const items = [
                        {
                            value: a.dataset[name],
                            isPreferred: typeof a.dataset.preferred === "string" && a.dataset.preferred !== "false",
                        },
                        {
                            value: b.dataset[name],
                            isPreferred: typeof b.dataset.preferred === "string" && b.dataset.preferred !== "false",
                        }
                    ];
    
                    return this.compareItems(items, direction);
    
                };
    
            } else {
    
                sortFunction = (a, b) => {
    
                    const childOfA = a.querySelector(sortString),
                          childOfB = b.querySelector(sortString);
    
                    if (! childOfA || ! childOfB) {
                        return true;
                    }
    
                    const items = [
                        {
                            value: childOfA.innerText,
                            isPreferred: typeof a.dataset.preferred === "string" && a.dataset.preferred !== "false",
                        },
                        {
                            value: childOfB.innerText,
                            isPreferred: typeof b.dataset.preferred === "string" && b.dataset.preferred !== "false",
                        }
                    ];
    
                    return this.compareItems(items, direction);
    
                };
    
            }
    
            return sortFunction;
    
        }
    
        FilterPanel.prototype.compareItems = function (items, direction) {
    
            if (typeof direction !== "number") {
                direction = 1;
            }
    
            // Check if value is string with boolean value or undefined.
            //   If so, sort empty strings before undefined values
    
            items.forEach((item) => {
    
                if (typeof item.value === "undefined") {
                    item.value = "0";
                } else if (typeof item.value === "string" && ["true", "false"].includes(item.value)) {
                    item.value = (item.value === "true") ? (-1 * direction) + "" : direction + "";
                }
    
            });
    
            let comparison = items[0].value.localeCompare(items[1].value);
    
            // If items have same value, sort preferred before others
    
            if (comparison === 0) {
    
                if (items[0].isPreferred) {
                    comparison = -1 * direction;
                } else if (items[1].isPreferred) {
                    comparison = direction;
                }
    
            }
    
            return direction * comparison;
    
        }
    
        FilterPanel.prototype.getItems = function() {
    
            if (this.target.querySelectorAll('[data-item]')) {
                return this.target.querySelectorAll('[data-item]');
            }
    
            // Support of legacy method using data attributes with filter names
            //   Attention: Does not work with groups
    
            let selectors = [];
    
            if (Object.keys(this.filters).length === 0) {
                return {};
            }
    
            for (let key of Object.keys(this.filters)) {
                selectors.push(`[data-filter-${key}]`);
            }
    
            return this.target.querySelectorAll(selectors.join());
    
        }
    
        FilterPanel.prototype.countItems = function(currentFilters) {
    
            const items = this.items;
    
            if (typeof currentFilters !== "object" || Object.keys(currentFilters).length === 0) {
                return items.length;
            }
    
            let count = 0;
    
            items.forEach((item) => {
    
                let isFilteredOut = false;
    
                for (let [key, value] of Object.entries(currentFilters)) {
    
                    const values = this.getItemValues(item, key);
    
                    isFilteredOut = isFilteredOut || (value !== "") && ! values.includes(value);
    
                }
    
                if (! isFilteredOut) {
                    count++;
                }
    
            });
    
            return count;
    
        }
    
        FilterPanel.prototype.getItemValues = function(item, keys) {
    
            let values = [];
    
            keys.split(/\+/).forEach((name) => {
    
                const datasetName = camelCase(`filter-${name}`);
    
                if (! item.dataset[datasetName]) {
                    return;
                }
    
                const data = item.dataset[datasetName].split(this.settings.splitPattern);
    
                values = values.concat(data);
    
            });
    
            return values;
    
        }
    
        FilterPanel.prototype.updateFilters = function() {
    
            // Deactivate updateFilters because it returns unexpected results
            return;
    
            Object.keys(this.filters).forEach((key) => {
                this.filters[key].update();
            });
    
        }
    
        FilterPanel.prototype.updateFilter = function(key) {
    
            if (typeof this.filters !== "object") {
                return;
            }
    
            const keys = Object.keys(this.filters);
    
            if (! keys.includes(key)) {
                return;
            }
    
            this.filters[key].update({
                removeFiltered: true,
            });
    
        }
    
        FilterPanel.prototype.resetFilters = function() {
    
            Object.keys(this.filters).forEach((key) => {
                this.filters[key].reset();
            });
    
            if (this.cascadingFilters) {
                this.cascadingFilters.resetFilters();
            }
    
        }
    
        FilterPanel.prototype.setFilters = function() {
            /*
             * FIXME: If filters has option groups, filtering only works if last filter has options.
             */
    
            if (! window.location.search) {
                return;
            }
    
            let values = {},
                isLiveFilter = false,
                queryString = window.location.search.replace(/^\?/, "");
    
            const filterIds = Object.keys(this.filters);
    
            queryString.split(/&/).forEach(entry => {
    
                let [key, value] = entry.split(/=/);
    
                value = decodeURIComponent(value);
    
                if (key === "search") {
    
                    values = {
                        "search": value,
                    };
    
                    isLiveFilter = true;
    
                    return;
    
                }
    
                const filterId = filterIds.find(filterId => {
                    return filterId.split(/\+/).includes(key);
                });
    
                if (filterId) {
                    values[filterId] = decodeURIComponent(value);
                }
    
            });
    
            if (Object.keys(values).length === 0) {
                return;
            }
    
            if (isLiveFilter && this.liveFilter) {
                this.liveFilter.filter(values["search"]);
                return;
            }
    
            // Sort values by filters
    
            let sortedValues = {};
    
            Object.keys(this.filters).forEach((key) => {
    
                let value = "";
    
                if (typeof values[key] === "string") {
                    value = values[key];
                }
    
                sortedValues[key] = value;
    
            });
    
            let anchestorValue = null,
                currentFilters = {};
    
            Object.keys(sortedValues).forEach((key, index) => {
    
                if (this.settings.isCascading && (anchestorValue === "")) {
                    console.warn(`filterPanel: Can’t set cascading filters, because the ancestor of the filter with key "${key}" is not set.`);
                    return
                } else {
                    anchestorValue = "";
                }
    
                let isValid = false,
                    filterItems = false;
    
                currentFilters[key] = sortedValues[key];
    
                if (! this.settings.isCascading) {
    
                    let currentFilter = {};
                    currentFilter[key] = sortedValues[key];
    
                    isValid = (this.countItems(currentFilter) !== 0);
    
                } else {
    
                    isValid = (this.countItems(currentFilters) !== 0);
    
                }
    
                if (isValid) {
    
                    const value = sortedValues[key];
    
                    anchestorValue = value;
    
                    this.filter(this.filters[key], value);
    
                    if (this.filters[key].input.select) {
    
                        this.filters[key].input.update(value);
    
                    } else if (this.filters[key].input.combobox && value !== "") {
    
                        const combobox = this.filters[key].input.combobox;
    
                        if (combobox.querySelector(`[data-value="${value}"]`)) {
    
                            const event = new CustomEvent('combobox:update', {
                                detail: {
                                    value: value,
                                },
                            });
    
                            combobox.dispatchEvent(event);
    
                        }
    
                    }
    
                    if (this.settings.isCascading) {
                        this.cascadingFilters.updateFilters(index);
                    }
    
                }
    
            });
    
        }
    
        FilterPanel.prototype.updateURL = function() {
    
            if (typeof this.currentFilters !== "object" || Object.keys(this.currentFilters).length === 0) {
                this.currentFilters = {};
            }
    
            let parameters = {};
    
            if (window.location.search) {
    
                let queryString = window.location.search.replace(/^\?/, "");
    
                queryString.split(/&/).forEach(entry => {
    
                    const [key, value] = entry.split(/=/);
                    parameters[key] = value;
    
                });
    
            }
    
            function setParameter(key, value) {
    
                if (typeof value === "string" && value !== "") {
                    parameters[key] = value;
                } else if (parameters[key]) {
                    delete parameters[key];
                }
    
            }
    
            if (typeof this.currentFilters === "object" || Object.keys(this.currentFilters).length !== 0) {
    
                for (let [key, value] of Object.entries(this.currentFilters)) {
    
                    // If key is option group, only pass the parameter witch have items
    
                    if (key.includes("+")) {
    
                        let valueAlreadyFound = false;
    
                        key.split(/\+/).forEach((filterId) => {
    
                            setParameter(filterId);
    
                            let currentFilter = {};
                                currentFilter[filterId] = value;
    
                            let isValid = value !== "" && (this.countItems(currentFilter) !== 0);
    
                            if (! isValid) {
                                return;
                            }
    
                            if (valueAlreadyFound) {
                                console.warn(`filterPanel: Value "${value}" of Option Group "${key}" is ambiguous.`);
                                return;
                            }
    
                            valueAlreadyFound = true;
                            setParameter(filterId, value);
    
                        });
    
                    } else {
    
                        const isValid = (value !== "");
    
                        if (! isValid) {
                            value = "";
                        }
    
                        setParameter(key, value);
    
                    }
    
                }
    
            }
    
            // Live filter
    
            if (typeof this.currentLiveFilter === "string") {
                setParameter("search", this.currentLiveFilter);
            }
    
            // Sorting
    
            if (typeof this.currentSorting === "string") {
    
                if (this.currentSorting !== "" && this.currentSorting !== this.defaultSortingString) {
                    setParameter("sorting", this.currentSorting);
                } else {
                    setParameter("sorting");
                }
    
            }
    
            let url = window.location.origin + window.location.pathname;
    
            if (Object.keys(parameters).length !== 0) {
    
                let queryString = [];
    
                Object.keys(parameters).forEach(key => {
                    queryString.push(`${key}=${parameters[key]}`);
                });
    
                url += "?" + queryString.join("&");
    
            }
    
            history.replaceState(null, "", url);
    
        }
    
        FilterPanel.prototype.scrollIntoView = function() {
    
            if (! this.isInitialized) {
                return;
            }
    
            this.container.scrollIntoView();
    
        }
    
        /******************/
        /***** Filter *****/
        /******************/
    
        const Filter = function(container, filterId, parent) {
    
            this.container = container;
            this.parent = parent;
            this.root = parent;
    
            this.settings = this.parent.settings;
    
            this.filterId = filterId;
    
            this.datasetNames = this.filterId.split(/\+/);
    
            // Items
    
            let selectors = [];
    
            this.datasetNames.forEach((id) => {
                selectors.push(`[data-filter-${id}]`);
            });
    
            this.items = this.parent.target.querySelectorAll(selectors.join(","));
    
            // Input
    
            this.input = new Input(this.container);
    
            if (! this.input) {
                this.update = function() {};
                return;
            }
    
            // Index for cascading filters
    
            if (this.settings.isCascading) {
                this.index = Object.keys(this.parent.filters).length;
            }
    
            // Init
    
            if (this.items.length === 0) {
                this.input.disable();
                this.update = function() {};
                return;
            }
    
            // Options
    
            this.options = [];
    
            this.update();
            this.setDisabledOptions();
    
            // Events
    
            if (this.input.combobox) {
    
                this.input.combobox.addEventListener('combobox:update', (event) => {
    
                    if (typeof event.detail.value !== "string" || typeof event.detail.element !== "object") {
                        return;
                    }
    
                    const element = event.detail.element,
                          filter = element.closest(`${this.settings.selectors.filter}[data-filter-id]`);
    
                    if (typeof filter !== "object" || getFilterId(filter) !== this.filterId) {
                        return;
                    }
    
                    this.root.filter(this, event.detail.value);
    
                }, false);
    
            }
    
            if (this.input.select) {
    
                this.input.select.addEventListener(
                    "change",
                    (event) => {
                        this.root.filter(this, this.input.select.value);
                    }
                );
    
            }
    
        }
    
        Filter.prototype.update = function(settings) {
    
            settings = settings || {};
    
            const removeFiltered = settings.removeFiltered || false;
    
            if (this.datasetNames.length  > 1) {
    
                if (this.input.select) {
                    this.options = Array.from(this.input.select.querySelectorAll("option"));
                }
    
                this.datasetNames.forEach((id) => {
    
                    const items = this.parent.target.querySelectorAll(`[data-filter-${id}]`);
    
                    let label = this.container.getAttribute(`data-label-${id}`);
    
                    if (typeof label !== "string" || label === "") {
                        label = capitalize(id);
                    }
    
                    if (this.input.select) {
    
                        let optionGroup = this.input.select.querySelector(`optgroup[label="${label}"]`);
    
                        if (! optionGroup) {
    
                            optionGroup = document.createElement("optgroup");
                            optionGroup.label = label;
    
                            this.input.select.appendChild(optionGroup);
    
                        }
    
                        const options = this.updateOptions({
                            element: optionGroup,
                            items: items,
                            filterId: id,
                            removeFiltered: removeFiltered,
                        });
    
                        this.options = this.options.concat(options);
    
                    } else if (this.input.combobox) {
    
                        this.options = this.updateOptions({
                            element: this.input.container,
                            items: items,
                            filterId: this.filterId,
                            removeFiltered: removeFiltered,
                        });
    
                    }
    
                });
    
            } else {
    
                let selectors = [];
    
                this.datasetNames.forEach((id) => {
                    selectors.push(`[data-filter-${id}]`);
                });
    
                const items = this.parent.target.querySelectorAll(selectors.join(","));
    
                this.options = this.updateOptions({
                    element: this.input.container,
                    items: items,
                    filterId: this.filterId,
                    removeFiltered: removeFiltered,
                });
    
            }
    
        }
    
        Filter.prototype.updateOptions = function(settings) {
    
            settings = settings || {};
    
            const element = settings.element,
                  items = settings.items,
                  filterId = settings.filterId;
    
            const removeFiltered = (typeof settings.removeFiltered === "boolean" && settings.removeFiltered);
    
            let values = [];
    
            items.forEach((item) => {
    
                if (removeFiltered && item.dataset.filteredOut === "true") {
                    return;
                }
    
                const itemValues = this.parent.getItemValues(item, filterId);
    
                itemValues.forEach((value) => {
    
                    if (! values.includes(value)) {
                        values.push(value);
                    }
    
                });
    
            });
    
            values = values.sort();
    
            const options = [];
    
            if (this.input.select) {
    
                element.querySelectorAll("option").forEach((option) => {
    
                    if (! option.value) {
                        return;
                    }
    
                    if (removeFiltered && ! values.includes(option.value) ) {
                        element.removeChild(option);
                        return;
                    }
    
                    options.push(option);
    
                });
    
                values.forEach((value) => {
    
                    if (element.querySelector(`option[value="${value}"]`)) {
                        return;
                    }
    
                    const option = document.createElement("option");
    
                    option.value = value;
                    option.innerHTML = value;
    
                    options.push(option);
                    element.appendChild(option);
    
                });
    
            } else if (this.input.combobox) {
    
                // To keep things simple, no new entries are added to the combo box. Instead, existing entries are activated or deactivated depending on the search results.
    
                element.querySelectorAll(".combobox-item").forEach((option) => {
    
                    if (! option.hasAttribute("data-value")) {
                        return;
                    }
    
                    const value = option.getAttribute("data-value");
    
                    if (value !== "" && ! values.includes(value)) {
                        option.setAttribute("aria-disabled", true);
                        return;
                    }
    
                    option.removeAttribute("aria-disabled");
                    options.push(option);
    
                });
    
            }
    
            return options;
    
        }
    
        Filter.prototype.setDisabledOptions = function() {
    
            this.options.forEach((option) => {
    
                if (typeof option.value === "string" && option.value === "") {
                    return;
                }
    
                let selectors = [];
    
                this.datasetNames.forEach((name) => {
    
                    selectors = selectors.concat([
                        `[data-filter-${name}="${option.value}"]`,     // Is
                        `[data-filter-${name}^="${option.value},"]`,   // Starts with
                        `[data-filter-${name}$=", ${option.value}"]`,  // Ends with
                        `[data-filter-${name}$=",${option.value}"]`,   //
                        `[data-filter-${name}*=", ${option.value},"]`, // Contains
                        `[data-filter-${name}*=",${option.value},"]`,  //
                    ]);
    
                });
    
                const items = this.parent.target.querySelectorAll(selectors.join(","));
    
                const matches = Array.from(items).filter((item) => {
                    return item.dataset.filteredOut !== "true";
                });
    
                option.disabled = (matches.length === 0);
    
            });
    
        }
    
        Filter.prototype.reset = function() {
            this.input.reset();
        }
    
        /***********************/
        /***** Live Filter *****/
        /***********************/
    
        const LiveFilter = function(container, parent, options) {
    
            this.settings = Object.assign({}, options);
    
            this.input =  container.querySelector(`${this.settings.selectors.search} input[type="text"]`)
    
            if (! this.input) {
                return;
            }
    
            this.container = container;
            this.parent = parent || {};
    
            const items = this.parent.items;
    
            if (items.length === 0) {
                this.input.disabled = true;
                return;
            }
    
            this.items = [];
    
            items.forEach((item) => {
    
                let keywords = [];
    
                if (typeof item.dataset.keywords === "string") {
                    keywords = item.dataset.keywords.toLowerCase().split(this.settings.splitPattern);
                }
    
                this.items.push({
                    container: item,
                    keywords: keywords,
                })
    
            });
    
            this.input.addEventListener(
                "input",
                (e) => {
                    this.filter(e.target.value);
                    e.preventDefault();
                }
            );
    
        }
    
        LiveFilter.prototype.filter = function(searchString) {
    
            if (typeof this.parent === "undefined") {
                return;
            }
    
            this.parent.resetFilters();
    
            if (typeof searchString !== "string") {
                searchString = "";
            }
    
            const items = this.parent.items;
    
            let count = 0;
    
            function stringContainAllItems(string, items) {
    
                return items.every(item => {
                    const regex = new RegExp(`^${item.toLowerCase()}| ${item.toLowerCase()}`);
                    return regex.test(string);
                });
    
            }
    
            items.forEach((item) => {
    
                if (searchString === "") {
                    item.dataset.filteredOut = false;
                    return;
                }
    
                if (typeof item.dataset.keywords !== "string" || item.dataset.keywords === "") {
                    item.dataset.filteredOut = true;
                    return;
                }
    
                const searchStrings = searchString.split(' ');
    
                // Replace special characters with spaces
                const keywords = item.dataset.keywords.toLowerCase().replace(/[.,;\/\-] ?/g, " ");
    
                const isFilteredOut = ! stringContainAllItems(keywords, searchStrings);
    
                item.dataset.filteredOut = isFilteredOut;
    
                if (! isFilteredOut) {
                    count++;
                }
    
            });
    
            this.parent.currentLiveFilter = searchString;
            this.input.value = searchString;
    
            if (searchString === "" && items.length !== 0) {
                count = "default";
            }
    
            this.parent.status.update(count);
            this.parent.updateURL();
            this.parent.scrollIntoView();
    
        }
    
        LiveFilter.prototype.reset = function() {
    
            if (! this.input) {
                return;
            }
    
            this.input.value = "";
    
        }
    
        /******************/
        /***** Status *****/
        /******************/
    
        const Status = function(parent) {
    
            this.parent = parent;
            this.messages = this.parent.messages;
    
            this.statuses = document.querySelectorAll(`[data-target='${parent.container.dataset.target}'] ${parent.settings.selectors.status}`);
    
            if (! this.statuses) {
                this.update = function(){};
                return;
            }
    
        }
    
        Status.prototype.update = function(type) {
    
            this.statuses.forEach((status) => {
                status.innerHTML = "";
            });
    
            let count;
    
            if (typeof type === "number") {
    
                count = type;
    
                if (count === 0) type = "zero"
                else if (count === 1) type = "one"
                else type = "other";
    
            }
    
            if (typeof type !== "string" || this.messages[type] === "") {
                return;
            }
    
            if (typeof this.messages[type] === "undefined") {
                console.warn(`filterPanel: Message type "${type}" is not defined!`);
                return;
            }
    
            if (this.parent.placeholder) {
                this.parent.placeholder.dataset.visible = (type === "zero");
            }
    
            this.statuses.forEach((status) => {
    
                let message = this.messages[type].cloneNode(true);
    
                message.innerHTML = message.innerHTML.replace(/\{\{count\}\}/, count);
    
                status.appendChild(message);
    
            });
    
        }
    
        /*****************************/
        /***** Cascading Filters *****/
        /*****************************/
    
        const CascadingFilters = function(container, parent, options) {
    
            const settings = {
                disabledMarker: "–",
            };
    
            this.settings = Object.assign({}, settings, options);
    
            this.container = container;
            this.parent = parent || {};
    
            const filters = this.container.querySelectorAll(defaults.selectors.filter);
    
            if (! filters) {
                return;
            }
    
            this.filters = {};
    
            filters.forEach((filter, index) => {
    
                let label;
    
                if (filter.querySelector('label')) {
    
                    const element = filter.querySelector('label');
    
                    label = {
                        container: element,
                        content: element.innerHTML,
                    };
    
                }
    
                // Input
    
                const input = new Input(filter);
    
                if (! input) {
                    return;
                }
    
                input.disable(index !== 0);
    
                // Filter ID
    
                let filterId = getFilterId(filter);
    
                // Event listener
    
                if (input.combobox) {
    
                    input.combobox.addEventListener('combobox:update', (event) => {
    
                        if (typeof event.detail.value !== "string" || typeof event.detail.element !== "object") {
                            return;
                        }
    
                        this.updateFilters(index);
    
                    }, false);
    
                }
    
                input.container.addEventListener(
                    "change",
                    (e) => {
                        e.preventDefault();
                        this.updateFilters(index);
                    }
                );
    
                // Add filter
    
                this.filters[filterId] = {
                    container: filter,
                    label: label,
                    input: input,
                    select: input.select,
                    combobox: input.combobox,
                };
    
            });
    
            this.updateFilters(-1);
    
        }
    
        CascadingFilters.prototype.updateFilters = function(baseIndex) {
    
            let ancestor;
    
            Object.keys(this.filters).forEach((key, index) => {
    
                const filter = this.filters[key];
    
                let isDisabled = (index > baseIndex + 1);
    
                if (ancestor) {
                    isDisabled = (ancestor.input.getValue() === "");
                }
    
                if (typeof this.parent.items === "object" && this.parent.items.length === 0) {
                    isDisabled = true;
                }
    
                if (index !== 0) {
                    filter.container.dataset.disabled = isDisabled;
                }
    
                filter.input.disable(isDisabled);
    
                if (typeof filter.label === "object") {
                    filter.label.container.innerHTML = isDisabled && index !== 0 ? "&nbsp;" : filter.label.content;
                }
    
                if (typeof filter.input.placeholder === "object") {
    
                    const placeholder = (isDisabled ? this.settings.disabledMarker : filter.input.placeholder.content);
                    filter.input.setPlaceholder(placeholder);
    
                }
    
                if (index > baseIndex) {
                    filter.input.update("");
                }
    
                if (typeof this.parent === "object" && typeof this.parent.updateFilter === "function" && index === baseIndex + 1) {
                    this.parent.updateFilter(key);
                }
    
                ancestor = filter;
    
            });
    
        }
    
        CascadingFilters.prototype.resetFilters = function() {
            this.updateFilters(0);
        }
    
        /*****************/
        /***** Input *****/
        /*****************/
    
        const Input = function(parent) {
    
            this.select = parent.querySelector("select"),
            this.combobox = parent.querySelector(".combobox");
    
            if (! this.select && ! this.combobox) {
                return;
            }
    
            this.container = this.select || this.combobox;
    
            this.placeholder = this.getPlaceholder();
    
        }
    
        Input.prototype.disable = function(disable) {
    
            if (typeof disable !== "boolean") {
                disable = true;
            }
    
            if (this.select) {
    
                this.select.disabled = disable;
    
            } else if (this.combobox) {
    
                const event = new CustomEvent('combobox:disable', {
                    detail: {
                        disable: disable,
                    }
                });
    
                this.combobox.dispatchEvent(event);
    
            }
    
        }
    
        Input.prototype.reset = function() {
    
            if (this.select) {
    
                this.select.value = "";
    
            } else if (this.combobox) {
    
                const event = new CustomEvent('combobox:update', {
                    detail: {
                        value: "",
                    },
                });
    
                this.combobox.dispatchEvent(event);
    
            }
    
        }
    
        Input.prototype.update = function(value) {
    
            if (this.select) {
    
                this.select.value = value;
    
            } else if (this.combobox) {
    
                const event = new CustomEvent('combobox:update', {
                    detail: {
                        value: value,
                    },
                });
    
                this.combobox.dispatchEvent(event);
    
            }
    
        }
    
        Input.prototype.getPlaceholder = function() {
    
            let placeholder;
    
            const entry = this.container.querySelector('[value=""], [data-value=""]');
    
            if (! entry) {
                return placeholder;
            }
    
            if (entry.dataset.label) {
                // Is combobox
    
                placeholder = {
                    container: entry,
                    content: entry.dataset.label,
                }
    
                this.setPlaceholder(placeholder.content);
    
            } else if (entry.innerHTML) {
                // Is select
    
                placeholder = {
                    container: entry,
                    content: entry.innerHTML,
                }
    
            }
    
            return placeholder;
    
        }
    
        Input.prototype.getValue = function() {
    
            let value = "";
    
            if (this.select) {
    
                value =  this.select.value;
    
            } else if (this.combobox) {
    
                const input = this.combobox.querySelector("input");
    
                if (input && input.value) {
                    value =  input.value;
                }
    
            }
    
            return value;
    
        }
    
        Input.prototype.setPlaceholder = function(content) {
    
            if (this.combobox) {
    
                const comboboxInput = this.combobox.querySelector("input");
    
                if (comboboxInput) {
                    comboboxInput.placeholder = content;
                }
    
            } else if (this.select) {
    
                this.placeholder.container.innerHTML = content;
    
            }
    
        }
    
        /*************************/
        /***** Miscellaneous *****/
        /*************************/
    
        function getFilterId(filter, fallback) {
    
            fallback = fallback || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
    
            if (typeof filter !== "object" || typeof filter.dataset !== "object") {
                return fallback;
            }
    
            if (typeof filter.dataset.filterId === "string" && filter.dataset.filterId !== "") {
                return filter.dataset.filterId.replace(defaults.splitPattern, "+");
            }
    
            return fallback;
    
        }
    
        function camelCase(input) {
    
            return input.toLowerCase().replace(/-(.)/g, (match, capture) => {
                return capture.toUpperCase();
            });
    
        }
    
        function capitalize(input) {
    
            return input.toLowerCase().replace(/-/, " ").replace(/\b\w/g, (match) => {
                return match.toUpperCase();
            });
    
        }
    
        return {
            init: init
        }
    
    })();
    
  • URL: /components/raw/filterpanel/_filterPanel.script.js
  • Filesystem Path: components/03-fragments/filterPanel/_filterPanel.script.js
  • Size: 41.3 KB
  • Content:
    @import "_filterPanel.settings";
    @import "_filterPanel.styles";
    
    .filterPanel {
    
        .pageHeader + &,
        .pageHeader + .topicFinder & {
            @include stack-spacing(0);
    
            .filterPanel-filters {
                padding-bottom: var(--sp-component);
            }
    
        }
    
        .pageHeader[data-theme="dark"]:has(+ &),
        .pageHeader[data-theme="dark"]:has(+ .topicFinder &) {
            padding-bottom: 0;
    
            &::before {
                border-bottom: none;
            }
    
        }
    
        .pageHeader[data-theme="dark"]:not(.is-narrow) + & &-filters,
        .pageHeader[data-theme="dark"]:not(.is-narrow) + .topicFinder &-filters {
            justify-content: flex-start;
        }
    
        .pageHeader[data-theme="dark"] + & &-filters {
    
            &::before {
                border-bottom: var(--bw-large) solid $_accent-color;
                height: calc(100% + var(--bw-large));
            }
    
        }
    
    }
    
    .teaserSection {
    
        &-subsection:has(&-item[data-filtered-out]) {
            display: none;
        }
    
        &-subsection:has(&-item[data-filtered-out="false"]) {
            display: block;
        }
    
    }
    
  • URL: /components/raw/filterpanel/_filterPanel.scss
  • Filesystem Path: components/03-fragments/filterPanel/_filterPanel.scss
  • Size: 1.1 KB
  • Content:
    $filterPanel_separator_icon: '<svg width="32" height="32" viewBox="0 0 32 32" fill="none" stroke="{{color}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M16 8L24 16M24 16L16 24M24 16H8"/></svg>' !default;
    
  • URL: /components/raw/filterpanel/_filterPanel.settings.scss
  • Filesystem Path: components/03-fragments/filterPanel/_filterPanel.settings.scss
  • Size: 269 Bytes
  • Content:
    .filterPanel {
        $label_indent: .33em;
    
        @include stack-spacing(component);
    
        $gap: calc(var(--sp-component) - #{nth($field_padding, 1)});
    
        display: flex;
        flex-wrap: wrap;
    
        position: relative;
        @include z-index(filterPanel);
    
        & ~ & {
            z-index: unset;
        }
    
        form {
            width: 100%;
        }
    
        .dropdownMenu {
            @include stack-spacing(0);
    
            flex-direction: column;
            row-gap: $field_stack-spacing;
            align-items: stretch;
        }
    
        .formCombobox {
            @include stack-spacing(0);
        }
    
       .label {
            margin-top: (-1 * $label_indent) !important;
        }
    
        .searchField,
        .formFieldCombo {
            @include stack-spacing(0);
        }
    
        .tagGroup {
            @include stack-spacing(0);
        }
    
        &-filters {
            flex: 1 0 auto;
            width: 100%;
    
            padding-top: var(--sp-large);
            padding-bottom: var(--sp-large);
    
            @extend %dark-theme;
            @include full-width-backdrop();
    
            &:not(:has(*)) {
                display: none;
            }
    
            &:has(.filterPanel-filter:first-child .filterPanel-filter-label),
            &:has(.filterPanel-filter:first-child .label) {
                padding-top: calc(var(--sp-large) - #{$label_indent});
            }
    
            .dropdownMenu {
                width: 100%;
            }
    
        }
    
        &-filter {
    
            &-label {
                @extend %label;
                margin-top: -.3em;
            }
    
            &-label + &-tags {
                margin-top: .6rem;
            }
    
            &[data-hide-unmatched="true"] {
    
                .combobox-item[aria-disabled="true"] {
                    display: none;
                }
    
                // Hide empty groups
                .combobox-group:not(:has(.combobox-item:not([aria-disabled="true"]))) {
                    display: none;
                }
    
            }
    
        }
    
    
        &-filters[data-cascading="true"] &-filter:has(.dropdownMenu, .formCombobox):has(+ &-filter) {
            position: relative;
    
            &::after {
                $color: get-theme-property(dark, accessible-color);
    
                content: "\00a0";
    
                display: block;
                padding: nth($field_padding, 1) 0;
    
                width: var(--gg);
    
                position: absolute;
                left: 100%;
                bottom: 0;
    
                font-size: $field_font-size;
                line-height: $field_line-height;
    
                background: svg-url($filterPanel_separator_icon, $color) center center no-repeat;
            }
    
        }
    
        &-filters[data-cascading="true"] .combobox-item[aria-disabled="true"] {
            display: none;
        }
    
        &-divider {
            display: block;
    
            width: 100%;
            height: auto;
    
            align-self: center;
            text-align: center;
    
            border: none;
    
            position: relative;
            z-index: 1;
    
            &::before {
                content: "";
    
                display: block;
                width: 100%;
                height: var(--bw);
    
                position: absolute;
                top: 50%;
                left: 50%;
                z-index: -1;
    
                transform: translate(-50%, -50%);
    
                background-color: $_border-color;
            }
    
            &::after {
                content: attr(data-label);
    
                @include styles($label_styles);
                font-weight: $_font-weight;
                display: inline-block;
    
                padding: 0 .5em;
    
                background: var(--background-color);
            }
    
        }
    
        &-status {
            margin-top: $gap;
            flex: 1 1 auto;
            align-self: baseline;
        }
    
        &-sorting {
            margin-top: $gap;
            margin-bottom: (-1 * nth($field_padding, 1));
    
            margin-top: $gap;
            justify-self: end;
            align-self: baseline;
    
            flex: 1 0 auto;
    
            .dropdownMenu {
                margin-left: auto;
            }
    
            label {
                @extend %visually-hidden;
            }
    
            select {
                text-align: right;
            }
    
        }
    
        &-filters:not(:has(> *)) ~ &-status,
        &-filters:not(:has(> *)) ~ &-sorting {
            margin-top: 0;
        }
    
        &-placeholder {
            @include apply-theme(warning);
    
            padding: var(--bp-small) var(--bp);
            @include border-radius(default, (border-top-left-radius, border-top-right-radius));
    
            width: max-content;
            margin-inline: auto;
    
            background-color: var(--backdrop-color, #{$_backdrop-color});
            border-bottom: solid var(--bw-large) var(--border-color, #{$_accent-color});
    
            @include font-size(small);
    
            &:not([data-visible="true"]) {
                display: none;
            }
    
        }
    
        @include only-on-desktop(){
    
            &-filters {
                display: flex;
                column-gap: var(--gg);
                row-gap: calc(var(--bp) + #{strip-unit($label_indent)} * var(--fs-small));
                justify-content: center;
    
                flex-wrap: wrap;
    
                &:has(.tagGroup) {
    
                    &:has(.filterPanel-filter:not(:only-child)) {
                        display: grid;
                        column-gap: var(--gg);
                        grid-template-columns: 1fr 1fr;
                    }
    
                    .filterPanel-filter {
                        flex: 1;
                        width: 100%;
    
                        &:has(.tagGroup):not(:has(+ .filterPanel-filter.has-taggroup)) {
                            grid-column: span 2;
                        }
    
                        &:has(.tagGroup) + .filterPanel-filter.has-taggroup {
                            grid-column: span 1;
                        }
    
                    }
    
                }
    
            }
    
            &-filters > * {
                flex: 0 0 get-columns-width(4);
            }
    
            &-search {
                flex-basis: 100% !important;
    
                .searchField,
                .formFieldCombo {
                    width: get-columns-width(8);
                    margin-left: auto;
                    margin-right: auto;
                }
    
            }
    
            &-divider {
                flex-basis: 100% !important;
                margin: calc(-.5 * var(--bp)) 0;
    
                &::before {
                    width: get-columns-width(8);
                    margin-left: auto;
                    margin-right: auto;
                }
    
            }
    
            //** Side-by-side layout for search with single filter **//
    
            &-filters:has(&-divider + &-filter:last-child) {
                column-gap: 0;
            }
    
            &-search:has(+ &-divider + &-filter:last-child) {
                flex: 0 0 get-columns-width(4) !important;
    
                .searchField,
                .formFieldCombo {
                    width: 100%;
                }
    
            }
    
            &-divider:has(+ &-filter:last-child) {
                $margin: .25ch;
    
                flex: 0 1 auto !important;
                //box-sizing: content-box;
                width: var(--gg);
                min-width: max-content;
                margin: 0;
    
                padding-inline: $margin;
    
                position: relative;
                bottom: list.nth($field_border-width, 3);
    
                align-self: flex-end;
    
                display: flex;
                align-items: center;
    
                &::before {
                    position: static;
                    transform: none;
                    background: transparent;
                    height: auto;
                    margin: 0;
    
                    content: "\00a0";
    
                    padding: nth($field_padding, 1) 0;
                    width: 0;
                    overflow: hidden;
    
                    font-size: $field_font-size;
                    line-height: $field_line-height;
    
                }
    
                &::after {
                    width: 100%;
                }
    
            }
    
        }
    
        @include not-on-desktop(){
    
            &-filters {
    
                > * + * {
                    margin-top: var(--bp);
                }
    
                &:has(.tagGroup):has(.filterPanel-filter:not(:only-child)) {
                    row-gap: 0;
                }
    
            }
    
            &-filters[data-cascading="true"] {
                display: block;
            }
    
            &-filters[data-cascading="true"] &-filter {
    
                &[data-disabled="true"] {
                    display: none;
                }
    
                &:not(:last-child) {
    
                    &::after {
                        content: none !important;
                    }
    
                }
    
            }
    
            &-divider + &-filter {
    
                .label {
                    margin-top: 0 !important;
                }
    
            }
    
        }
    
    }
    
    [data-filtered-out="true"] {
        display: none !important;
    }
    
    [filterpanel-cloak] {
        display: none;
    }
    
  • URL: /components/raw/filterpanel/_filterPanel.styles.scss
  • Filesystem Path: components/03-fragments/filterPanel/_filterPanel.styles.scss
  • Size: 8.3 KB