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.