PageSourceSearch

https://www.ballerina.de/functions.js?fqnames=js=/layout/layout667…/_lp/ballerina-isotop-lang&v=01102026164957

js ballerina.de collected 2026-10-05 23:12:50 UTC 25,115 bytes, 758 lines download raw bytes

1
2
3const IsotopeManager = {
4  defaults: {
5    gridSelector: "#q0_isotope_item_wrapper_template",
6    filterSelector: "#q0_isotope_filter_template",
7    itemSelector: ".filter-item",
8    statusSelector: "#q0_isotope_status_message",
9    statusLanguageformat: {
10      allItems: "Alle Einträge",
11      foundItems: "{count} gefundene Einträge unter: {filters}",
12      foundItem: "{count} gefundener Eintrag unter: {filters}",
13      filterSeperator: ", ",
14      groupSeperator: " & ",
15    },
16    layoutMode: "fitRows",
17    urlSync: {
18      enabled: true, // Filter beim Laden aus der URL übernehmen
19      writeToUrl: true, // URL bei jeder Filteränderung aktualisieren (teilbarer Link)
20      mode: "query", // "query" => ?filter=a,b  |  "hash" => #filter=a,b
21      filterParam: "filter", // Klassennamen ohne Punkt, getrennt durch separator
22      searchParam: "search", // Freitext der PLZ-Suche
23      separator: ",",
24    },
25  },
26  customSettings: {},
27  settings: {},
28  $grid: null,
29  observer: null,
30  isInitialized: false,
31
32  configure(options) {
33    const newSettings =
34      options && options.settings ? options.settings : options;
35    this.customSettings = $.extend(true, {}, this.customSettings, newSettings);
36  },
37
38  init() {
39    if (this.isInitialized) return;
40    // Deep extend, damit Teil-Overrides (z. B. nur urlSync.mode) die Defaults nicht wegwerfen
41    this.settings = $.extend(true, {}, this.defaults, this.customSettings);
42    if ($(this.settings.gridSelector).length === 0) return;
43
44    this.generateFiltersFromGrid();
45    this.initializeGrid();
46    this.setupFilterHandlers();
47    this.setupSearchAutocomplete();
48    this.setupScrollObserver();
49    this.addBase();
50    this.adjustEqualHeights();
51    this.setupUrlSync(); // muss nach generateFiltersFromGrid laufen (PLZ-Buttons existieren erst dann)
52    this.isInitialized = true;
53  },
54
55  generateFiltersFromGrid() {
56    const gridElement = document.querySelector(this.settings.gridSelector);
57    if (!gridElement) return;
58
59    const mainCategoryButtons = document.querySelectorAll(
60      ".main-category-wrapper .categorys button[data-filter]",
61    );
62    const mainCategoryClasses = Array.from(mainCategoryButtons)
63      .map((btn) => {
64        const filterValue = btn.getAttribute("data-filter");
65        return filterValue ? filterValue.replace(".", "") : "";
66      })
67      .filter(Boolean);
68
69    const teasers = gridElement.querySelectorAll(this.settings.itemSelector);
70    const uniqueClasses = new Set();
71    const excludedClasses = [
72      "teaserQ-a11y",
73      "teaserQ",
74      "isotope-item",
75      ...mainCategoryClasses,
76    ];
77
78    teasers.forEach((teaser) => {
79      teaser.classList.forEach((className) => {
80        if (!excludedClasses.includes(className)) {
81          uniqueClasses.add(className);
82        }
83      });
84    });
85
86    const filterBox = document.getElementById("IsotopeCombineFilter");
87    if (filterBox) {
88      let buttonsHtml = "";
89      uniqueClasses.forEach((className) => {
90        if (
91          className.startsWith("postal-code-") &&
92          !filterBox.querySelector(`[data-filter=".${className}"]`)
93        ) {
94          const postalNumber = className.replace("postal-code-", "");
95          buttonsHtml += `<button class="btn btn-xs" data-filter=".${className}" type="button">${postalNumber}</button>\n`;
96        }
97      });
98      if (buttonsHtml) {
99        filterBox.insertAdjacentHTML("beforeend", buttonsHtml);
100      }
101    } else {
102      console.warn("IsotopeCombineFilter not found on the page.");
103    }
104  },
105
106  setupSearchAutocomplete() {
107    const $searchInput = $("#isotopeSearchFilter");
108    if (!$searchInput.length) return;
109    $searchInput.wrap(
110      '<div class="autocomplete-wrapper" style="position:relative; width:100%;"></div>',
111    );
112    const $suggestionsBox = $(
113      '<div id="q0_isotope_search_suggestions" role="listbox" aria-label="Postleitzahl Suchvorschläge"></div>',
114    );
115    const existingRelation =
116      $searchInput.attr("data-relation") || "combinesearch";
117
118    $searchInput.attr({
119      role: "combobox",
120      "aria-autocomplete": "list",
121      "aria-expanded": "false",
122      "aria-controls": "q0_isotope_search_suggestions",
123      "data-relation": existingRelation,
124    });
125
126    $searchInput.after($suggestionsBox);
127
128    $searchInput.on("input", (e) => {
129      const val = $(e.target).val().toLowerCase().trim();
130      $suggestionsBox.empty().hide();
131      $searchInput.attr("aria-expanded", "false");
132      if (!val) {
133        if ($searchInput.hasClass("active")) {
134          $searchInput.removeAttr("data-filter");
135          $searchInput.removeClass("active");
136          this.handleFilterChange($searchInput[0]);
137        }
138        return;
139      }
140      const availableFilters = [];
141      $("#IsotopeCombineFilter button").each(function () {
142        const text = $(this).text().trim();
143        const dataFilter = $(this).attr("data-filter") || "";
144        if (
145          dataFilter !== "*" &&
146          dataFilter !== "clear" &&
147          dataFilter.includes("postal-code-")
148        ) {
149          availableFilters.push(text);
150        }
151      });
152      const matches = availableFilters.filter((f) =>
153        f.toLowerCase().startsWith(val),
154      );
155      if (matches.length > 0) {
156        matches.forEach((match, index) => {
157          const $item = $(
158            `<button type="button" class="suggestion-item" tabindex="0" role="option" id="suggestion-item-${index}">${match}</button>`,
159          );
160          $item.on("click", (ev) => {
161            ev.preventDefault();
162            this.triggerSearchSelection(match, $searchInput, $suggestionsBox);
163          });
164          $suggestionsBox.append($item);
165        });
166        $suggestionsBox.show();
167        $searchInput.attr("aria-expanded", "true");
168      }
169    });
170
171    $searchInput.on("keydown", (e) => {
172      const $items = $suggestionsBox.find(".suggestion-item");
173      const $activeItem = $suggestionsBox.find(".suggestion-item.is-focused");
174      if (e.key === "Enter") {
175        e.preventDefault();
176
177        if ($activeItem.length) {
178          $activeItem.click();
179          return;
180        }
181
182        $suggestionsBox.hide();
183        $searchInput.attr("aria-expanded", "false");
184        // Gleiche Logik wie beim URL-Restore, inkl. Bereinigung der Eingabe
185        this.setSearchFilter($searchInput.val());
186        this.handleFilterChange($searchInput[0]);
187      } else if (e.key === "ArrowDown") {
188        e.preventDefault();
189        if ($items.length) {
190          $items.removeClass("is-focused").attr("aria-selected", "false");
191          let $next = !$activeItem.length
192            ? $items.first()
193            : $activeItem.next(".suggestion-item").length
194              ? $activeItem.next(".suggestion-item")
195              : $items.first();
196          $next.addClass("is-focused").attr("aria-selected", "true");
197          $searchInput.attr("aria-activedescendant", $next.attr("id"));
198        }
199      } else if (e.key === "ArrowUp") {
200        e.preventDefault();
201        if ($items.length) {
202          $items.removeClass("is-focused").attr("aria-selected", "false");
203          let $prev = !$activeItem.length
204            ? $items.last()
205            : $activeItem.prev(".suggestion-item").length
206              ? $activeItem.prev(".suggestion-item")
207              : $items.last();
208          $prev.addClass("is-focused").attr("aria-selected", "true");
209          $searchInput.attr("aria-activedescendant", $prev.attr("id"));
210        }
211      }
212    });
213
214    $suggestionsBox.on("keydown", ".suggestion-item", (e) => {
215      if (e.key === "ArrowDown") {
216        e.preventDefault();
217        $(e.target).next(".suggestion-item").focus();
218      } else if (e.key === "ArrowUp") {
219        e.preventDefault();
220        $(e.target).prev(".suggestion-item").focus();
221      }
222    });
223
224    $(document).on("click", (e) => {
225      if (!$(e.target).closest(".autocomplete-wrapper").length) {
226        $suggestionsBox.hide();
227        $searchInput.attr("aria-expanded", "false");
228      }
229    });
230
231    $searchInput.parent().on("focusout", function (e) {
232      const wrapper = this;
233      setTimeout(() => {
234        if (!wrapper.contains(document.activeElement)) {
235          $suggestionsBox.hide();
236          $searchInput.attr("aria-expanded", "false");
237        }
238      }, 10);
239    });
240  },
241
242  triggerSearchSelection(term, $input, $box) {
243    $input.val(term);
244    $box.empty().hide();
245    $input.attr("aria-expanded", "false");
246    $input.removeAttr("aria-activedescendant");
247
248    $input.attr("data-filter", `.postal-code-${term}`);
249    $input.addClass("active");
250
251    $("#IsotopeCombineFilter button.active").removeClass("active");
252
253    this.handleFilterChange($input[0]);
254  },
255
256  /**
257   * Setzt den Filterzustand der PLZ-Suche (ohne Isotope auszuführen).
258   * Wird von Enter-Taste und URL-Restore gemeinsam genutzt.
259   */
260  setSearchFilter(rawValue) {
261    const $searchInput = $("#isotopeSearchFilter");
262    if (!$searchInput.length) return false;
263
264    const term = this.sanitizeSearchTerm(rawValue);
265    if (!term) {
266      $searchInput.removeAttr("data-filter").removeClass("active");
267      return false;
268    }
269
270    const val = term.toLowerCase();
271    const $exactMatch = $("#IsotopeCombineFilter button")
272      .filter(
273        (_, el) =>
274          (el.getAttribute("data-filter") || "").toLowerCase() ===
275          `.postal-code-${val}`,
276      )
277      .first();
278
279    $searchInput
280      .val(term)
281      .attr(
282        "data-filter",
283        $exactMatch.length
284          ? $exactMatch.attr("data-filter")
285          : `[class*="postal-code-${val}"]`,
286      )
287      .addClass("active");
288
289    $("#IsotopeCombineFilter button.active").removeClass("active");
290    return true;
291  },
292
293  initializeGrid() {
294    this.$grid = $(this.settings.gridSelector).isotope({
295      itemSelector: this.settings.itemSelector,
296      layoutMode: this.settings.layoutMode,
297    });
298  },
299
300  setupFilterHandlers() {
301    const $container = $(this.settings.filterSelector);
302    $container.on("change click", "select, button", (e) => {
303      if ($(e.currentTarget).hasClass("suggestion-item")) return;
304      this.handleFilterChange(e.currentTarget);
305    });
306  },
307
308  handleFilterChange(triggerElement) {
309    const $trigger = $(triggerElement);
310    if (
311      $trigger.is('[data-filter="clear"]') ||
312      ($trigger.is("select") &&
313        $trigger.find("option:selected").attr("data-filter") === "clear")
314    ) {
315      this.resetAllFilters();
316      return;
317    }
318    this.updateActiveStates($trigger);
319    const combinedFilters = this.calculateCombinedFilters();
320    this.applyFilters(combinedFilters);
321    this.writeUrlState();
322  },
323
324  calculateCombinedFilters() {
325    let filters = new Set();
326    const $container = $(this.settings.filterSelector);
327
328    const $activeElements = $container
329      .find(".active")
330      .add("#isotopeSearchFilter.active");
331    const relationGroups = {};
332    const locationFilters = [];
333
334    $activeElements.each((_, el) => {
335      const $el = $(el);
336      const val = $el.attr("data-filter");
337      if (!val) return;
338
339      // PLZ (Suchfeld + PLZ-Buttons) ist eine Einschränkung für ALLE übrigen Filter,
340      // keine weitere ODER-Option. Wird unten per Kreuzprodukt angehängt.
341      if (
342        $el.is("#isotopeSearchFilter") ||
343        $el.closest("#IsotopeCombineFilter").length
344      ) {
345        if (val !== "*" && val !== "**" && !locationFilters.includes(val)) {
346          locationFilters.push(val);
347        }
348        return;
349      }
350
351      const $relationParent = $el.closest("[data-relation]");
352      const key = $relationParent.length
353        ? $relationParent.attr("data-relation")
354        : $el.attr("data-relation");
355
356      if (key) {
357        relationGroups[key] = relationGroups[key] || [];
358        relationGroups[key].push(val);
359      } else {
360        filters.add(val);
361      }
362    });
363
364    for (const key in relationGroups) {
365      filters.add(relationGroups[key].join(""));
366    }
367
368    if (!locationFilters.length) return filters;
369
370    // (A, B) UND (PLZ1, PLZ2)  =>  A.PLZ1, A.PLZ2, B.PLZ1, B.PLZ2
371    const baseFilters = Array.from(filters).filter(
372      (f) => f !== "*" && f !== "**",
373    );
374    if (!baseFilters.length) return new Set(locationFilters);
375
376    const combined = new Set();
377    baseFilters.forEach((base) => {
378      locationFilters.forEach((location) => combined.add(base + location));
379    });
380    return combined;
381  },
382
383  applyFilters(filtersSet) {
384    if (filtersSet.size > 0) {
385      filtersSet.delete("*");
386      filtersSet.delete("**");
387    }
388    if (filtersSet.size === 0) {
389      filtersSet.add("*");
390    }
391    const filterString = Array.from(filtersSet).join(", ");
392    this.$grid.isotope({ filter: filterString });
393    const count = this.$grid.data("isotope").filteredItems.length;
394    this.updateStatusMessage(count, filtersSet);
395  },
396
397  /* ------------------------------------------------------------------
398   * URL-Sync
399   * Format:  seite.html?filter=kategorie-a,postal-code-48149&search=481
400   * Werte sind die data-filter-Klassen ohne Punkt. Die Buttons/Options
401   * werden aktiv gesetzt und laufen dann durch die normale Pipeline
402   * (Relations, Statusmeldung, aria-live bleiben dadurch konsistent).
403   * ------------------------------------------------------------------ */
404
405  setupUrlSync() {
406    const cfg = this.settings.urlSync;
407    if (!cfg || !cfg.enabled) return;
408
409    this.applyFiltersFromUrl();
410
411    // Für Links innerhalb derselben Seite (#filter=…). replaceState löst kein hashchange aus,
412    // daher entsteht hier keine Schleife mit writeUrlState.
413    $(window)
414      .off("hashchange.isotopeUrl")
415      .on("hashchange.isotopeUrl", () => {
416        this.applyFiltersFromUrl();
417      });
418  },
419
420  applyFiltersFromUrl() {
421    const cfg = this.settings.urlSync;
422    const state = this.readUrlState();
423
424    const tokens = state.filter
425      .split(cfg.separator)
426      .map((token) => this.sanitizeToken(token))
427      .filter(Boolean);
428    const searchTerm = this.sanitizeSearchTerm(state.search);
429
430    // Keine Filter in der URL (z. B. normale Sprungmarke) => aktuellen Zustand nicht anfassen
431    if (!tokens.length && !searchTerm) return false;
432
433    this.clearActiveStates();
434
435    tokens.forEach((token) => {
436      const $element = this.findFilterElement(token);
437      if (!$element.length) {
438        console.warn(
439          `IsotopeManager: Kein Filter-Element für "${token}" gefunden.`,
440        );
441        return;
442      }
443      if ($element.is("option")) {
444        $element.closest("select").find("option").removeClass("active");
445        $element.prop("selected", true).addClass("active");
446      } else {
447        $element.addClass("active");
448      }
449    });
450
451    if (searchTerm) {
452      this.setSearchFilter(searchTerm);
453    }
454
455    this.applyFilters(this.calculateCombinedFilters());
456    return true;
457  },
458
459  writeUrlState() {
460    const cfg = this.settings.urlSync;
461    if (
462      !cfg ||
463      !cfg.enabled ||
464      !cfg.writeToUrl ||
465      !window.history ||
466      !window.history.replaceState
467    )
468      return;
469
470    const tokens = [];
471    $(this.settings.filterSelector)
472      .find("button.active, option.active")
473      .each((_, el) => {
474        const token = this.sanitizeToken(el.getAttribute("data-filter"));
475        if (token && !tokens.includes(token)) tokens.push(token);
476      });
477
478    const $searchInput = $("#isotopeSearchFilter");
479    const searchTerm = $searchInput.hasClass("active")
480      ? this.sanitizeSearchTerm($searchInput.val())
481      : "";
482
483    const url = new URL(window.location.href);
484    const queryParams = new URLSearchParams(url.search);
485    const hashParams = this.getHashParams();
486
487    // Alte Werte an beiden Stellen entfernen (eingehender Link kann das andere Format nutzen)
488    [queryParams, hashParams].forEach((params) => {
489      params.delete(cfg.filterParam);
490      params.delete(cfg.searchParam);
491    });
492
493    const target = cfg.mode === "hash" ? hashParams : queryParams;
494    if (tokens.length) target.set(cfg.filterParam, tokens.join(cfg.separator));
495    if (searchTerm) target.set(cfg.searchParam, searchTerm);
496
497    // Trennzeichen lesbar lassen (?filter=a,b statt ?filter=a%2Cb)
498    const encodedSeparator = encodeURIComponent(cfg.separator);
499    const serialize = (params) =>
500      params.toString().split(encodedSeparator).join(cfg.separator);
501
502    url.search = serialize(queryParams);
503
504    // Normale Sprungmarken (#kontakt) nur überschreiben, wenn wirklich Filter in den Hash sollen
505    const hashHadParams = window.location.hash.includes("=");
506    if (hashHadParams || (cfg.mode === "hash" && hashParams.toString())) {
507      url.hash = serialize(hashParams);
508    }
509
510    if (url.href !== window.location.href) {
511      window.history.replaceState(window.history.state, "", url.href);
512    }
513  },
514
515  readUrlState() {
516    const cfg = this.settings.urlSync;
517    const queryParams = new URLSearchParams(window.location.search);
518    const hashParams = this.getHashParams();
519    const read = (name) => queryParams.get(name) || hashParams.get(name) || "";
520
521    return {
522      filter: read(cfg.filterParam),
523      search: read(cfg.searchParam),
524    };
525  },
526
527  getHashParams() {
528    const hash = window.location.hash.replace(/^#/, "");
529    return new URLSearchParams(hash.includes("=") ? hash : "");
530  },
531
532  findFilterElement(token) {
533    // Vergleich per Attributwert statt Selektor-String => keine Selector-Injection aus der URL
534    const target = `.${token}`;
535    return $(this.settings.filterSelector)
536      .find("button[data-filter], option[data-filter]")
537      .filter((_, el) => el.getAttribute("data-filter") === target)
538      .first();
539  },
540
541  sanitizeToken(value) {
542    const token = String(value || "")
543      .trim()
544      .replace(/^\./, "");
545    return /^[\p{L}\p{N}_.-]+$/u.test(token) ? token : null;
546  },
547
548  sanitizeSearchTerm(value) {
549    // Alles außer Buchstaben, Ziffern, _ und - raus, sonst kann z. B. ein " den
550    // [class*="…"]-Selektor zerschießen (per URL wäre das von außen auslösbar)
551    return String(value || "")
552      .trim()
553      .replace(/[^\p{L}\p{N}_-]/gu, "");
554  },
555
556  /* ------------------------------------------------------------------ */
557
558  setupScrollObserver() {
559    this.observer = new IntersectionObserver(
560      (entries) => {
561        entries.forEach((entry) => {
562          if (entry.isIntersecting) this.adjustEqualHeights();
563        });
564      },
565      { threshold: 0.1 },
566    );
567    document.querySelectorAll(this.settings.itemSelector).forEach((item) => {
568      this.observer.observe(item);
569    });
570  },
571
572  adjustEqualHeights() {
573    const $items = this.$grid.find(this.settings.itemSelector + ":visible");
574    if (!$items.length) return;
575    $items.css({ "min-height": "", height: "auto" });
576    let maxHeight = 0;
577    $items.each((_, el) => {
578      maxHeight = Math.max(maxHeight, $(el).outerHeight());
579    });
580    if (maxHeight > 0) {
581      $items.css("min-height", maxHeight + "px");
582    }
583    this.$grid.isotope("layout");
584    $("img").trigger("invalidate.ieQ.image");
585  },
586
587  addBase() {
588    const $filter = $(this.settings.filterSelector);
589    const $grid = $(this.settings.gridSelector);
590    const $items = $(this.settings.itemSelector);
591    $filter.attr({
592      "aria-label": "Filter",
593      role: "region",
594    });
595
596    $grid.css("margin-inline", "");
597    $items.css({
598      "margin-block": "",
599      "margin-inline": "",
600    });
601    if (this.settings.itemSelector === ".teaserQ") {
602      $grid.css("margin-inline", "-1rem");
603      $items.css({
604        "margin-block": "1rem",
605        "margin-inline": "1rem",
606      });
607    }
608  },
609
610  updateStatusMessage(count, activeFilters) {
611    const filternames = [];
612    const $container = $(this.settings.filterSelector);
613    const lang = this.settings.statusLanguageformat;
614
615    Array.from(activeFilters).forEach((item) => {
616      if (item === "*" || item === "**") {
617        filternames.push(lang.allItems);
618        return;
619      }
620
621      let currentItem = item;
622      let componentNames = [];
623      let wildcardLabel = null;
624      //if Template update follows postal-code will get removed and filter values will be placed directly inside  search suggestions
625      const wildcardMatch = currentItem.match(
626        /\[class\*="postal-code-([^"]+)"\]/,
627      );
628      if (wildcardMatch && wildcardMatch[1]) {
629        const postalLabel = wildcardMatch[1]; //lang.postalcode.replace('{code}', wildcardMatch[1]); *ATS: no need for a search prefix
630        wildcardLabel = postalLabel; // PLZ ans Ende, wie bei exakter PLZ
631        currentItem = currentItem.replace(
632          /\[class\*="postal-code-[^"]+"\]/g,
633          "",
634        );
635      }
636
637      const cleanComponents = currentItem
638        .replace(/\*/g, "")
639        .split(".")
640        .filter(Boolean);
641
642      cleanComponents.forEach((component) => {
643        try {
644          const $foundElements = $container.find(
645            `[data-filter=".${component}"]`,
646          );
647
648          if ($foundElements.length) {
649            const $textElement = $foundElements.not("input").first();
650            if ($textElement.length && $textElement.text().trim()) {
651              componentNames.push($textElement.text().trim());
652              return;
653            }
654
655            const $inputElement = $foundElements.filter("input").first();
656            if ($inputElement.length && $inputElement.val().trim()) {
657              componentNames.push($inputElement.val().trim());
658              return;
659            }
660          }
661
662          if (component.startsWith("postal-code-")) {
663            const code = component.replace("postal-code-", "");
664            componentNames.push(code); //search prefix removed
665            return;
666          }
667
668          componentNames.push(component);
669        } catch (e) {
670          componentNames.push(component);
671        }
672      });
673
674      if (wildcardLabel) componentNames.push(wildcardLabel);
675
676      if (componentNames.length > 1) {
677        filternames.push(componentNames.join(lang.groupSeperator));
678      } else if (componentNames.length === 1) {
679        filternames.push(componentNames[0]);
680      }
681    });
682    let $statusElement = $(this.settings.statusSelector);
683    if (!$statusElement.length) {
684      $statusElement = $(
685        '<span id="q0_isotope_status_message" class="text-danger"></span>',
686      );
687      $(this.settings.filterSelector).append($statusElement);
688    }
689    $statusElement.attr({
690      role: "status",
691      "aria-live": "polite",
692      "aria-atomic": "true", //origanly 'additions text'  (because browsers would ignore 'aria-atomic': 'true' in the past)
693    });
694    const template = count === 1 ? lang.foundItem : lang.foundItems;
695    // filterSeperator enthält bereits das Leerzeichen (vorher entstand ",  " mit Doppel-Space)
696    const filterString =
697      filternames.filter(Boolean).join(lang.filterSeperator) || lang.allItems;
698    const message = template
699      .replace("{count}", count)
700      .replace("{filters}", filterString);
701
702    $statusElement.text(message);
703  },
704
705  clearActiveStates() {
706    const $container = $(this.settings.filterSelector);
707    $container.find("select").prop("selectedIndex", 0);
708    $container.find("option, button").removeClass("active");
709
710    const $searchInput = $("#isotopeSearchFilter");
711    if ($searchInput.length) {
712      $searchInput.val("").removeAttr("data-filter").removeClass("active");
713    }
714  },
715
716  resetAllFilters() {
717    this.clearActiveStates();
718    this.applyFilters(new Set(["*"]));
719    this.writeUrlState();
720  },
721
722  updateActiveStates($trigger) {
723    if ($trigger.is("button")) {
724      $trigger.toggleClass("active");
725
726      if ($trigger.closest("#IsotopeCombineFilter").length) {
727        const $searchInput = $("#isotopeSearchFilter");
728        if ($searchInput.length && $searchInput.val() !== "") {
729          $searchInput.val("").removeAttr("data-filter").removeClass("active");
730        }
731      }
732    } else if ($trigger.is("input")) {
733    } else {
734      $trigger.find("option").removeClass("active");
735      $trigger.find("option:selected").addClass("active");
736    }
737  },
738};
739
740$(document).on("ajaxStop", function () {
741  IsotopeManager.init();
742});
743
744
745
746 IsotopeManager.configure({
747     settings: {
748        statusLanguageformat: {
749            allItems: "Alle einträge",
750            foundItems: "{count} gefundene Einträge unter: {filters}",
751            foundItem: "{count} gefundener Eintrage unter: {filters}",
752            filterSeperator:", ",
753            groupSeperator:" & "
754        }
755    },
756});
757
758

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.