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.