PageSourceSearch

https://mirostudio.ch/_assets/miro-content-refinement-v3.js?v=20260730i

js mirostudio.ch collected 2026-10-10 02:09:07 UTC 13,306 bytes, 387 lines download raw bytes

1(function () {
2  "use strict";
3
4  document.documentElement.classList.add("miro-refinement-js");
5
6  function cardLeft(track, card) {
7    var trackRect = track.getBoundingClientRect();
8    var cardRect = card.getBoundingClientRect();
9    return cardRect.left - trackRect.left + track.scrollLeft;
10  }
11
12  function visibleCardIndex(track, cards) {
13    var left = track.scrollLeft;
14    var closest = 0;
15    var distance = Infinity;
16    cards.forEach(function (card, index) {
17      var nextDistance = Math.abs(cardLeft(track, card) - left);
18      if (nextDistance < distance) {
19        closest = index;
20        distance = nextDistance;
21      }
22    });
23    return closest;
24  }
25
26  function initReviewCarousel(root) {
27    var track = root.querySelector(".miro-reviews-v2__grid");
28    var previous = root.querySelector(".miro-reviews-v2__control--previous");
29    var next = root.querySelector(".miro-reviews-v2__control--next");
30    var position = root.querySelector(".miro-reviews-v2__position");
31    var dialog = root.querySelector(".miro-review-dialog");
32    var dialogQuote = dialog && dialog.querySelector(".miro-review-dialog__quote");
33    var dialogAuthor = dialog && dialog.querySelector(".miro-review-dialog__author");
34    var dialogClose = dialog && dialog.querySelector(".miro-review-dialog__close");
35    var dialogTrigger = null;
36    if (!track || root.getAttribute("data-miro-carousel-ready") === "true") return;
37
38    track
39      .querySelectorAll("[data-miro-review-clone]")
40      .forEach(function (clone) {
41        clone.remove();
42      });
43    var spacer = track.querySelector(".miro-reviews-v2__spacer");
44    if (spacer) spacer.remove();
45
46    var cards = Array.prototype.filter.call(track.children, function (child) {
47      return child.classList && child.classList.contains("miro-review-v2");
48    });
49    if (!cards.length) return;
50    root.setAttribute("data-miro-carousel-ready", "true");
51
52    cards.forEach(function (card, index) {
53      card.setAttribute("data-miro-review-index", String(index));
54      card.setAttribute("aria-posinset", String(index + 1));
55      card.setAttribute("aria-setsize", String(cards.length));
56    });
57
58    // MIRO_PERF_FINAL: one read batch instead of a scrollHeight read between
59    // every pair of attribute writes - that interleaving relaid out the whole
60    // document once per card and cost ~155 ms.
61    //
62    // Full review text lives in the blockquote (crawlable) and is only
63    // visually shortened via -webkit-line-clamp. Drop the clamp again when the
64    // quote fits anyway, so the "read more" button stays honest - but only
65    // once the clamp is actually computed. Without that guard a stylesheet
66    // that has not been applied yet reads as "everything fits" and strips
67    // is-clamped for good.
68    var quotes = cards.map(function (card) {
69      return card.querySelector(".miro-review-v2__quote");
70    });
71    var clamped = quotes.some(function (quote) {
72      return quote && window.getComputedStyle(quote).webkitLineClamp !== "none";
73    });
74    if (clamped) {
75      var fits = quotes.map(function (quote) {
76        return !!quote && quote.scrollHeight <= quote.clientHeight + 2;
77      });
78      fits.forEach(function (ok, index) {
79        if (ok) cards[index].classList.remove("is-clamped");
80      });
81    }
82
83    function cloneCard(card, index) {
84      var clone = card.cloneNode(true);
85      clone.setAttribute("data-miro-review-clone", "");
86      clone.setAttribute("data-miro-review-index", String(index));
87      clone.setAttribute("aria-hidden", "true");
88      clone.setAttribute("role", "presentation");
89      clone.removeAttribute("aria-posinset");
90      clone.removeAttribute("aria-setsize");
91      clone.querySelectorAll("[id]").forEach(function (element) {
92        element.removeAttribute("id");
93      });
94      clone
95        .querySelectorAll(
96          "a, button, input, select, textarea, summary, [tabindex]",
97        )
98        .forEach(function (element) {
99          element.setAttribute("tabindex", "-1");
100        });
101      return clone;
102    }
103
104    var before = document.createDocumentFragment();
105    var after = document.createDocumentFragment();
106    cards.forEach(function (card, index) {
107      before.appendChild(cloneCard(card, index));
108      after.appendChild(cloneCard(card, index));
109    });
110    track.insertBefore(before, cards[0]);
111    track.appendChild(after);
112
113    var renderedCards = Array.prototype.filter.call(
114      track.children,
115      function (child) {
116        return child.classList && child.classList.contains("miro-review-v2");
117      },
118    );
119    var cardCount = cards.length;
120    var currentLogicalIndex = 0;
121    var settleTimer = 0;
122    var jumpFrame = 0;
123
124    function logicalIndex(card) {
125      var value = Number(card && card.getAttribute("data-miro-review-index"));
126      return Number.isFinite(value) ? value : 0;
127    }
128
129    function setPosition(index) {
130      currentLogicalIndex = ((index % cardCount) + cardCount) % cardCount;
131      if (position) {
132        var label =
133          String(currentLogicalIndex + 1) + " / " + String(cardCount);
134        if (position.textContent !== label) position.textContent = label;
135      }
136      if (previous) previous.disabled = false;
137      if (next) next.disabled = false;
138    }
139
140    function jumpToCard(card) {
141      if (!card) return;
142      if (jumpFrame) cancelAnimationFrame(jumpFrame);
143      track.classList.add("miro-reviews-v2__grid--jumping");
144      track.scrollTo({ left: cardLeft(track, card), behavior: "auto" });
145      jumpFrame = requestAnimationFrame(function () {
146        jumpFrame = requestAnimationFrame(function () {
147          track.classList.remove("miro-reviews-v2__grid--jumping");
148          jumpFrame = 0;
149        });
150      });
151    }
152
153    function normalizePosition() {
154      var physicalIndex = visibleCardIndex(track, renderedCards);
155      var targetIndex = physicalIndex;
156      if (physicalIndex < cardCount) {
157        targetIndex = physicalIndex + cardCount;
158      } else if (physicalIndex >= cardCount * 2) {
159        targetIndex = physicalIndex - cardCount;
160      }
161      if (targetIndex !== physicalIndex) {
162        jumpToCard(renderedCards[targetIndex]);
163      }
164      setPosition(logicalIndex(renderedCards[targetIndex]));
165      return targetIndex;
166    }
167
168    function scheduleSettle() {
169      window.clearTimeout(settleTimer);
170      settleTimer = window.setTimeout(normalizePosition, 180);
171    }
172
173    root.addEventListener("click", function (event) {
174      var button = event.target.closest(".miro-review-v2__more");
175      if (!button || !root.contains(button)) return;
176      var visibleCard = button.closest(".miro-review-v2");
177      var index = logicalIndex(visibleCard);
178      var sourceCard = cards[index];
179      var sourceButton = sourceCard.querySelector(".miro-review-v2__more");
180      var full = sourceCard.querySelector(".miro-review-v2__quote");
181      if (!full || !dialog || !dialogQuote || !dialogAuthor) return;
182
183      if (visibleCard.hasAttribute("data-miro-review-clone")) {
184        var physicalIndex = renderedCards.indexOf(visibleCard);
185        if (physicalIndex < cardCount) {
186          jumpToCard(renderedCards[physicalIndex + cardCount]);
187        } else if (physicalIndex >= cardCount * 2) {
188          jumpToCard(renderedCards[physicalIndex - cardCount]);
189        }
190      }
191      setPosition(index);
192      dialogQuote.textContent = full.textContent.trim();
193      dialogAuthor.textContent =
194        sourceCard.getAttribute("data-review-author") || "";
195      dialogTrigger = sourceButton;
196      if (typeof dialog.showModal === "function") {
197        dialog.showModal();
198      } else {
199        dialog.setAttribute("open", "");
200      }
201    });
202
203    function closeDialog() {
204      if (!dialog) return;
205      if (typeof dialog.close === "function") {
206        dialog.close();
207      } else {
208        dialog.removeAttribute("open");
209      }
210    }
211
212    if (dialogClose) dialogClose.addEventListener("click", closeDialog);
213    if (dialog) {
214      dialog.addEventListener("click", function (event) {
215        if (event.target === dialog) closeDialog();
216      });
217      dialog.addEventListener("close", function () {
218        if (dialogTrigger) dialogTrigger.focus();
219        dialogTrigger = null;
220      });
221    }
222
223    function scrollToCard(card) {
224      var target = card;
225      if (!target) return;
226      track.scrollTo({
227        left: cardLeft(track, target),
228        behavior:
229          window.matchMedia &&
230          window.matchMedia("(prefers-reduced-motion: reduce)").matches
231            ? "auto"
232            : "smooth",
233      });
234      scheduleSettle();
235    }
236
237    function moveBy(delta) {
238      var physicalIndex = normalizePosition();
239      var target = renderedCards[physicalIndex + delta];
240      if (!target) {
241        jumpToCard(cards[currentLogicalIndex]);
242        physicalIndex = cardCount + currentLogicalIndex;
243        target = renderedCards[physicalIndex + delta];
244      }
245      scrollToCard(target);
246    }
247
248    if (previous) {
249      previous.addEventListener("click", function () {
250        moveBy(-1);
251      });
252    }
253    if (next) {
254      next.addEventListener("click", function () {
255        moveBy(1);
256      });
257    }
258    track.addEventListener("keydown", function (event) {
259      if (event.key === "ArrowLeft") {
260        event.preventDefault();
261        moveBy(-1);
262      } else if (event.key === "ArrowRight") {
263        event.preventDefault();
264        moveBy(1);
265      } else if (event.key === "Home") {
266        event.preventDefault();
267        scrollToCard(cards[0]);
268      } else if (event.key === "End") {
269        event.preventDefault();
270        scrollToCard(cards[cardCount - 1]);
271      }
272    });
273
274    track.addEventListener(
275      "scroll",
276      scheduleSettle,
277      { passive: true },
278    );
279    track.addEventListener("scrollend", normalizePosition, { passive: true });
280    window.addEventListener(
281      "resize",
282      function () {
283        window.clearTimeout(settleTimer);
284        settleTimer = window.setTimeout(function () {
285          var physicalIndex = visibleCardIndex(track, renderedCards);
286          currentLogicalIndex = logicalIndex(renderedCards[physicalIndex]);
287          jumpToCard(cards[currentLogicalIndex]);
288          setPosition(currentLogicalIndex);
289        }, 100);
290      },
291      { passive: true },
292    );
293
294    jumpToCard(cards[0]);
295    setPosition(0);
296  }
297
298  function faqTarget(toggle) {
299    var selector = toggle.getAttribute("data-target") || toggle.getAttribute("href");
300    if (!selector || selector.charAt(0) !== "#") return null;
301    try {
302      return document.querySelector(selector);
303    } catch (_error) {
304      return null;
305    }
306  }
307
308  function setFaqState(toggle, panel, open) {
309    toggle.setAttribute("aria-expanded", String(open));
310    toggle.classList.toggle("collapsed", !open);
311    toggle.classList.toggle("active", open);
312    panel.classList.toggle("miro-faq-is-open", open);
313    panel.classList.remove("in", "show", "collapsing");
314    panel.setAttribute("aria-hidden", String(!open));
315    panel.style.height = "";
316  }
317
318  function initFaq(root) {
319    var toggles = Array.prototype.slice.call(
320      root.querySelectorAll(".panel-title > a[data-target], .panel-title > a[href^='#']"),
321    );
322    toggles.forEach(function (toggle) {
323      var panel = faqTarget(toggle);
324      if (panel) {
325        if (panel.id) toggle.setAttribute("aria-controls", panel.id);
326        setFaqState(toggle, panel, false);
327      }
328    });
329  }
330
331  function handleFaqClick(event) {
332    var toggle = event.target.closest(".miro-faq .panel-title > a");
333    if (!toggle) return;
334    var panel = faqTarget(toggle);
335    if (!panel) return;
336
337    event.preventDefault();
338    event.stopPropagation();
339    if (typeof event.stopImmediatePropagation === "function") {
340      event.stopImmediatePropagation();
341    }
342
343    var root = toggle.closest(".miro-faq");
344    var shouldOpen = !panel.classList.contains("miro-faq-is-open");
345    root.querySelectorAll(".panel-title > a").forEach(function (otherToggle) {
346      var otherPanel = faqTarget(otherToggle);
347      if (otherPanel) setFaqState(otherToggle, otherPanel, false);
348    });
349    if (shouldOpen) setFaqState(toggle, panel, true);
350  }
351
352  function handleFaqKeydown(event) {
353    if (event.key !== " " && event.key !== "Spacebar") return;
354    var toggle = event.target.closest(".miro-faq .panel-title > a");
355    if (!toggle) return;
356    handleFaqClick(event);
357  }
358
359  function whenNear(element, run) {
360    if (!("IntersectionObserver" in window)) return run();
361    new IntersectionObserver(function (entries, observer) {
362      if (!entries[0].isIntersecting) return;
363      observer.disconnect();
364      run();
365    }, { rootMargin: "400px 0px" }).observe(element);
366  }
367
368  function init() {
369    // Cloning the cards twice and measuring them cost ~235 ms of blocking time
370    // at load for a section that sits far below the fold. Pay it on approach.
371    document.querySelectorAll(".miro-reviews-v2").forEach(function (root) {
372      whenNear(root, function () {
373        initReviewCarousel(root);
374      });
375    });
376    document.querySelectorAll(".miro-faq").forEach(initFaq);
377  }
378
379  document.addEventListener("click", handleFaqClick, true);
380  document.addEventListener("keydown", handleFaqKeydown, true);
381
382  if (document.readyState === "loading") {
383    document.addEventListener("DOMContentLoaded", init);
384  } else {
385    init();
386  }
387})();

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.