PageSourceSearch

https://homezero.ai/js/hz-mobile-nav.js

js homezero.ai collected 2026-10-05 16:08:49 UTC 16,881 bytes, 309 lines download raw bytes

1/* HomeZero mobile nav.
2 * Vanilla JS, idempotent. Replaces the Webflow hamburger nav on small screens
3 * (≤991px). Works on the static homepage AND on React-rendered pages.
4 *
5 * - Hides the original .sect-nav on mobile via injected CSS.
6 * - Mounts a fixed top bar with logo + language switcher + hamburger.
7 * - Opens a right-side drawer with accordion sections, B2C/B2B2C tabs,
8 *   and Pico + Contact CTAs.
9 * - Language switcher integrates with window.__hzI18n (hz-i18n.js).
10 */
11(function () {
12  if (window.__hzMobileNav) return;
13  window.__hzMobileNav = true;
14
15  var FLAGS = {
16    nl: '<svg viewBox="0 0 30 21" width="22" height="15" style="display:block;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.08)"><rect width="30" height="7" fill="#AE1C28"/><rect y="7" width="30" height="7" fill="#fff"/><rect y="14" width="30" height="7" fill="#21468B"/></svg>',
17    en: '<svg viewBox="0 0 60 30" width="22" height="15" style="display:block;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.08)"><clipPath id="hzt"><path d="M0,0 v30 h60 v-30 z"/></clipPath><path d="M0,0 v30 h60 v-30 z" fill="#012169"/><path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/><path d="M0,0 L60,30 M60,0 L0,30" clip-path="url(#hzt)" stroke="#C8102E" stroke-width="4"/><path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/><path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/></svg>',
18    fr: '<svg viewBox="0 0 30 21" width="22" height="15" style="display:block;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.08)"><rect width="10" height="21" fill="#0055A4"/><rect x="10" width="10" height="21" fill="#fff"/><rect x="20" width="10" height="21" fill="#EF4135"/></svg>',
19    de: '<svg viewBox="0 0 30 21" width="22" height="15" style="display:block;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.08)"><rect width="30" height="7" fill="#000"/><rect y="7" width="30" height="7" fill="#DD0000"/><rect y="14" width="30" height="7" fill="#FFCE00"/></svg>',
20  };
21  var LANGS = ["nl", "en", "de", "fr"];
22
23  var SECTIONS = [
24    {
25      id: "oplossingen", label: "Oplossingen",
26      groups: [
27        { title: "Leads", items: [
28          ["Lead Flows","/#sales"],["Lead Filter","/#sales"],["Rekenmodellen","/#sales"],
29          ["Website widget","/#sales"],["Huisscan","/#sales"],["Workflow","/#sales"],
30        ]},
31        { title: "Sales", items: [
32          ["Sales CRM","/#operations"],["Offerte Module","/#operations"],
33          ["Lead Management","/#operations"],["Sales Automations","/#operations"],
34          ["Adviseurs Planning","/#operations"],["Schouwformulieren","/#operations"],
35        ]},
36        { title: "Ops", items: [
37          ["Planning","/#services"],["Field App","/#services"],["Netwerkbeheer","/#services"],
38        ]},
39      ],
40    },
41    {
42      id: "voor-wie", label: "Voor wie",
43      groups: [
44        { title: "Installateurs & bedrijven", items: [
45          ["Zonnepaneel installateur","/use-cases/zonnepaneel-installateur"],
46          ["Warmtepomp installateur","/use-cases/warmtepomp-installateur"],
47          ["Airco installateur","/use-cases/airco-installateur"],
48          ["Thuisbatterij installateur","/use-cases/thuisbatterij-installateur"],
49          ["Isolatiebedrijf","/use-cases/isolatiebedrijf"],
50          ["Duurzame totaal installateur","/use-cases/duurzame-totaal-installateur"],
51          ["Installatiebedrijven","/use-cases/installatiebedrijven"],
52          ["Adviseurs","/use-cases/adviseurs"],
53        ]},
54        { title: "B2B2C", items: [
55          ["Groothandels","/use-cases/groothandels"],["Fabrikanten","/use-cases/fabrikanten"],
56          ["Vergelijkers","/use-cases/vergelijkers"],["Banken","/use-cases/banken"],
57          ["Verzekeraars","/use-cases/verzekeraars"],["Verenigingen","/use-cases/verenigingen"],
58        ]},
59      ],
60    },
61    {
62      id: "info", label: "Info",
63      groups: [
64        { title: "Bedrijf", items: [
65          ["Over ons","/over-ons"],["Pico","/project-pico"],
66          ["Vacatures","/vacatures"],["Blog","/blog"],
67        ]},
68      ],
69    },
70  ];
71
72  var ICON_BURGER = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>';
73  var ICON_CLOSE = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>';
74  var ICON_CHEV = '<svg class="hz-mnav-chev" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 9l6 6 6-6"/></svg>';
75
76  function injectStyles() {
77    if (document.getElementById("hz-mnav-styles")) return;
78    var css = ''
79      + '.hz-mnav-root{display:none;}'
80      + '@media(max-width:991px){'
81      + '.sect-nav,.hz-nav{display:none!important;}'
82      + 'body{padding-top:64px;}'
83      + '.sect:first-of-type{padding-top:0!important;}'
84      + '.sect:first-of-type .hero-cont{padding-top:28px!important;}'
85      + '.hz-mnav-root{display:block;}'
86      + '.hz-mnav-bar{position:fixed;top:0;left:0;right:0;z-index:9000;height:64px;padding:0 14px;display:flex;align-items:center;justify-content:space-between;gap:10px;background:#fff;border-bottom:1px solid rgba(0,0,0,.06);box-shadow:0 2px 12px rgba(0,0,0,.04);}'
87      + '.hz-mnav-logo{display:inline-flex;align-items:center;flex:none;}'
88      + '.hz-mnav-logo img{height:60px;width:auto;display:block;}'
89      + '.hz-mnav-btn img{height:20px;width:auto;display:block;flex:none;}'
90      + '.hz-mnav-bar-right{display:flex;align-items:center;gap:8px;flex:none;margin-left:auto;}'
91      + '.hz-mnav-lang{position:relative;display:inline-block;}'
92      + '.hz-mnav-lang-btn{appearance:none;background:transparent;border:0;padding:6px;cursor:pointer;display:inline-flex;align-items:center;line-height:0;border-radius:8px;}'
93      + '.hz-mnav-lang-btn:hover{background:#f5f5f5;}'
94      + '.hz-mnav-lang-menu{position:absolute;
94left:0;top:calc(100% + 6px);background:#fff;color:#111;list-style:none;margin:0;padding:6px;border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.18);border:1px solid rgba(0,0,0,.06);min-width:56px;z-index:10000;}'
95      + '.hz-mnav-lang-menu li{margin:0;padding:0;list-style:none;}'
96      + '.hz-mnav-lang-menu button{width:100%;padding:6px;border-radius:8px;border:0;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:0;}'
97      + '.hz-mnav-lang-menu button.active{background:rgba(0,0,0,.06);}'
98      + '.hz-mnav-toggle{appearance:none;background:#fff;color:#0a0a0a;border:1px solid rgba(0,0,0,.1);border-radius:10px;width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;}'
99      + '.hz-mnav-toggle:hover{background:#f5f5f5;}'
100      + '.hz-mnav-backdrop{position:fixed;inset:0;z-index:9001;background:rgba(0,0,0,.45);opacity:0;transition:opacity .2s ease;}'
101      + '.hz-mnav-backdrop.open{opacity:1;}'
102      + '.hz-mnav-drawer{position:fixed;top:0;right:0;bottom:0;width:min(360px,88vw);z-index:9002;background:#fff;color:#0a0a0a;transform:translateX(100%);transition:transform .25s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;overflow-y:auto;padding:76px 20px 24px;box-shadow:-16px 0 40px rgba(0,0,0,.18);}'
103      + '.hz-mnav-drawer.open{transform:translateX(0);}'
104      + '.hz-mnav-tabs{display:grid;grid-template-columns:1fr 1fr;background:#f3f4f6;border-radius:12px;padding:4px;margin-bottom:18px;}'
105      + '.hz-mnav-tab{text-align:center;padding:10px 0;font-size:14px;font-weight:600;color:#4b5563;text-decoration:none;border-radius:9px;}'
106      + '.hz-mnav-tab.active{background:#fff;color:#0a0a0a;box-shadow:0 1px 3px rgba(0,0,0,.06);}'
107      + '.hz-mnav-sections{display:flex;flex-direction:column;}'
108      + '.hz-mnav-section + .hz-mnav-section{border-top:1px solid rgba(0,0,0,.06);}'
109      + '.hz-mnav-section-head{width:100%;appearance:none;background:transparent;border:0;display:flex;align-items:center;justify-content:space-between;padding:14px 4px;font-size:16px;font-weight:600;color:#0a0a0a;
109cursor:pointer;}'
110      + '.hz-mnav-section-head .hz-mnav-chev{transition:transform .2s;}'
111      + '.hz-mnav-section.open .hz-mnav-chev{transform:rotate(180deg);}'
112      + '.hz-mnav-section-body{display:none;padding:4px 4px 14px;flex-direction:column;gap:16px;}'
113      + '.hz-mnav-section.open .hz-mnav-section-body{display:flex;}'
114      + '.hz-mnav-group-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#6b7280;margin-bottom:6px;}'
115      + '.hz-mnav-group-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;}'
116      + '.hz-mnav-group-list li{margin:0;}'
117      + '.hz-mnav-group-list a{display:block;padding:10px 12px;margin-left:-12px;font-size:15px;color:#1f2937;text-decoration:none;border-radius:8px;}'
118      + '.hz-mnav-group-list a:hover{background:#f5f5f5;}'
119      + '.hz-mnav-cta{margin-top:auto;padding-top:20px;display:flex;flex-direction:column;gap:10px;}'
120      + '.hz-mnav-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:48px;padding:0 16px;border-radius:12px;font-size:15px;font-weight:600;text-decoration:none;}'
121      + '.hz-mnav-btn.ghost{background:#fff;color:#0a0a0a;border:1px solid rgba(0,0,0,.12);}'
122      + '.hz-mnav-btn.primary{background:linear-gradient(135deg,#2d8aff 0%,#1a56db 100%);color:#fff;box-shadow:0 6px 18px rgba(26,86,219,.25);}'
123      + '}';
124    var s = document.createElement("style");
125    s.id = "hz-mnav-styles";
126    s.textContent = css;
127    document.head.appendChild(s);
128  }
129
130  function el(tag, attrs, html) {
131    var n = document.createElement(tag);
132    if (attrs) for (var k in attrs) {
133      if (k === "class") n.className = attrs[k];
134      else if (k === "html") n.innerHTML = attrs[k];
135      else n.setAttribute(k, attrs[k]);
136    }
137    if (html != null) n.innerHTML = html;
138    return n;
139  }
140
141  function getLang() {
142    try { return (window.__hzI18n && window.__hzI18n.get && window.__hzI18n.get()) || "nl"; }
143    catch (_) { return "nl"; }
144  }
145  function setLang(l) {
146    try { if (window.__hzI18n && window.__hzI18n.set) window.__hzI18n.set(l); }
147    catch (_) {}
148  }
149
150  function buildLangSwitcher() {
151    var wrap = el("div", { class: "hz-mnav-lang", "data-no-i18n": "" });
152    var btn = el("button", { class: "hz-mnav-lang-btn", type: "button", "aria-label": "Taal kiezen", "aria-haspopup": "listbox", "aria-expanded": "false" });
153    btn.innerHTML = FLAGS[getLang()] || FLAGS.nl;
154    wrap.appendChild(btn);
155
156    var menu = el("ul", { class: "hz-mnav-lang-menu", role: "listbox" });
157    LANGS.forEach(function (l) {
158      var li = el("li");
159      var b = el("button", { type: "button", role: "option", "data-lang": l, title: l.toUpperCase() });
160      b.innerHTML = FLAGS[l];
161      if (l === getLang()) b.classList.add("active");
162      b.addEventListener("click", function (e) {
163        e.stopPropagation();
164        setLang(l);
165        btn.innerHTML = FLAGS[l];
166        Array.prototype.forEach.call(menu.querySelectorAll("button"), function (x) { x.classList.toggle("active", x.getAttribute("data-lang") === l); });
167        menu.style.display = "none";
168        btn.setAttribute("aria-expanded", "false");
169      });
170      li.appendChild(b);
171      menu.appendChild(li);
172    });
173    menu.style.display = "none";
174    wrap.appendChild(menu);
175
176    btn.addEventListener("click", function (e) {
177      e.stopPropagation();
178      var isOpen = menu.style.display === "block";
179      menu.style.display = isOpen ? "none" : "block";
180      btn.setAttribute("aria-expanded", String(!isOpen));
181    });
182    document.addEventListener("click", function (e) {
183      if (!wrap.contains(e.target)) {
184        menu.style.display = "none";
185        btn.setAttribute("aria-expanded", "false");
186      }
187    });
188
189    // Sync flag if language changes elsewhere
190    try {
191      if (window.__hzI18n && window.__hzI18n.onChange) {
192        window.__hzI18n.onChange(function (l) {
193          btn.innerHTML = FLAGS[l] || FLAGS.nl;
194          Array.prototype.forEach.call(menu.querySelectorAll("button"), function (x) { x.classList.toggle("active", x.getAttribute("data-lang") === l); });
195        });
196      }
197    } catch (_) {}
198    return wrap;
199  }
200
201  function buildDrawer() {
202    var drawer = el("div", { class: "hz-mnav-drawer", "aria-hidden": "true" });
203    var path = window.location.pathname;
204    var isB2B2C = path.indexOf("/b2b2c") === 0;
205
206    var tabs = el("div", { class: "hz-mnav-tabs", role: "tablist", "aria-label": "Doelgroep" });
207    tabs.appendChild(el("a", { href: "/", class: "hz-mnav-tab" + (!isB2B2C ? " active" : "") }, "B2C"));
208    tabs.appendChild(el("a", { href: "/b2b2c", class: "hz-mnav-tab" + (isB2B2C ? " active" : "") }, "B2B2C"));
209    drawer.appendChild(tabs);
210
211
212
213
214    var secWrap = el("nav", { class: "hz-mnav-sections" });
215    SECTIONS.forEach(function (sec) {
216      var s = el("div", { class: "hz-mnav-section", "data-sec": sec.id });
217      var head = el("button", { type: "button", class: "hz-mnav-section-head", "aria-expanded": "false" });
218      head.innerHTML = "<span>" + sec.label + "</span>" + ICON_CHEV;
219      s.appendChild(head);
220      var body = el("div", { class: "hz-mnav-section-body" });
221      sec.groups.forEach(function (g) {
222        var gw = el("div", { class: "hz-mnav-group" });
223        gw.appendChild(el("div", { class: "hz-mnav-group-title" }, g.title));
224        var ul = el("ul", { class: "hz-mnav-group-list" });
225        g.items.forEach(function (it) {
226          var li = el("li");
227          li.appendChild(el("a", { href: it[1] }, it[0]));
228          ul.appendChild(li);
229        });
230        gw.appendChild(ul);
231        body.appendChild(gw);
232      });
233      s.appendChild(body);
234      head.addEventListener("click", function () {
235        var willOpen = !s.classList.contains("open");
236        Array.prototype.forEach.call(secWrap.querySelectorAll(".hz-mnav-section"), function (n) { n.classList.remove("open"); n.querySelector(".hz-mnav-section-head").setAttribute("aria-expanded", "false"); });
237        if (willOpen) { s.classList.add("open"); head.setAttribute("aria-expanded", "true"); }
238      });
239      secWrap.appendChild(s);
240    });
241    drawer.appendChild(secWrap);
242
243    var cta = el("div", { class: "hz-mnav-cta" });
244    cta.innerHTML =
245      '<a href="https://pico.homezero.nl" class="hz-mnav-btn ghost"><img src="/images/pico-icon.png" alt=""/><span>Pico</span></a>' +
246      '<a href="/#contact-form" class="hz-mnav-btn primary">Contact</a>';
247    drawer.appendChild(cta);
248
249    return drawer;
250  }
251
252  function mount() {
253    if (document.querySelector(".hz-mnav-root")) return;
254    injectStyles();
255    var root = el("div", { class: "hz-mnav-root" });
256    var bar = el("div", { class: "hz-mnav-bar" });
257    bar.innerHTML = '<a href="/" class="hz-mnav-logo" aria-label="HomeZero home"><img src="/images/Frame-1996662659.png" alt="HomeZero"/></a>';
258    var right = el("div", { class: "hz-mnav-bar-right" });
259    right.appendChild(buildLangSwitcher());
260    var toggle = el("button", { type: "button", class: "hz-mnav-toggle", "aria-label": "Menu openen", "aria-expanded": "false" }, ICON_BURGER);
261    right.appendChild(toggle);
262    bar.appendChild(right);
263    root.appendChild(bar);
264
265    var backdrop = el("div", { class: "hz-mnav-backdrop" });
266    backdrop.style.display = "none";
267    var drawer = buildDrawer();
268
269    root.appendChild(backdrop);
270    root.appendChild(drawer);
271    document.body.appendChild(root);
272
273    var open = false;
274    function setOpen(v) {
275      open = v;
276      drawer.classList.toggle("open", v);
277      drawer.setAttribute("aria-hidden", String(!v));
278      toggle.innerHTML = v ? ICON_CLOSE : ICON_BURGER;
279      toggle.setAttribute("aria-label", v ? "Menu sluiten" : "Menu openen");
280      toggle.setAttribute("aria-expanded", String(v));
281      if (v) {
282        backdrop.style.display = "block";
283        requestAnimationFrame(function () { backdrop.classList.add("open"); });
284        document.body.style.overflow = "hidden";
285      } else {
286        backdrop.classList.remove("open");
287        setTimeout(function () { if (!open) backdrop.style.display = "none"; }, 200);
288        document.body.style.overflow = "";
289      }
290    }
291    toggle.addEventListener("click", function () { setOpen(!open); });
292    backdrop.addEventListener("click", function () { setOpen(false); });
293    document.addEventListener("keydown", function (e) { if (e.key === "Escape" && open) setOpen(false); });
294    // Close drawer when an internal link is clicked
295    drawer.addEventListener("click", function (e) {
296      var t = e.target;
297      while (t && t !== drawer) {
298        if (t.tagName === "A") { setOpen(false); return; }
299        t = t.parentNode;
300      }
301    });
302  }
303
304  if (document.readyState === "loading") {
305    document.addEventListener("DOMContentLoaded", mount);
306  } else {
307    mount();
308  }
309})();

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.