1(function () { 2 "use strict"; 3 4 // DOM Elements - with null checks for AEM 5 const modal = document.querySelector(".new-cars-shared-features-modal"); 6 const modalClose = modal ? modal.querySelector(".new-cars-shared-features-modal-close") : null; 7 const modalContent = modal ? modal.querySelector(".new-cars-shared-features-modal-content") : null; 8 const cardWrapper = document.querySelector(".new-cars-shared-features-cards"); 9 const arrowLeft = document.querySelector(".new-cars-shared-features-arrow-left"); 10 const arrowRight = document.querySelector(".new-cars-shared-features-arrow-right"); 11 const arrowWrapper = document.querySelector(".new-cars-shared-features-arrow-wrapper"); 12 const cards = document.querySelectorAll(".new-cars-shared-features-card"); 13 14 if (!cardWrapper || cards.length === 0) { 15 return; 16 } 17 18 /* Constants -------------------------------------------------------------------*/ 19 const CARD_WIDTH = 236; 20 const EXPANDED_CARD_WIDTH = 500; 21 const MOBILE_BREAKPOINT = 767; 22 const DESKTOP_BREAKPOINT = 961; 23 const DRAG_THRESHOLD = 5; 24 const INITIAL_CARD_TO_SHOW = 0; 25 26 const totalCardsWidth = cards.length * CARD_WIDTH + EXPANDED_CARD_WIDTH; 27 28 let isDragging = false; 29 let startX = 0; 30 let scrollLeft = 0; 31 let hasDragged = false; 32 let lastFocusedElement = null; 33 34 // On mobile there should not be a default card open. 35 const cardToShow = cards[INITIAL_CARD_TO_SHOW]; 36 if (cardToShow && !isMobile()) { 37 cardToShow.classList.add("active"); 38 } 39 40 /* Utility Functions --------------------------------------------------------*/ 41 function isMobile() { 42 return window.innerWidth <= MOBILE_BREAKPOINT; 43 } 44 45 function scrollToCard(card) { 46 if (card && typeof card.scrollIntoView === "function") { 47 card.scrollIntoView({ 48 behavior: "smooth", 49 block: "nearest", 50 inline: "center" 51 }); 52 } 53 } 54 55 function setActiveCard(targetCard) { 56 cards.forEach(function (card) { 57 card.classList.remove("active"); 58 }); 59 if (targetCard) { 60 targetCard.classList.add("active"); 61 targetCard.focus(); 62 } 63 } 64 65 /* Focus Trap Functions -------------------------------------------------------*/ 66 function getFocusableElements(element) { 67 const focusableElementsString = 68 'a[href], area[href], input:not([disabled]):not([type="hidden"]):not([aria-hidden]), select:not([disabled]):not([aria-hidden]), textarea:not([disabled]):not([aria-hidden]), button:not([disabled]):not([aria-hidden]), iframe, object, embed, [contenteditable], [tabindex]:not([tabindex^="-"])'; 69 return element.querySelectorAll(focusableElementsString); 70 } 71 72 function trapFocus(event) { 73 if (event.key !== "Tab" || !modalContent) return; 74 75 const focusableElements = getFocusableElements(modalContent); 76 const firstElement = focusableElements[0]; 77 const lastElement = focusableElements[focusableElements.length - 1]; 78 79 if (event.shiftKey) { 80 if (document.activeElement === firstElement || document.activeElement === modalContent) { 81 event.preventDefault(); 82 lastElement.focus(); 83 } 84 } else if (document.activeElement === lastElement) { 85 event.preventDefault(); 86 firstElement.focus(); 87 } 88 } 89 90 function handleModalKeydown(event) { 91 if (event.key === "Escape") { 92 hideMobileModal(); 93 return; 94 } 95 96 trapFocus(event); 97 } 98 99 function setInitialModalFocus() { 100 if (!modalContent) return; 101 modalContent.focus(); 102 } 103 104 /* Modal Functions -----------------------------------------------------------*/ 105 function showMobileModal() { 106 if (!modal) return; 107 108 lastFocusedElement = document.activeElement; 109 110 modal.classList.add("active"); 111 112 setTimeout(() => { 113 setInitialModalFocus(); 114 }, 50); 115 116 document.addEventListener("keydown", handleModalKeydown); 117 } 118 119 function hideMobileModal() { 120 if (!modal) return; 121 122 modal.classList.remove("active"); 123 cards.forEach(function (card) { 124 card.classList.remove("active"); 125 }); 126 127 document.removeEventListener("keydown", handleModalKeydown); 128 129 if (lastFocusedElement) { 130 lastFocusedElement.focus(); 131 lastFocusedElement = null; 132 } 133 } 134 135 /* Arrow Functions -----------------------------------------------------------*/ 136 function checkScrollArrows() { 137 if (!arrowLeft || !arrowRight || !cardWrapper) return; 138 139 const hasScrollbar = cardWrapper.scrollWidth > cardWrapper.clientWidth; 140 141 // Hide/show arrow wrapper based on scrollbar presence 142 if (arrowWrapper) { 143 if (hasScrollbar) { 144 arrowWrapper.style.display = "flex"; 145
146 // Check scroll position for individual arrow states 147 const isAtStart = cardWrapper.scrollLeft <= 0; 148 const isAtEnd = cardWrapper.scrollLeft >= cardWrapper.scrollWidth - cardWrapper.clientWidth - 1; 149 150 if (isAtStart) { 151 arrowLeft.classList.add("disabled"); 152 } else { 153 arrowLeft.classList.remove("disabled"); 154 } 155 156 if (isAtEnd) { 157 arrowRight.classList.add("disabled"); 158 } else { 159 arrowRight.classList.remove("disabled"); 160 } 161 } else { 162 // No scrollbar, hide arrows completely 163 arrowWrapper.style.display = "none"; 164 } 165 } 166 } 167 168 function updateCardContainerLayout() { 169 const screenWidth = window.innerWidth; 170 171 if (cardWrapper) { 172 // Calculate if content fits within viewport 173 const hasScrollbar = cardWrapper.scrollWidth > cardWrapper.clientWidth; 174 175 if (!hasScrollbar && screenWidth > DESKTOP_BREAKPOINT && screenWidth > totalCardsWidth) { 176 // Center the grid when content fits and on larger screens 177 cardWrapper.style.justifyContent = "center"; 178 } else { 179 // Use default grid behavior (start alignment) when scrolling is needed 180 cardWrapper.style.justifyContent = "start"; 181 } 182 } 183 } 184 185 /* Drag Handlers --------------------------------------------------------------*/ 186 function handleMouseDown(e) { 187 // Only allow dragging if there's a scrollbar 188 const hasScrollbar = cardWrapper.scrollWidth > cardWrapper.clientWidth; 189 if (!hasScrollbar) return; 190 191 isDragging = true; 192 hasDragged = false; 193 startX = e.pageX - cardWrapper.offsetLeft; 194 scrollLeft = cardWrapper.scrollLeft; 195 cardWrapper.style.userSelect = "none"; 196 cardWrapper.classList.add("dragging"); 197 } 198 199 function handleMouseUp() { 200 isDragging = false; 201 cardWrapper.style.userSelect = ""; 202 cardWrapper.classList.remove("dragging"); 203 } 204 205 function handleMouseLeave() { 206 isDragging = false; 207 cardWrapper.style.userSelect = ""; 208 cardWrapper.classList.remove("dragging"); 209 } 210 211 function handleMouseMove(e) { 212 if (!isDragging) return; 213 e.preventDefault(); 214 215 const x = e.pageX - cardWrapper.offsetLeft; 216 const walk = x - startX; 217 218 if (Math.abs(walk) > DRAG_THRESHOLD) { 219 hasDragged = true; 220 } 221 222 cardWrapper.scrollLeft = scrollLeft - walk; 223 } 224 225 /* Card Click Handler ----------------------------------------------------------*/ 226 function createCardClickHandler(card, index) { 227 return function (e) { 228 const headlineElement = card.querySelector(".new-cars-shared-features-headline"); 229 const cardHeadline = headlineElement ? headlineElement.textContent.trim() : ""; 230 231 T1.channels.statistics.publish(dxp?.constants?.TOPICS_T1.STATS_TRACK, { 232 extraData: { 233 event: { 234 name: "componentevent", 235 componentName: "shared-features", 236 componentGroup: "content", 237 action: "expand-card", 238 label: cardHeadline || `card-${index}` 239 } 240 } 241 }); 242 243 if (hasDragged) { 244 e.preventDefault(); 245 e.stopPropagation(); 246 return; 247 } 248 249 const isCurrentlyActive = card.classList.contains("active"); 250 251 // Mobile behavior 252 if (isMobile()) { 253 if (!isCurrentlyActive && modal) {
254 setActiveCard(card); 255 256 // Update modal content from data attributes 257 const description = card.getAttribute("data-description"); 258 const linkUrl = card.getAttribute("data-link-url"); 259 const linkLabel = card.getAttribute("data-link-label"); 260 const externalLink = card.getAttribute("data-external-link"); 261 262 if (modal) { 263 const modalDescription = modal.querySelector(".new-cars-shared-features-modal-description"); 264 const modalLink = modal.querySelector(".new-cars-shared-features-modal-link"); 265 const externalLinkIcon = modal.querySelector(".external-link-icon"); 266 267 if (modalDescription) { 268 modalDescription.innerHTML = description || ""; 269 } 270 271 if (modalLink) { 272 if (linkUrl) { 273 modalLink.href = linkUrl; 274 modalLink.style.display = "block"; 275 const linkLabelElement = modalLink.querySelector(".link-label"); 276 if (linkLabelElement) { 277 linkLabelElement.textContent = linkLabel || ""; 278 } 279 280 if (externalLink === "true" && externalLinkIcon) { 281 externalLinkIcon.style.display = "inline"; 282 } else if (externalLinkIcon) { 283 externalLinkIcon.style.display = "none"; 284 } 285 } else { 286 modalLink.style.display = "none"; 287 } 288 } 289 } 290 291 showMobileModal(); 292 scrollToCard(card); 293 } 294 return; 295 } 296 297 // Desktop behavior remains unchanged 298 if (isCurrentlyActive) { 299 card.classList.remove("active"); 300 const nextIndex = (index + 1) % cards.length; 301 const nextCard = cards[nextIndex]; 302 nextCard.classList.add("active");
303 setActiveCard(nextCard); 304 scrollToCard(nextCard); 305 } else { 306 setActiveCard(card); 307 scrollToCard(card); 308 } 309 }; 310 } 311 312 /* Arrow Click Handlers --------------------------------------------------------*/ 313 function handleLeftArrowClick() { 314 if (arrowLeft && !arrowLeft.classList.contains("disabled")) { 315 const scrollDistance = totalCardsWidth / cards.length; 316 cardWrapper.scrollBy({ 317 left: -scrollDistance, 318 behavior: "smooth" 319 }); 320 321 T1.channels.statistics.publish(dxp?.constants?.TOPICS_T1.STATS_TRACK, { 322 extraData: { 323 event: { 324 name: "componentevent", 325 componentName: "shared-features", 326 componentGroup: "content", 327 action: "browse-content" 328 } 329 } 330 }); 331 } 332 } 333 334 function handleRightArrowClick() { 335 if (arrowRight && !arrowRight.classList.contains("disabled")) { 336 const scrollDistance = totalCardsWidth / cards.length; 337 cardWrapper.scrollBy({ 338 left: scrollDistance, 339 behavior: "smooth" 340 }); 341 342 T1.channels.statistics.publish(dxp?.constants?.TOPICS_T1.STATS_TRACK, { 343 extraData: { 344 event: { 345 name: "componentevent", 346 componentName: "shared-features", 347 componentGroup: "content", 348 action: "browse-content" 349 } 350 } 351 }); 352 } 353 } 354 355 /* Initialize Event Listeners -------------------------------------------------*/ 356 function initializeEventListeners() { 357 // Modal events 358 if (modalClose) { 359 modalClose.addEventListener("click", hideMobileModal); 360 } 361 if (modal) { 362 const modalLink = modal.querySelector(".new-cars-shared-features-modal-link"); 363 if (modalLink) { 364 modalLink.addEventListener("click", hideMobileModal); 365 } 366 367 modal.addEventListener("click", function (e) { 368 if (e.target === modal) { 369 hideMobileModal(); 370 } 371 }); 372 } 373 374 // Drag events 375 cardWrapper.addEventListener("mousedown", handleMouseDown); 376 cardWrapper.addEventListener("mouseleave", handleMouseLeave); 377 cardWrapper.addEventListener("mouseup", handleMouseUp); 378 cardWrapper.addEventListener("mousemove", handleMouseMove); 379 380 // Card click events 381 cards.forEach(function (card, index) { 382 const animationDelay = index * 100; 383 card.style.setProperty("--shared-features-animation-delay", animationDelay + "ms"); 384 card.addEventListener("click", createCardClickHandler(card, index)); 385 386 const readMoreButton = card.querySelector(".new-cars-shared-features-bottom-readmore"); 387 if (readMoreButton) { 388 readMoreButton.addEventListener("click", function (e) { 389 e.stopPropagation(); // Prevent card click from firing 390 }); 391 } 392 }); 393 394 // Arrow events 395 if (arrowLeft) { 396 arrowLeft.addEventListener("click", handleLeftArrowClick); 397 } 398 if (arrowRight) { 399 arrowRight.addEventListener("click", handleRightArrowClick); 400 } 401 402 // Window events 403 window.addEventListener("resize", function () { 404 updateCardContainerLayout(); 405 checkScrollArrows(); 406 }); 407 408 cardWrapper.addEventListener("scroll", function () { 409 checkScrollArrows(); 410 }); 411 } 412 413 /* Initialize everything ------------------------------------------------------*/ 414 function init() { 415 initializeEventListeners(); 416 updateCardContainerLayout(); 417 checkScrollArrows(); 418 } 419 420 init(); 421})();
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.