1(function ($, Drupal, once) { 2 'use strict'; 3 Drupal.behaviors.contentrowCarousel = { 4 attach: function (context, settings) { 5 const apiUrl = drupalSettings?.astro_radio_components?.api_url ?? ''; 6 7 8 9 const carousels = once('contentrowCarouselInit', '.content-row-items', context); 10 //console.log(carousels); 11 if (!carousels.length) { 12 return; 13 } 14 const settingsGroup = drupalSettings.astro_radix || {}; 15 const useFade = settingsGroup.enableFade === true; 16 const sliderSpeed = settingsGroup.sliderSpeed || 3000; 17 //console.log(carousels); 18 carousels.forEach(function (el) { 19 if (el.classList.contains('dummy-slider')) { 20 fetchData(apiUrl, el); 21 return 22 }; 23 24 initSwiper(el); 25 26 }); 27 function initSwiper(el) { 28 if (window.innerWidth > 960) { 29 return; 30 } 31 32 const hasThirteenLayout = el.querySelector('.thirteen-content-row'); 33 34 const slides = el.querySelectorAll('.swiper-slide'); 35 36 const totalItems = slides.length; 37 38 const swiperOptions = { 39 slidesPerView: 'auto', 40 autoplay: { 41 delay: sliderSpeed, 42 }, 43 }; 44 if (hasThirteenLayout) { 45 swiperOptions.spaceBetween = 8; 46 } 47 else {; 48 swiperOptions.spaceBetween = 12; 49 if (totalItems > 6) { 50 const rows = Math.ceil(totalItems / 6); 51 swiperOptions.grid = { 52 rows: rows, 53 fill: 'row', 54 }; 55 swiperOptions.autoplay.disableOnInteraction = false; 56 swiperOptions.autoplay.pauseOnMouseEnter = true; 57 swiperOptions.watchOverflow = true; 58 } 59 } 60 if (useFade && !swiperOptions.grid) { 61 swiperOptions.effect = 'fade'; 62 swiperOptions.fadeEffect = { 63 crossFade: true, 64 }; 65 } 66 67 new Swiper(el, swiperOptions); 68 } 69 70 71 async function fetchData(apiUrl, el) { 72 73 const controller = new AbortController(); 74 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout 75 76 let payload = null; 77 try { 78 const res = await fetch(apiUrl, { 79 method: 'GET', 80 headers: { 'Accept': 'application/json' }, 81 credentials: 'omit', 82 cache: 'no-store', 83 signal: controller.signal 84 }); 85 if (!res.ok) throw new Error('HTTP ' + res.status); 86 payload = await res.json(); 87 88 const ok = payload && Number(payload.statusCode) === 200 && Array.isArray(payload.data) && payload.data.length > 0; 89 if (!ok) { 90 91 return; 92 } 93 console.log(payload); 94 applyUpdates(el, payload); 95 } catch (e) { 96 // console.warn('Podcast API error:', e); 97 clearTimeout(timeoutId); 98 // Return the container (now placed) so caller can decide on fallback UI 99 return null; 100 } finally { 101 clearTimeout(timeoutId); 102 } 103 return payload; 104 } 105 106 107 function applyUpdates(el, payload) { 108 // A) Identify nodes 109 const templateHost = el.closest('.js-content-row-template'); 110 if (!templateHost) return; 111 112 const swiperCarousel = el.closest('.swiper-carousel'); 113 if (!swiperCarousel) return; 114 115 // B) Find the sibling `.content-row` 116 const contentRow = findSiblingContentRow(templateHost) || templateHost.parentElement || document.body; 117 118 try { 119 // 1) Normalize data array 120 const dataArray = Array.isArray(payload) 121 ? payload 122 : (payload && Array.isArray(payload.data) ? payload.data : []); 123 124 if (!dataArray.length) { 125 console.warn('[applyUpdates] No items in payload to render.'); 126 return; 127 } 128 129 // 2) Get the template slide HTML from inside this carousel 130 const wrapper = swiperCarousel.querySelector('.swiper-wrapper') || swiperCarousel; 131 const templateSlide = swiperCarousel.querySelector('.swiper-slide'); 132 let templateHTML; 133 134 if (templateSlide) { 135 templateHTML = templateSlide.outerHTML; 136 } 137 else { 138 return; 139 } 140 141 // 3) Replace existing slides with new ones 142 wrapper.innerHTML = ''; // replace, not append 143 const frag = document.createDocumentFragment(); 144 145 dataArray.forEach(item => { 146 147 const imgUrl = 148 item.squareImage || item.landscapeImage || item.portraitImage || ''; 149 const title = item.name || item.alternativeTitle || ''; 150 const link = item.showUrl || '#'; 151 let show_id = item.id || 'show_id'; 152 const slugtitle = item.alternativeTitle || item.name || ''; 153 154 if (typeof slugUtil?.generateSlug == 'function') { 155 show_id = slugUtil.generateSlug(show_id, slugtitle); 156 } 157 // Use the same token replacement style you used previously 158 let html = templateHTML; 159 html = html 160 .replace(/@img/g, imgUrl || '') 161 .replace(/@title/g, title || '') 162 .replace(/show_id_token/g, show_id || ''); 163 164 165 const slideEl = htmlToElement(html); 166 167 // Ensure the root has the correct class 168 if (!slideEl.classList.contains('swiper-slide')) { 169 slideEl.classList.add('swiper-slide'); 170 } 171 // If the template was initially in the DOM with real attributes that 172 // didnât include tokens (rare), apply a minimal DOM fallback as a safety net:
173 //domSafetyFallback(slideEl, { imgUrl, title, link }); 174 175 frag.appendChild(slideEl); 176 }); 177 178 wrapper.appendChild(frag); 179 180 // 4) Re-init Swiper safely (optional but recommended after rebuilding slides) 181 //safeReinitSwiper(swiperCarousel); 182 183 // 5) Move `.swiper-carousel` after `.content-row-head` (keep header intact) 184 const head = contentRow.querySelector('.content-row-head'); 185 186 if (head && head.parentNode === contentRow) { 187 contentRow.insertBefore(swiperCarousel, head.nextSibling); 188 } else { 189 contentRow.appendChild(swiperCarousel); 190 } 191 192 try { 193 const parent = contentRow.parentElement; 194 if (parent) { 195 // Option A: remove the first matching sibling under the same parent 196 const loader = parent.querySelector(':scope > .loading-state-wrapper'); 197 if (loader) { 198 loader.remove?.() || loader.parentNode?.removeChild(loader); 199 } 200 } 201 } catch (e) { 202 console.warn('[applyUpdates] Could not remove sibling .loading-state-wrapper:', e); 203 } 204 205 206 207 const container = contentRow.querySelector('.content-row-items'); 208 209 if (!container) { 210 console.warn('[applyUpdates] No .content-row-items container found to initialize Swiper.'); 211 return; 212 } 213 214 initSwiper(container); 215 } catch (err) { 216 console.error('[applyUpdates] Error while building/replacing swiper slides:', err); 217 } 218 219 // ========= Helpers ========= 220 function findSiblingContentRow(node) { 221 // 1) Direct prev/next siblings 222 const prev = previousMatching(node, '.content-row'); 223 if (prev) return prev; 224 const next = nextMatching(node, '.content-row'); 225 if (next) return next; 226 227 // 2) Scan parentâs children (same level) 228 if (node.parentElement) { 229 const siblings = Array.from(node.parentElement.children); 230 const found = siblings.find( 231 (n) => n !== node && n.classList && n.classList.contains('content-row') 232 ); 233 if (found) return found; 234 } 235 return null; 236 } 237 238 function previousMatching(node, selector) { 239 let cur = node.previousElementSibling; 240 while (cur) { 241 if (cur.matches && cur.matches(selector)) return cur; 242 cur = cur.previousElementSibling; 243 } 244 return null; 245 } 246 247 function nextMatching(node, selector) { 248 let cur = node.nextElementSibling; 249 while (cur) { 250 if (cur.matches && cur.matches(selector)) return cur; 251 cur = cur.nextElementSibling; 252 } 253 return null; 254 } 255 256 function htmlToElement(html) { 257 const t = document.createElement('template'); 258 t.innerHTML = html.trim(); 259 return t.content.firstElementChild; 260 } 261 262 // Minimal DOM safety in case the template had some static attributes without tokens 263 function domSafetyFallback(slideEl, { imgUrl, title, link }) { 264 // Anchor 265 const a = slideEl.querySelector('a'); 266 if (a && (!a.hasAttribute('href') || a.getAttribute('href').includes('@link'))) { 267 a.href = link || '#'; 268 } 269 270 // Image 271 const imgEl = slideEl.querySelector('img'); 272 if (imgEl) { 273 const needsSrc = 274 !imgEl.hasAttribute('src') || 275 imgEl.getAttribute('src').includes('@img') || 276 imgEl.getAttribute('src') === ''; 277 if (needsSrc && imgUrl) imgEl.setAttribute('src', imgUrl); 278 if (!imgEl.hasAttribute('alt') || imgEl.getAttribute('alt').includes('@title')) { 279 imgEl.setAttribute('alt', title || 'Image'); 280 } 281 if (imgEl.hasAttribute('data-src')) imgEl.setAttribute('data-src', imgUrl || ''); 282 if (imgEl.hasAttribute('srcset')) imgEl.setAttribute('srcset', imgUrl || ''); 283 } 284 285 // Title text 286 const titleEl = 287 slideEl.querySelector('[data-field="title"], .js-card-title, .card-title, h3, h4, .slide-title'); 288 if (titleEl) { 289 const current = (titleEl.textContent || '').trim(); 290 if (!current || current.includes('@title')) { 291 titleEl.textContent = title || ''; 292 } 293 } 294 } 295 296 function safeReinitSwiper(containerEl) { 297 // Destroy existing instance if any 298 if (containerEl.swiper && typeof containerEl.swiper.destroy === 'function') { 299 containerEl.swiper.destroy(true, true); 300 } 301 // Recreate if Swiper is available 302 if (typeof Swiper !== 'undefined') { 303 // eslint-disable-next-line no-undef 304 const instance = new Swiper(containerEl, { 305 slidesPerView: 2, 306 spaceBetween: 12, 307 loop: false, 308 watchSlidesProgress: true, 309 breakpoints: { 310 480: { slidesPerView: 2.2, spaceBetween: 12 }, 311 768: { slidesPerView: 3, spaceBetween: 16 }, 312 1024: { slidesPerView: 4, spaceBetween: 20 }, 313 }, 314 navigation: { 315 nextEl: containerEl.querySelector('.swiper-button-next'), 316 prevEl: containerEl.querySelector('.swiper-button-prev'), 317 }, 318 pagination: { 319 el: containerEl.querySelector('.swiper-pagination'), 320 clickable: true, 321 }, 322 }); 323 324 } else { 325 console.warn('[applyUpdates] Swiper constructor not found; skipped reinit.'); 326 } 327 } 328 } 329 330 331 332 333 } 334 }; 335 Drupal.behaviors.contentRowCarouselListing = { 336 attach: function (context, settings) {
337 const analyticsPage = settings?.analytics_data?.page; 338 const contentBrand = drupalSettings?.astro_analytics?.brand || ''; 339 if (!analyticsPage) return; 340 once('content_row_carousel', 'body', context).forEach(() => { 341 document.addEventListener('click', function (e) { 342 const anchor = e.target.closest('.content-row .content-row-item .content-row-wrap a'); 343 if (!anchor) return; 344 const rowItem = anchor.closest('.content-row-item'); 345 const data = { 346 event: 'content_click', 347 content_brand: contentBrand, 348 list_type: 'Content row', 349 }; 350 Drupal.astro_analytics.setCustomEvent('content_click', data, analyticsPage); 351 anchor.classList.add('analytics-processed'); 352 }, true); 353 }); 354 } 355 }; 356})(jQuery, Drupal, once);
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.