PageSourceSearch

https://firmendo-test.netlify.app/site-nav.js

js firmendo-test.netlify.app collected 2026-10-03 10:44:10 UTC 11,659 bytes, 275 lines download raw bytes

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.