PageSourceSearch

https://www.phos.de/js/phos-gawel-shop.js?20260626b

js phos.de collected 2026-09-26 05:03:03 UTC 21,137 bytes, 832 lines download raw bytes

1(function () {
2  "use strict";
3
4  var CONFIG = {
5    shopBase: {
6      de: "/shop",
7      en: "/shop/en"
8    },
9    themeCssStorageKey: "phos_shop_theme_css",
10    changeQuantityDelay: 800,
11    fetchOptions: {
12      credentials: "include",
13      cache: "no-store"
14    },
15    ajaxHeaders: {
16      "X-Requested-With": "XMLHttpRequest",
17      Accept: "text/html, application/xhtml+xml"
18    }
19  };
20
21  var dom = {};
22  var state = {
23    lang: "de",
24    cartActionPending: false,
25    cartEventsBound: false,
26    quantityDebounceTimer: null
27  };
28
29  function getShopBase() {
30    return CONFIG.shopBase[state.lang] || CONFIG.shopBase.de;
31  }
32
33  function getCartInfoUrl() {
34    return getShopBase() + "/widgets/checkout/info";
35  }
36
37  function getCartOffcanvasUrl() {
38    return getShopBase() + "/checkout/offcanvas";
39  }
40
41  function getShopHomeUrl() {
42    return getShopBase() + "/";
43  }
44
45  function detectLang() {
46    if (dom.offcanvas) {
47      var attr = dom.offcanvas.getAttribute("data-gawel-shop-lang");
48      if (attr === "en") {
49        return "en";
50      }
51    }
52
53    if (document.documentElement.lang === "en") {
54      return "en";
55    }
56
57    return "de";
58  }
59
60  function getLoadingText() {
61    return state.lang === "en" ? "Loading …" : "Wird geladen …";
62  }
63
64  function getErrorText() {
65    return state.lang === "en"
66      ? "Cart could not be loaded."
67      : "Warenkorb konnte nicht geladen werden.";
68  }
69
70  function cacheDom() {
71    dom.trigger = document.querySelectorAll("[data-gawel-cart-trigger]");
72    dom.count = document.querySelectorAll("[data-gawel-cart-count]");
73    dom.offcanvas = document.getElementById("gawelCartOffcanvas");
74    dom.content = document.querySelector("[data-gawel-cart-content]");
75    state.lang = detectLang();
76  }
77
78  function parseHtml(html) {
79    return new DOMParser().parseFromString(html, "text/html");
80  }
81
82  function resolveShopUrl(url) {
83    if (!url) {
84      return getCartOffcanvasUrl();
85    }
86
87    if (/^https?:\/\//i.test(url)) {
88      try {
89        var parsed = new URL(url);
90        if (parsed.origin === window.location.origin) {
91          return parsed.pathname + parsed.search;
92        }
93      } catch (error) {
94        return url;
95      }
96      return url;
97    }
98
99    if (url.charAt(0) === "/") {
100      if (/^\/shop(\/en)?\//i.test(url) || url === "/shop" || url === "/shop/en") {
101        return url;
102      }
103      if (/^\/(checkout|widgets|wishlist)\//i.test(url)) {
104        return getShopBase() + url;
105      }
106      return url;
107    }
108
109    return getShopBase() + "/" + url;
110  }
111
112  function parseCartCount(doc) {
113    var countEl = doc.querySelector(".offcanvas-cart-header-count");
114    if (countEl) {
115      var match = countEl.textContent.match(/(\d+)/);
116      if (match) {
117        return parseInt(match[1], 10);
118      }
119    }
120
121    var items = doc.querySelectorAll(
122      ".offcanvas-cart-items > .line-item, .offcanvas-cart-items > li, .offcanvas-cart .line-item, .offcanvas-cart .cart-item"
123    );
124    var count = 0;
125
126    items.forEach(function (item) {
127      if (item.classList.contains("is-child") || item.classList.contains("is-nested")) {
128        return;
129      }
130      count += 1;
131    });
132
133    if (count > 0) {
134      return count;
135    }
136
137    var hidden = doc.querySelector(".hidden-line-items-information");
138    if (hidden) {
139      var hiddenItems = hidden.querySelectorAll("[data-line-item-id], .hidden-line-item");
140      if (hiddenItems.length > 0) {
141        return hiddenItems.length;
142      }
143    }
144
145    var emptyAlert = doc.querySelector(".offcanvas-cart .alert");
146    if (emptyAlert && /leer|empty/i.test(emptyAlert.textContent)) {
147      return 0;
148    }
149
150    return 0;
151  }
152
153  function updateBadge(count) {
154    if (!dom.count || !dom.count.length) {
155      return;
156    }
157
158    var safeCount = Math.max(0, Number(count) || 0);
159
160    dom.count.forEach(function (badge) {
161      badge.textContent = String(safeCount);
162
163      if (safeCount <= 0) {
164        badge.classList.add("is-empty");
165        badge.setAttribute("hidden", "");
166      } else {
167        badge.classList.remove("is-empty");
168        badge.removeAttribute("hidden");
169      }
170    });
171  }
172
173  async function fetchCartOffcanvasHtml() {
174    var response = await fetch(getCartOffcanvasUrl(), CONFIG.fetchOptions);
175    if (!response.ok) {
176      throw new Error("Cart offcanvas request failed");
177    }
178    return response.text();
179  }
180
181  async function getShopThemeCssUrl() {
182    var storageKey = CONFIG.themeCssStorageKey + "_" + state.lang;
183
184    try {
185      var cached = sessionStorage.getItem(storageKey);
186      if (cached) {
187        return cached;
188      }
189    } catch (error) {
190      /* ignore */
191    }
192
193    var response = await fetch(getShopHomeUrl(), CONFIG.fetchOptions);
194    if (!response.ok) {
195      throw new Error("Shop theme discovery failed");
196    }
197
198    var html = await response.text();
199    var match = html.match(/\/shop\/theme\/[a-f0-9]+\/css\/all\.css[^"'\\s]*/i);
200    if (!match) {
201      throw new Error("Shop theme CSS not found");
202    }
203
204    try {
205      sessionStorage.setItem(storageKey, match[0]);
206    } catch (error) {
207      /* ignore */
208    }
209
210    return match[0];
211  }
212
213  function loadStylesheet(shadow, href) {
214    return new Promise(function (resolve, reject) {
215      var existing = shadow.querySelector('link[data-shop-theme="true"]');
216      if (existing) {
217        if (existing.sheet) {
218          resolve(existing);
219          return;
220        }
221        existing.addEventListener("load", function () {
222          resolve(existing);
223        });
224        existing.addEventListener("error", reject);
225        return;
226      }
227
228      var link = document.createElement("link");
229      link.rel = "stylesheet";
230      link.href = href;
231      link.setAttribute("data-shop-theme", "true");
232      link.addEventListener("load", function () {
233        resolve(link);
234      });
235      link.addEventListener("error", reject);
236      shadow.appendChild(link);
237    });
238  }
239
240  function ensureThemeStyles(shadow) {
241    var existing = shadow.querySelector('link[data-shop-theme="true"]');
242    if (existing && existing.sheet) {
243      return Promise.resolve(existing);
244    }
245
246    return getShopThemeCssUrl()
247      .then(function (url) {
248        return loadStylesheet(shadow, url);
249      })
250      .catch(function (error) {
251        console.warn("Gawel cart theme CSS skipped:", error);
252        return null;
253      });
254  }
255
256  function getShadowHost() {
257    var host = dom.content.querySelector("[data-gawel-cart-shadow-host]");
258    if (!host) {
259      dom.content.innerHTML =
260        '<div data-gawel-cart-shadow-host class="phos-gawel-cart-offcanvas__host"></div>';
261      host = dom.content.querySelector("[data-gawel-cart-shadow-host]");
262    }
263    return host;
264  }
265
266  function getShadowRoot() {
267    var host = getShadowHost();
268    if (!host.shadowRoot) {
269      host.attachShadow({ mode: "open" });
270      ensureShellStyles(host.shadowRoot);
271    }
272    return host.shadowRoot;
273  }
274
275  function ensureShellStyles(shadow) {
276    if (shadow.querySelector("style[data-gawel-cart-shell]")) {
277      return;
278    }
279
280    var style = document.createElement("style");
281    style.setAttribute("data-gawel-cart-shell", "");
282    style.textContent =
283      ":host{display:block;font-family:Inter,system-ui,sans-serif;font-size:16px;line-height:1.5;color:#2b3136}" +
284      ".phos-gawel-cart-offcanvas__loading{padding:2rem 1.25rem;text-align:center;color:#6b7280;font-size:.875rem}" +
285      ".phos-gawel-cart-offcanvas__error{padding:2rem 1.25rem;text-align:center;color:#b42318;font-size:.875rem;margin:0}" +
286      ".phos-gawel-cart-offcanvas__updating{opacity:.55;pointer-events:none}";
287    shadow.appendChild(style);
288  }
289
290  function loadShadowStylesheet(shadow, href, marker) {
291    return new Promise(function (resolve, reject) {
292      var existing = shadow.querySelector('link[data-gawel-asset="' + marker + '"]');
293      if (existing) {
294        if (existing.sheet) {
295          resolve(existing);
296          return;
297        }
298        existing.addEventListener("load", function () {
299          resolve(existing);
300        });
301        existing.addEventListener("error", reject);
302        return;
303      }
304
305      var link = document.createElement("link");
306      link.rel = "stylesheet";
307      link.href = href;
308      link.setAttribute("data-gawel-asset", marker);
309      link.addEventListener("load", function () {
310        resolve(link);
311      });
312      link.addEventListener("error", reject);
313      shadow.appendChild(link);
314    });
315  }
316
317  function ensureEmbedAssets(shadow) {
318    return Promise.all([
319      loadShadowStylesheet(
320        shadow,
321        "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap",
322        "font-inter"
323      ),
324      loadShadowStylesheet(shadow, "/css/phos-gawel-shop-cart-embed.css?20260626b", "embed-css")
325    ]).catch(function (error) {
326      console.warn("Gawel cart embed CSS skipped:", error);
327      return null;
328    });
329  }
330
331  function getShadowMount(shadow) {
332    var mount = shadow.querySelector("[data-gawel-cart-mount]");
333    if (!mount) {
334      mount = document.createElement("div");
335      mount.setAttribute("data-gawel-cart-mount", "");
336      mount.className =
337        "phos-shop-cart-embed is-active-route-frontend-cart-offcanvas";
338      shadow.appendChild(mount);
339    }
340    return mount;
341  }
342
343  function wrapOffcanvasContent(innerHtml) {
344    return (
345      '<div class="offcanvas cart-offcanvas">' +
346      '<div class="offcanvas-body">' +
347      innerHtml +
348      "</div></div>"
349    );
350  }
351
352  function showLoadingInShadow(shadow) {
353    var mount = getShadowMount(shadow);
354    mount.innerHTML =
355      '<div class="phos-gawel-cart-offcanvas__loading">' + getLoadingText() + "</div>";
356  }
357
358  function showError(message) {
359    var shadow = getShadowRoot();
360    var mount = getShadowMount(shadow);
361    mount.innerHTML =
362      '<p class="phos-gawel-cart-offcanvas__error">' + message + "</p>";
363  }
364
365  function setCartUpdating(isUpdating) {
366    var shadow = getShadowRoot();
367    var mount = getShadowMount(shadow);
368    if (!mount) {
369      return;
370    }
371
372    if (isUpdating) {
373      mount.classList.add("phos-gawel-cart-offcanvas__updating");
374      return;
375    }
376
377    mount.classList.remove("phos-gawel-cart-offcanvas__updating");
378  }
379
380  async function refreshCartBadge() {
381    try {
382      var infoResponse = await fetch(getCartInfoUrl(), CONFIG.fetchOptions);
383
384      if (infoResponse.status === 204) {
385        updateBadge(0);
386        return;
387      }
388
389      if (infoResponse.ok) {
390        var infoHtml = await infoResponse.text();
391        if (infoHtml && infoHtml.trim()) {
392          var infoDoc = parseHtml(infoHtml);
393          var badge = infoDoc.querySelector(".header-cart-badge");
394          if (badge) {
395            updateBadge(parseInt(badge.textContent, 10) || 0);
396            return;
397          }
398        }
399      }
400
401      var html = await fetchCartOffcanvasHtml();
402      updateBadge(parseCartCount(parseHtml(html)));
403    } catch (error) {
404      console.error("Gawel cart badge refresh failed:", error);
405    }
406  }
407
408  function extractOffcanvasContent(doc, rawHtml) {
409    var cart = doc.querySelector(".offcanvas-cart");
410    if (cart) {
411      return cart.outerHTML;
412    }
413
414    var body = doc.querySelector(".offcanvas-body");
415    if (body) {
416      return body.innerHTML;
417    }
418
419    if (doc.body && doc.body.firstElementChild) {
420      var root = doc.body.firstElementChild;
421      if (
422        root.classList &&
423        (root.classList.contains("offcanvas-cart") ||
424          root.classList.contains("offcanvas") ||
425          root.querySelector(".offcanvas-cart"))
426      ) {
427        var nestedCart = root.querySelector(".offcanvas-cart");
428        if (nestedCart) {
429          return nestedCart.outerHTML;
430        }
431        return root.outerHTML;
432      }
433    }
434
435    if (rawHtml && /offcanvas-cart/i.test(rawHtml)) {
436      return rawHtml;
437    }
438
439    return "";
440  }
441
442  function getFormAction(form) {
443    return (form && form.getAttribute("action")) || "";
444  }
445
446  function isCartActionForm(form) {
447    if (!form || form.tagName !== "FORM") {
448      return false;
449    }
450
451    if (form.classList.contains("js-offcanvas-cart-remove-product")) {
452      return true;
453    }
454
455    if (form.classList.contains("js-offcanvas-cart-add-promotion")) {
456      return true;
457    }
458
459    var action = getFormAction(form);
460    return /line-item\/(delete|change-quantity)|promotion\/add/i.test(action);
461  }
462
463  function isQuantityControl(element) {
464    if (!element) {
465      return false;
466    }
467
468    return (
469      element.matches(".js-offcanvas-cart-change-quantity") ||
470      element.matches(".js-offcanvas-cart-change-quantity-number") ||
471      (element.matches('input[name="quantity"]') &&
472        /line-item\/change-quantity/i.test(getFormAction(element.closest("form"))))
473    );
474  }
475
476  function findQuantityForm(element) {
477    if (!element) {
478      return null;
479    }
480
481    if (element.tagName === "FORM" && /line-item\/change-quantity/i.test(getFormAction(element))) {
482      return element;
483    }
484
485    return element.closest("form");
486  }
487
488  function appendFormCsrf(formData, form) {
489    if (!form) {
490      return formData;
491    }
492
493    form.querySelectorAll('input[type="hidden"]').forEach(function (input) {
494      if (!input.name) {
495        return;
496      }
497
498      if (!formData.has(input.name)) {
499        formData.append(input.name, input.value);
500      }
501    });
502
503    return formData;
504  }
505
506  async function postCartForm(form) {
507    var url = resolveShopUrl(getFormAction(form));
508    var formData = appendFormCsrf(new FormData(form), form);
509
510    var response = await fetch(url, {
511      method: "POST",
512      body: formData,
513      credentials: "include",
514      cache: "no-store",
515      headers: CONFIG.ajaxHeaders
516    });
517
518    if (!response.ok) {
519      throw new Error("Cart action failed with status " + response.status);
520    }
521
522    return response.text();
523  }
524
525  function renderOffcanvasHtml(html) {
526    var doc = parseHtml(html);
527    var content = extractOffcanvasContent(doc, html);
528
529    if (!content) {
530      return false;
531    }
532
533    var shadow = getShadowRoot();
534    var mount = getShadowMount(shadow);
535    mount.innerHTML = wrapOffcanvasContent(content);
536
537    dom.content.classList.add("phos-gawel-cart-offcanvas__content--ready");
538    updateBadge(parseCartCount(doc));
539    fixShadowLinks(shadow);
540
541    ensureThemeStyles(shadow);
542    ensureEmbedAssets(shadow);
543
544    return true;
545  }
546
547  async function submitCartForm(form) {
548    if (!form || state.cartActionPending) {
549      return false;
550    }
551
552    state.cartActionPending = true;
553    setCartUpdating(true);
554
555    try {
556      var html = await postCartForm(form);
557      if (!renderOffcanvasHtml(html)) {
558        await reloadOffcanvasContent({ showLoading: false });
559      }
560      return true;
561    } catch (error) {
562      console.error("Gawel cart form submit failed:", error);
563      try {
564        await reloadOffcanvasContent({ showLoading: false });
565      } catch (reloadError) {
566        console.error("Gawel cart reload after action failed:", reloadError);
567      }
568      return false;
569    } finally {
570      state.cartActionPending = false;
571      setCartUpdating(false);
572    }
573  }
574
575  function debouncedSubmitCartForm(form) {
576    if (!form) {
577      return;
578    }
579
580    if (state.quantityDebounceTimer) {
581      window.clearTimeout(state.quantityDebounceTimer);
582    }
583
584    state.quantityDebounceTimer = window.setTimeout(function () {
585      state.quantityDebounceTimer = null;
586      submitCartForm(form);
587    }, CONFIG.changeQuantityDelay);
588  }
589
590  function adjustQuantityFromButton(button, direction) {
591    var group = button.closest(".quantity-selector-group, [data-quantity-selector]");
592    if (!group) {
593      return null;
594    }
595
596    var input = group.querySelector('input[name="quantity"]');
597    var form = findQuantityForm(group);
598    if (!input || !form) {
599      return null;
600    }
601
602    var step = parseInt(input.getAttribute("step"), 10) || 1;
603    var min = parseInt(input.getAttribute("min"), 10) || 1;
604    var max = parseInt(input.getAttribute("max"), 10) || 9999;
605    var current = parseInt(input.value, 10) || min;
606    var next = direction === "plus" ? current + step : current - step;
607
608    next = Math.max(min, Math.min(max, next));
609
610    if (next === current) {
611      return null;
612    }
613
614    input.value = String(next);
615    return form;
616  }
617
618  function onShadowSubmit(event) {
619    var form = event.target;
620    if (!isCartActionForm(form)) {
621      return;
622    }
623
624    event.preventDefault();
625    submitCartForm(form);
626  }
627
628  function onShadowChange(event) {
629    var target = event.target;
630
631    if (!isQuantityControl(target)) {
632      return;
633    }
634
635    var form = findQuantityForm(target);
636    if (!form) {
637      return;
638    }
639
640    if (target.matches(".js-offcanvas-cart-change-quantity")) {
641      submitCartForm(form);
642      return;
643    }
644
645    debouncedSubmitCartForm(form);
646  }
647
648  function onShadowClick(event) {
649    var plusButton = event.target.closest(".btn-plus");
650    var minusButton = event.target.closest(".btn-minus");
651    var quantityButton = plusButton || minusButton;
652
653    if (!quantityButton) {
654      return;
655    }
656
657    event.preventDefault();
658
659    var form = adjustQuantityFromButton(
660      quantityButton,
661      plusButton ? "plus" : "minus"
662    );
663
664    if (form) {
665      submitCartForm(form);
666    }
667  }
668
669  function fixShadowLinks(shadow) {
670    if (!shadow) {
671      return;
672    }
673
674    shadow.querySelectorAll("a[href]").forEach(function (link) {
675      var href = link.getAttribute("href");
676      if (!href || href.indexOf("javascript:") === 0 || href === "#") {
677        return;
678      }
679
680      if (/^\/(checkout|wishlist)\//i.test(href)) {
681        link.setAttribute("href", resolveShopUrl(href));
682      }
683
684      if (/^\/shop\//i.test(href)) {
685        link.setAttribute("target", "_self");
686      }
687    });
688
689    shadow.querySelectorAll("[data-bs-dismiss='offcanvas'], .js-offcanvas-close").forEach(function (button) {
690      if (button.getAttribute("data-gawel-bound") === "true") {
691        return;
692      }
693
694      button.setAttribute("data-gawel-bound", "true");
695      button.addEventListener("click", function (event) {
696        event.preventDefault();
697        closeOffcanvas();
698      });
699    });
700  }
701
702  function bindOffcanvasInteractions(shadow) {
703    if (!shadow || state.cartEventsBound) {
704      fixShadowLinks(shadow);
705      return;
706    }
707
708    shadow.addEventListener("submit", onShadowSubmit);
709    shadow.addEventListener("change", onShadowChange);
710    shadow.addEventListener("click", onShadowClick);
711    state.cartEventsBound = true;
712    fixShadowLinks(shadow);
713  }
714
715  async function reloadOffcanvasContent(options) {
716    var showLoading = !options || options.showLoading !== false;
717
718    if (!dom.content) {
719      return null;
720    }
721
722    var shadow = getShadowRoot();
723
724    if (showLoading) {
725      showLoadingInShadow(shadow);
726    }
727
728    var html = await fetchCartOffcanvasHtml();
729    var doc = parseHtml(html);
730    var content = extractOffcanvasContent(doc, html);
731
732    if (!content) {
733      showError(getErrorText());
734      return null;
735    }
736
737    var mount = getShadowMount(shadow);
738    mount.innerHTML = wrapOffcanvasContent(content);
739
740    dom.content.classList.add("phos-gawel-cart-offcanvas__content--ready");
741    updateBadge(parseCartCount(doc));
742    bindOffcanvasInteractions(shadow);
743
744    ensureThemeStyles(shadow);
745    ensureEmbedAssets(shadow);
746
747    return doc;
748  }
749
750  async function loadOffcanvasContent() {
751    try {
752      return await reloadOffcanvasContent({ showLoading: true });
753    } catch (error) {
754      console.error("Gawel cart offcanvas load failed:", error);
755      showError(getErrorText());
756      return null;
757    }
758  }
759
760  function openOffcanvas() {
761    if (!dom.offcanvas) {
762      return;
763    }
764
765    loadOffcanvasContent();
766
767    if (typeof bootstrap !== "undefined" && bootstrap.Offcanvas) {
768      bootstrap.Offcanvas.getOrCreateInstance(dom.offcanvas).show();
769      return;
770    }
771
772    dom.offcanvas.classList.add("show");
773  }
774
775  function closeOffcanvas() {
776    if (!dom.offcanvas) {
777      return;
778    }
779
780    if (typeof bootstrap !== "undefined" && bootstrap.Offcanvas) {
781      var instance = bootstrap.Offcanvas.getInstance(dom.offcanvas);
782      if (instance) {
783        instance.hide();
784      }
785      return;
786    }
787
788    dom.offcanvas.classList.remove("show");
789  }
790
791  function setupRefreshOnFocus() {
792    window.addEventListener("focus", refreshCartBadge);
793    document.addEventListener("visibilitychange", function () {
794      if (document.visibilityState === "visible") {
795        refreshCartBadge();
796      }
797    });
798  }
799
800  function init() {
801    if (window.__PHOS_GAWEL_SHOP_INIT__) {
802      return;
803    }
804    window.__PHOS_GAWEL_SHOP_INIT__ = true;
805
806    cacheDom();
807
808    if (!dom.trigger || !dom.trigger.length) {
809      return;
810    }
811
812    dom.trigger.forEach(function (trigger) {
813      trigger.addEventListener("click", function (event) {
814        event.preventDefault();
815        openOffcanvas();
816      });
817    });
818
819    if (dom.offcanvas) {
820      dom.offcanvas.addEventListener("hidden.bs.offcanvas", refreshCartBadge);
821    }
822
823    setupRefreshOnFocus();
824    refreshCartBadge();
825  }
826
827  if (document.readyState === "loading") {
828    document.addEventListener("DOMContentLoaded", init);
829  } else {
830    init();
831  }
832})();

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.