1(() => { 2 const shellAssetBase = (() => { 3 const script = document.currentScript || document.querySelector('script[src$="site-nav.js"]'); 4 return script ? new URL("./", script.src) : new URL("./", window.location.href); 5 })(); 6 7 function domReady() { 8 if (document.readyState === "loading") { 9 return new Promise((resolve) => { 10 document.addEventListener("DOMContentLoaded", resolve, { once: true }); 11 }); 12 } 13 14 return Promise.resolve(); 15 } 16 17 async function loadSharedNav() { 18 if (window.location.protocol === "file:") return false; 19 20 await domReady(); 21 22 const currentNav = document.getElementById("site-nav"); 23 const currentOverlay = document.getElementById("sn-search-overlay"); 24 if (currentNav && currentOverlay) return false; 25 26 try { 27 const response = await fetch("/snippets/nav.html", { cache: "no-cache" }); 28 if (!response.ok) return false; 29 30 const html = await response.text(); 31 const template = document.createElement("template"); 32 template.innerHTML = html.trim(); 33 34 const nextNav = template.content.querySelector("#site-nav"); 35 const nextOverlay = template.content.querySelector("#sn-search-overlay"); 36 if (!nextNav) return false; 37 38 if (currentNav) { 39 currentNav.replaceWith(nextNav); 40 } else { 41 document.body.prepend(nextNav); 42 } 43 44 if (nextOverlay) { 45 if (currentOverlay) { 46 currentOverlay.replaceWith(nextOverlay); 47 } else { 48 nextNav.insertAdjacentElement("afterend", nextOverlay); 49 } 50 } else if (currentOverlay) { 51 currentOverlay.remove(); 52 } 53 54 return true; 55 } catch (_) { 56 return false; 57 } 58 } 59 60 function initNav() { 61 const nav = document.getElementById("site-nav"); 62 const links = document.getElementById("sn-links"); 63 const burger = document.getElementById("sn-burger"); 64 65 if (!nav || nav.dataset.ready === "true") return; 66 67 if (window.location.protocol === "file:") { 68 nav.querySelectorAll('img[src^="/images/"]').forEach((img) => { 69 img.src = new URL(img.getAttribute("src").slice(1), shellAssetBase).href; 70 }); 71 } 72 73 const syncScrolled = () => { 74 nav.classList.toggle("scrolled", window.scrollY > 20); 75 }; 76 77 syncScrolled(); 78 window.addEventListener("scroll", syncScrolled, { passive: true }); 79 80 if (links && burger) { 81 const closeMenu = () => { 82 links.classList.remove("open"); 83 burger.classList.remove("open"); 84 burger.setAttribute("aria-expanded", "false"); 85 burger.setAttribute("aria-label", "Menü öffnen"); 86 }; 87 88 const toggleMenu = () => { 89 const open = links.classList.toggle("open"); 90 burger.classList.toggle("open", open); 91 burger.setAttribute("aria-expanded", open ? "true" : "false"); 92 burger.setAttribute("aria-label", open ? "Menü schlieÃen" : "Menü öffnen"); 93 }; 94 95 burger.addEventListener("click", toggleMenu); 96 97 links.querySelectorAll("a").forEach((link) => { 98 link.addEventListener("click", closeMenu); 99 }); 100 101 links.querySelectorAll(".sn-submenu-tabs").forEach((submenu) => { 102 const tabs = Array.from(submenu.querySelectorAll(".sn-submenu-tab")); 103 const panels = Array.from(submenu.querySelectorAll(".sn-submenu-panel")); 104 105 const activatePanel = (panelId) => {
106 tabs.forEach((tab) => { 107 const isActive = tab.dataset.panel === panelId; 108 tab.classList.toggle("is-active", isActive); 109 tab.setAttribute("aria-selected", isActive ? "true" : "false"); 110 }); 111 112 panels.forEach((panel) => { 113 const isActive = panel.id === panelId; 114 panel.classList.toggle("is-active", isActive); 115 panel.hidden = !isActive; 116 }); 117 }; 118 119 tabs.forEach((tab) => { 120 const activate = () => activatePanel(tab.dataset.panel); 121 tab.addEventListener("mouseenter", activate); 122 tab.addEventListener("focus", activate); 123 tab.addEventListener("click", activate); 124 }); 125 126 if (tabs[0]?.dataset.panel) { 127 activatePanel(tabs[0].dataset.panel); 128 } 129 }); 130 131 window.addEventListener("resize", () => { 132 if (window.innerWidth > 700) closeMenu(); 133 }); 134 } 135 136 nav.dataset.ready = "true"; 137 } 138 139 function initSearch() { 140 const trigger = document.getElementById("sn-search-open"); 141 const overlay = document.getElementById("sn-search-overlay"); 142 const closeBtn = document.getElementById("sn-search-close"); 143 const input = document.getElementById("sn-search-input"); 144 const results = document.getElementById("sn-search-results"); 145 146 if (!trigger || !overlay || !closeBtn || !input || !results || overlay.dataset.ready === "true") return; 147 148 const defaultItems = [ 149 { title: "Geschäftskonto Vergleich", subtitle: "Passende Konten nach Rechtsform, Bedarf und Kostenmodell", tag: "Banking", href: "/geschaeftskonto-vergleich/" }, 150 { title: "Kostenloses Geschäftskonto", subtitle: "Kostenfreie Geschäftskonten im Ãberblick", tag: "Vergleich", href: "/geschaeftskonto-kostenlos/" }, 151 { title: "Geschäftskonto ohne Schufa", subtitle: "Optionen für Kontoeröffnung ohne SCHUFA-Prüfung", tag: "Banking", href: "/geschaeftskonto-ohne-schufa/" }, 152 { title: "Girokonto für Selbstständige", subtitle: "Konten für Freiberufler und Einzelunternehmer", tag: "Selbstständige", href: "/girokonto-fuer-selbststaendige-und-freiberufler/" }, 153 { title: "Finom", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/finom-geschaeftskonto/" }, 154 { title: "FYRST", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/fyrst-geschaeftskonto/" }, 155 { title: "Qonto", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/qonto-geschaeftskonto/" }, 156 { title: "Vivid", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/vivid-geschaeftskonto/" }, 157 { title: "Commerzbank", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/commerzbank-geschaeftskonto/", keywords: "comm kommerzbank filialbank klassik premium" }, 158 { title: "Deutsche Bank", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/deutsche-bank-geschaeftskonto/", keywords: "db deutschebank filialbank" }, 159 { title: "Postbank", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/postbank-geschaeftskonto/" }, 160 { title: "DKB", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/dkb-geschaeftskonto/" }, 161 { title: "ING", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/ing-geschaeftskonto/" }, 162 { title: "HypoVereinsbank", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/hypovereinsbank-geschaeftskonto/", keywords: "hvb unicredit hypo vereinsbank" }, 163 { title: "Kontist", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/kontist-geschaeftskonto/" }, 164 { title: "Holvi", subtitle: "Geschäftskonto im Detailtest", tag: "Anbieter", href: "/holvi-geschaeftskonto/" }, 165 { title: "Kredit für Selbstständige", subtitle: "Finanzierungslösungen für Selbstständige", tag: "Kredit", href: "/kredit-fuer-selbststaendige/" }, 166 { title: "Rechnungen schreiben", subtitle: "Leitfaden für Selbstständige", tag: "Ratgeber", href: "/rechnungen-schreiben-selbststaendige/" }, 167 { title: "Krankenversicherung für Selbstständige", subtitle: "Wichtige Grundlagen kompakt erklärt", tag: "Ratgeber", href: "/krankenversicherung-selbststaendige/" }, 168 { title: "5 Finanzfehler für Einzelunternehmer", subtitle: "Typische Stolperfallen vermeiden", tag: "Ratgeber", href: "/5-finanzfehler-einzelunternehmer/" }, 169 { title: "Ratgeber", subtitle: "Alle Ratgeber und Grundlagen im Ãberblick", tag: "Wissen", href: "/ratgeber/" } 170 ]; 171 let items = defaultItems; 172 173 const loadSearchIndex = async () => { 174 if (window.location.protocol === "file:") return; 175
176 try { 177 const response = await fetch(new URL("search-index.json", shellAssetBase), { cache: "no-cache" }); 178 if (!response.ok) return; 179 180 const nextItems = await response.json(); 181 if (!Array.isArray(nextItems) || nextItems.length === 0) return; 182 183 items = nextItems.filter((item) => item.title && item.href); 184 render(input.value); 185 } catch (_) { 186 items = defaultItems; 187 } 188 }; 189 190 const render = (query = "") => { 191 const q = query.trim().toLowerCase(); 192 const filtered = q 193 ? items 194 .map((item) => { 195 const title = String(item.title || "").toLowerCase(); 196 const subtitle = String(item.subtitle || "").toLowerCase(); 197 const tag = String(item.tag || "").toLowerCase(); 198 const href = String(item.href || "").toLowerCase(); 199 const keywords = String(item.keywords || "").toLowerCase(); 200 const haystack = `${title} ${subtitle} ${tag} ${href} ${keywords}`; 201 if (!haystack.includes(q)) return null; 202 203 let score = 0; 204 if (title.startsWith(q)) score += 100; 205 if (title.includes(q)) score += 70; 206 if (href.includes(q)) score += 50; 207 if (subtitle.includes(q)) score += 25; 208 if (tag.includes(q)) score += 15; 209 if (keywords.includes(q)) score += 5; 210 211 return { item, score }; 212 }) 213 .filter(Boolean) 214 .sort((a, b) => b.score - a.score || a.item.title.localeCompare(b.item.title, "de")) 215 .map((entry) => entry.item) 216 : items.slice(0, 8); 217 218 results.innerHTML = filtered.length 219 ? filtered.slice(0, 8).map((item) => ` 220 <a class="sn-search-link" href="${item.href}"> 221 <span class="sn-search-link-main"> 222 <span class="sn-search-link-title">${item.title}</span> 223 <span class="sn-search-link-subtitle">${item.subtitle}</span> 224 </span> 225 <span class="sn-search-link-tag">${item.tag}</span> 226 </a> 227 `).join("") 228 : `<div class="sn-search-link"><span class="sn-search-link-main"><span class="sn-search-link-title">Keine Treffer</span><span class="sn-search-link-subtitle">Versuchen Sie es mit einem Anbieter, Thema oder Produkt.</span></span><span class="sn-search-link-tag">Suche</span></div>`; 229 }; 230 231 const openSearch = () => { 232 overlay.hidden = false; 233 document.body.style.overflow = "hidden"; 234 render(input.value); 235 window.setTimeout(() => input.focus(), 20); 236 }; 237 238 const closeSearch = () => { 239 overlay.hidden = true; 240 document.body.style.overflow = ""; 241 }; 242 243 trigger.addEventListener("click", openSearch); 244 closeBtn.addEventListener("click", closeSearch); 245 overlay.querySelectorAll("[data-close-search]").forEach((node) => { 246 node.addEventListener("click", closeSearch); 247 }); 248 input.addEventListener("input", (event) => render(event.target.value)); 249 results.addEventListener("click", (event) => { 250 if (event.target.closest("a")) closeSearch(); 251 }); 252 document.addEventListener("keydown", (event) => { 253 if (event.key === "Escape" && !overlay.hidden) closeSearch(); 254 }); 255 256 render(); 257 loadSearchIndex(); 258 overlay.dataset.ready = "true"; 259 } 260 261 function init() { 262 initNav(); 263 initSearch(); 264 } 265 266 window.FirmendoSiteShell = { 267 ...(window.FirmendoSiteShell || {}), 268 init, 269 initNav, 270 initSearch, 271 loadSharedNav, 272 }; 273 274 loadSharedNav().finally(init); 275})();
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.