1/* Coverflow navigation around the shared company-card renderer. 2 * The carousel owns only selection and motion. The card component remains the 3 * sole source of front/back markup on both the homepage and company pages. */ 4(function () { 5 "use strict"; 6 7 var AUTO_FLIP_MS = 2000; 8 var MAX_CARDS = 7; 9 var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 10 11 function t(source) { 12 return window.XAgentI18n && window.XAgentI18n.t ? window.XAgentI18n.t(source) : source; 13 } 14 15 function isInteractive(target) { 16 return Boolean(target && target.closest && target.closest("a, button, input, select, textarea")); 17 } 18 19 function installCard(card, isActive) { 20 if (!card || card.__businessCardController) return card && card.__businessCardController; 21 var front = card.querySelector(".namecard-front"); 22 var back = card.querySelector(".namecard-back"); 23 var autoFlipTimer = 0; 24 25 function active() { 26 return typeof isActive === "function" ? isActive() : true; 27 } 28 29 function cancelAutoFlip() { 30 if (!autoFlipTimer) return; 31 window.clearTimeout(autoFlipTimer); 32 autoFlipTimer = 0; 33 } 34 35 function render(flipped) { 36 var enabled = active(); 37 card.classList.toggle("is-flipped", Boolean(flipped)); 38 card.dataset.cardSide = flipped ? "agent" : "human"; 39 card.tabIndex = enabled ? 0 : -1; 40 card.setAttribute("aria-label", flipped ? t("View human side") : t("View Agent side")); 41 if (front) front.setAttribute("aria-hidden", enabled && !flipped ? "false" : "true"); 42 if (back) back.setAttribute("aria-hidden", enabled && flipped ? "false" : "true"); 43 [front, back].forEach(function (face, index) { 44 if (!face) return; 45 var faceActive = enabled && (flipped ? index === 1 : index === 0); 46 face.querySelectorAll("a, button").forEach(function (control) { 47 control.tabIndex = faceActive ? 0 : -1; 48 }); 49 }); 50 card.querySelectorAll("[data-copy-agent-id]").forEach(function (button) { 51 button.setAttribute("aria-label", t("Copy Agent ID")); 52 }); 53 } 54 55 function scheduleAutoFlip() { 56 cancelAutoFlip(); 57 if (reduceMotion || !active()) return; 58 autoFlipTimer = window.setTimeout(function () { 59 autoFlipTimer = 0; 60 if (active()) render(true); 61 }, AUTO_FLIP_MS); 62 } 63 64 function toggleCard() { 65 if (!active()) return; 66 cancelAutoFlip(); 67 render(!card.classList.contains("is-flipped")); 68 } 69 70 function onClick(event) { 71 if (isInteractive(event.target) || !active()) return; 72 toggleCard(); 73 } 74 75 function onKeydown(event) { 76 if (event.target !== card || !active() || (event.key !== "Enter" && event.key !== " ")) return; 77 event.preventDefault(); 78 toggleCard(); 79 } 80 81 card.addEventListener("click", onClick); 82 card.addEventListener("keydown", onKeydown); 83 card.addEventListener("pointerdown", cancelAutoFlip); 84 card.addEventListener("focusin", cancelAutoFlip); 85 86 var controller = { 87 activate: function () { 88 render(false); 89 scheduleAutoFlip(); 90 }, 91 deactivate: function () { 92 cancelAutoFlip(); 93 render(false); 94 }, 95 refresh: function () { 96 render(card.classList.contains("is-flipped")); 97 }, 98 destroy: function () { 99 cancelAutoFlip(); 100 card.removeEventListener("click", onClick); 101 card.removeEventListener("keydown", onKeydown); 102 delete card.__businessCardController; 103 } 104 }; 105 card.__businessCardController = controller; 106 render(false); 107 if (active()) scheduleAutoFlip(); 108 return controller; 109 } 110 111 function safeLocalPath(value) { 112 try { 113 var parsed = new URL(String(value || ""), window.location.href); 114 if (parsed.origin !== window.location.origin) return ""; 115 return parsed.pathname + parsed.search + parsed.hash; 116 } catch (_error) { 117 return ""; 118 } 119 } 120 121 function normalizeEntry(entry) { 122 var host = String((entry || {}).host || "").trim().toLowerCase(); 123 var sourceUrl = safeLocalPath((entry || {}).source_url); 124 var detailUrl = safeLocalPath((entry || {}).detail_url); 125 if (!host || !sourceUrl || !detailUrl) return null; 126 return { 127 host: host, 128 sourceUrl: sourceUrl, 129 detailUrl: detailUrl, 130 preload: (entry || {}).preload || null 131 }; 132 } 133
134 function createPreparedSlide(entry) { 135 var slide = document.createElement("article"); 136 slide.className = "business-card-coverflow-slide"; 137 slide.setAttribute("data-coverflow-slide", ""); 138 139 var card = document.createElement("div"); 140 card.className = "company-card-pair business-card"; 141 card.setAttribute("data-company-card", ""); 142 card.setAttribute("data-company-card-pair", ""); 143 card.setAttribute("data-company-card-flip", ""); 144 card.setAttribute("data-business-card", ""); 145 card.setAttribute("tabindex", "-1"); 146 card.setAttribute("role", "group"); 147 card.setAttribute("aria-label", t("View Agent side")); 148 149 var detail = document.createElement("a"); 150 detail.className = "business-card-detail-link"; 151 detail.setAttribute("data-coverflow-detail", ""); 152 detail.href = entry.detailUrl; 153 detail.innerHTML = t("View full company card") + ' <span aria-hidden="true">â</span>'; 154 155 slide.append(card, detail); 156 window.XAgentCompanyCard.mount(card); 157 if (entry.preload && window.XAgentCompanyCard.renderBootstrap) { 158 window.XAgentCompanyCard.renderBootstrap(card, entry.preload); 159 } 160 card.setAttribute("data-company-card-source", entry.sourceUrl); 161 return { entry: entry, slide: slide, card: card, detail: detail }; 162 } 163 164 function readPreloadEntries(stage) { 165 var source = stage.querySelector("[data-featured-card-preload]"); 166 if (!source) return []; 167 try { 168 var entries = JSON.parse(source.textContent || "[]"); 169 return Array.isArray(entries) 170 ? entries.map(normalizeEntry).filter(Boolean).slice(0, MAX_CARDS) 171 : []; 172 } catch (_error) { 173 return []; 174 } 175 } 176 177 function initCarousel(stage) { 178 var viewport = stage.querySelector("[data-coverflow-viewport]"); 179 var track = stage.querySelector("[data-coverflow-track]"); 180 var previous = stage.querySelector("[data-coverflow-previous]"); 181 var next = stage.querySelector("[data-coverflow-next]"); 182 var dots = stage.querySelector("[data-coverflow-dots]"); 183 var status = stage.querySelector("[data-coverflow-status]"); 184 var message = stage.querySelector("[data-coverflow-message]"); 185 if (!viewport || !track || !dots || !window.XAgentCompanyCard) return; 186 187 var slides = []; 188 var controllers = []; 189 var activeIndex = 0; 190 var pointerStart = null; 191 var suppressClick = false; 192 193 function cardName(index) { 194 return slides[index] ? slides[index].entry.host : ""; 195 } 196 197 function render(announce) { 198 slides.forEach(function (item, index) { 199 var offset = index - activeIndex; 200 var distance = Math.min(Math.abs(offset), 3); 201 var translate = offset * 64; 202 var rotate = offset * -30; 203 var scale = 1 - distance * 0.12; 204 var visible = distance <= 2; 205 item.slide.style.transform = "translate3d(calc(-50% + " + translate + "%), 0, 0) rotateY(" + rotate + "deg) scale(" + scale + ")"; 206 item.slide.style.zIndex = String(20 - distance); 207 item.slide.classList.toggle("is-active", index === activeIndex); 208 item.slide.classList.toggle("is-visible", visible); 209 item.slide.setAttribute("aria-current", index === activeIndex ? "true" : "false"); 210 item.slide.setAttribute("aria-hidden", index === activeIndex ? "false" : "true"); 211 item.detail.tabIndex = index === activeIndex ? 0 : -1; 212 if (index === activeIndex) controllers[index].activate(); 213 else controllers[index].deactivate(); 214 }); 215 dots.querySelectorAll("button").forEach(function (dot, index) { 216 dot.classList.toggle("is-active", index === activeIndex); 217 if (index === activeIndex) dot.setAttribute("aria-current", "true"); 218 else dot.removeAttribute("aria-current"); 219 }); 220 var disabled = slides.length < 2; 221 if (previous) previous.disabled = disabled; 222 if (next) next.disabled = disabled; 223 if (status && announce !== false && slides.length) { 224 status.textContent = (activeIndex + 1) + " / " + slides.length + ": " + cardName(activeIndex); 225 } 226 } 227 228 function setActive(index, announce) { 229 if (!slides.length) return; 230 activeIndex = (index + slides.length) % slides.length; 231 render(announce); 232 } 233 234 function register(items) {
235 controllers.forEach(function (controller) { controller.destroy(); }); 236 controllers = []; 237 slides = items; 238 activeIndex = 0; 239 track.replaceChildren(); 240 dots.replaceChildren(); 241 slides.forEach(function (item, index) { 242 item.slide.dataset.coverflowIndex = String(index); 243 track.appendChild(item.slide); 244 controllers.push(installCard(item.card, function () { return index === activeIndex; })); 245 item.slide.addEventListener("click", function (event) { 246 if (isInteractive(event.target) || index === activeIndex) return; 247 setActive(index, true); 248 }); 249 var dot = document.createElement("button"); 250 dot.type = "button"; 251 dot.setAttribute("aria-label", t("Show company card") + ": " + item.entry.host); 252 dot.addEventListener("click", function () { setActive(index, true); }); 253 dots.appendChild(dot); 254 }); 255 render(true); 256 stage.dataset.carouselState = "ready"; 257 if (message) message.hidden = true; 258 } 259 260 function findSlide(host) { 261 return slides.find(function (item) { return item.entry.host === host; }); 262 } 263 264 function hydrate(item) { 265 return window.XAgentCompanyCard.loadSource(item.card, item.entry.sourceUrl) 266 .then(function () { return item; }) 267 .catch(function () { return null; }); 268 } 269 270 if (previous) previous.addEventListener("click", function () { setActive(activeIndex - 1, true); }); 271 if (next) next.addEventListener("click", function () { setActive(activeIndex + 1, true); }); 272 viewport.addEventListener("keydown", function (event) { 273 if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; 274 event.preventDefault(); 275 setActive(activeIndex + (event.key === "ArrowRight" ? 1 : -1), true); 276 }); 277 viewport.addEventListener("pointerdown", function (event) { 278 if (isInteractive(event.target)) return; 279 pointerStart = { x: event.clientX, y: event.clientY }; 280 }); 281 viewport.addEventListener("pointerup", function (event) { 282 if (!pointerStart) return; 283 var deltaX = event.clientX - pointerStart.x; 284 var deltaY = event.clientY - pointerStart.y; 285 pointerStart = null; 286 if (Math.abs(deltaX) < 48 || Math.abs(deltaX) < Math.abs(deltaY)) return; 287 suppressClick = true; 288 setActive(activeIndex + (deltaX < 0 ? 1 : -1), true); 289 window.setTimeout(function () { suppressClick = false; }, 0); 290 }); 291 viewport.addEventListener("pointercancel", function () { pointerStart = null; }); 292 viewport.addEventListener("click", function (event) { 293 if (!suppressClick) return; 294 event.preventDefault(); 295 event.stopImmediatePropagation(); 296 }, true); 297 298 var collectionSource = safeLocalPath(stage.getAttribute("data-company-card-carousel-source")); 299 if (!collectionSource) return; 300 var preloadedEntries = readPreloadEntries(stage); 301 if (preloadedEntries.length) { 302 register(preloadedEntries.map(createPreparedSlide)); 303 } else { 304 stage.dataset.carouselState = "loading"; 305 if (message) { 306 message.hidden = false; 307 message.textContent = t("Loading featured company cards"); 308 } 309 } 310 fetch(collectionSource, { headers: { Accept: "application/json" }, credentials: "omit" }) 311 .then(function (response) { 312 return response.json().then(function (payload) { 313 if (!response.ok || !payload || !payload.ok || !Array.isArray(payload.entries)) { 314 throw new Error("featured_cards_unavailable"); 315 } 316 return payload.entries.map(normalizeEntry).filter(Boolean).slice(0, MAX_CARDS); 317 }); 318 }) 319 .then(function (entries) { 320 var sameRoster = entries.length === slides.length && entries.every(function (entry) { 321 return Boolean(findSlide(entry.host)); 322 }); 323 if (!sameRoster) { 324 register(entries.map(createPreparedSlide)); 325 } else { 326 entries.forEach(function (entry) { 327 var item = findSlide(entry.host); 328 item.entry.sourceUrl = entry.sourceUrl; 329 item.entry.detailUrl = entry.detailUrl; 330 item.detail.href = entry.detailUrl; 331 }); 332 } 333 return Promise.all(slides.map(hydrate)); 334 }) 335 .then(function (items) {
336 var loaded = items.filter(Boolean); 337 if (!slides.length || !loaded.length) throw new Error("featured_cards_unavailable"); 338 stage.dataset.carouselState = "ready"; 339 if (message) message.hidden = true; 340 }) 341 .catch(function () { 342 if (slides.length) { 343 stage.dataset.carouselState = "ready"; 344 if (message) message.hidden = true; 345 return; 346 } 347 controllers.forEach(function (controller) { controller.destroy(); }); 348 controllers = []; 349 slides = []; 350 track.replaceChildren(); 351 dots.replaceChildren(); 352 if (previous) previous.disabled = true; 353 if (next) next.disabled = true; 354 stage.dataset.carouselState = "fallback"; 355 var unavailable = t("Featured company cards are temporarily unavailable"); 356 if (message) { 357 message.hidden = false; 358 message.textContent = unavailable; 359 } 360 if (status) status.textContent = unavailable; 361 }); 362 363 window.addEventListener("xagent:languagechange", function () { 364 if (message && stage.dataset.carouselState !== "ready") { 365 message.textContent = t(stage.dataset.carouselState === "loading" 366 ? "Loading featured company cards" 367 : "Featured company cards are temporarily unavailable"); 368 } 369 controllers.forEach(function (controller) { controller.refresh(); }); 370 slides.forEach(function (item) { 371 item.detail.innerHTML = t("View full company card") + ' <span aria-hidden="true">â</span>'; 372 }); 373 render(false); 374 }); 375 } 376 377 document.querySelectorAll("[data-company-card-carousel]").forEach(initCarousel); 378 document.querySelectorAll("[data-business-card]").forEach(function (card) { 379 if (!card.closest("[data-company-card-carousel]")) installCard(card); 380 }); 381 window.XAgentBusinessCard = { install: installCard }; 382})();
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.