1/** 2 * @file 3 * Mixpanel tracking for media and menu interactions. 4 */ 5(function (Drupal) { 6 'use strict'; 7 Drupal.behaviors.cnarMediaMixpanelTracking = { 8 attach: function (context, settings) { 9 // Only run once, on initial page load 10 if (context !== document) { 11 return; 12 } 13 14 const playerState = { 15 currentMediaId: null, 16 isPlayerOpen: false, 17 isRadioPlaying: false, 18 lastEvents: { 19 thumbnail: 0, 20 podcast: 0, 21 radio: 0, 22 video: 0, 23 menuClick: 0 24 } 25 }; 26 27 function formatDuration(duration) { 28 if (!duration) return "0.00 Minutes"; 29 30 if (typeof duration === 'string' && duration.includes(':')) { 31 const parts = duration.split(':').map(part => parseInt(part) || 0); 32 if (parts.length === 3) { 33 const h = parts[0]; 34 const m = parts[1]; 35 const s = parts[2]; 36 const totalMinutes = (h * 60) + m + (s / 60); 37 return totalMinutes.toFixed(2) + " Minutes"; 38 } else if (parts.length === 2) { 39 const m = parts[0]; 40 const s = parts[1]; 41 const totalMinutes = m + (s / 60); 42 return totalMinutes.toFixed(2) + " Minutes"; 43 } 44 } 45 46 const seconds = parseFloat(duration); 47 if (!isNaN(seconds)) { 48 return (seconds / 60).toFixed(2) + " Minutes"; 49 } 50 51 const minutesMatch = String(duration).match(/(\d+)\s*min/i); 52 if (minutesMatch && minutesMatch[1]) { 53 return parseFloat(minutesMatch[1]).toFixed(2) + " Minutes"; 54 } 55 56 return "0.00 Minutes"; 57 } 58 59 function parsePresenters(text) { 60 if (!text) return []; 61 return String(text).split(',') 62 .map(item => item.trim()) 63 .filter(item => item.length > 0); 64 } 65 66 function findPodcastData(element) { 67 while (element && element !== document.body) { 68 if (element.hasAttribute('data-podcast-id') || 69 element.hasAttribute('data-program-id') || 70 element.hasAttribute('data-podcast-title')) { 71 return element; 72 } 73 element = element.parentElement; 74 } 75 return null; 76 } 77 78 function trackEvent(name, properties, mediaId) { 79 try { 80 const now = Date.now(); 81 82 if (name === 'listen_thumbnail_click' && now - playerState.lastEvents.thumbnail < 300) { 83 return; 84 } 85 86 if (name === 'podcast_start' && now - playerState.lastEvents.podcast < 300) { 87 return; 88 } 89 90 if (name === 'radio_start' && now - playerState.lastEvents.radio < 300) { 91 return; 92 } 93 94 if (name === 'watch_play' && now - playerState.lastEvents.video < 300) { 95 return; 96 } 97 98 if (name === 'content_click' && now - playerState.lastEvents.menuClick < 300) { 99 return; 100 } 101 102 if (name === 'listen_thumbnail_click') { 103 playerState.lastEvents.thumbnail = now; 104 } else if (name === 'podcast_start') { 105 playerState.lastEvents.podcast = now; 106 } else if (name === 'radio_start') { 107 playerState.lastEvents.radio = now; 108 } else if (name === 'watch_play') { 109 playerState.lastEvents.video = now; 110 } else if (name === 'content_click') { 111 playerState.lastEvents.menuClick = now; 112 } 113 114 if (window.mixpanelConfig && typeof window.mixpanelConfig.trackEvent === 'function') { 115 window.mixpanelConfig.trackEvent(name, properties); 116 } 117 } catch (e) { 118 console.error('Error tracking event:', e); 119 } 120 } 121 122 function handleThumbnailClick(event) { 123 const container = findPodcastData(event.target); 124 if (!container) return; 125 126 const properties = { 127 title: container.getAttribute('data-podcast-title') || '', 128 category: container.getAttribute('data-all-categories') || '', 129 page_name: document.title || '' 130 }; 131 132 const mediaId = container.getAttribute('data-podcast-id') || 133 container.getAttribute('data-program-id') || ''; 134 135 trackEvent('listen_thumbnail_click', properties, mediaId); 136 } 137 138 function formatDateToISO(timestamp) { 139 if (!timestamp) return ''; 140 141 try { 142 const date = new Date(parseInt(timestamp) * 1000); 143 return date.toISOString().split('.')[0]; 144 } catch (e) { 145 console.error('Error formatting date:', e); 146 return ''; 147 } 148 } 149 150 function handlePodcastStart(event) { 151 const container = findPodcastData(event.target); 152 if (!container) return; 153 154 const mediaId = container.getAttribute('data-podcast-id') || 155 container.getAttribute('data-program-id') || ''; 156 157 if (!mediaId) return; 158 159 if (playerState.currentMediaId === mediaId && playerState.isPlayerOpen) { 160 return; 161 } 162 163 playerState.currentMediaId = mediaId; 164 playerState.isPlayerOpen = true; 165 166 let isFromPill = false;
167 let element = event.target; 168 169 while (element && element !== container) { 170 if (element.classList.contains('list-object__datetime-duration')) { 171 isFromPill = true; 172 break; 173 } 174 element = element.parentElement; 175 } 176 177 let durationValue = container.getAttribute('data-duration') || ''; 178 179 if (!durationValue) { 180 const durationElem = container.querySelector('.duration'); 181 if (durationElem && durationElem.textContent) { 182 durationValue = durationElem.textContent.trim(); 183 } 184 } 185 186 const releaseDateValue = container.getAttribute('data-release-date') || ''; 187 const formattedReleaseDate = formatDateToISO(releaseDateValue); 188 189 const properties = { 190 source: isFromPill ? 'Pill' : '', 191 host: parsePresenters(container.getAttribute('data-presenters') || ''), 192 duration: formatDuration(durationValue) || '0.00 Minutes', 193 show_title: container.getAttribute('data-podcast-name') || '', 194 show_id: container.getAttribute('data-program-id') || '', 195 episode_title: container.getAttribute('data-podcast-title') || '', 196 episode_id: container.getAttribute('data-podcast-id') || '', 197 category: container.getAttribute('data-all-categories') || '', 198 season_number: parseInt(container.getAttribute('data-program-season')) || null, 199 published_date: formattedReleaseDate || '', 200 language: 'English' 201 }; 202 203 trackEvent('podcast_start', properties, mediaId); 204 } 205 206 function handleRadioStart(event) { 207 const audioPlayer = document.getElementById('sticky-audio-player'); 208 if (!audioPlayer) return; 209 const radioPlayer = document.getElementById('sticky-radio-player').classList.contains('hidden'); 210 if (!radioPlayer) return; 211 212 const audioSrc = audioPlayer.getAttribute('src') || ''; 213 214 const isLive = audioSrc.includes('live') || !!document.querySelector('.live-indicator, .stream-live, .radio-live'); 215 216 const properties = { 217 liveradio: isLive 218 }; 219 220 trackEvent('radio_start', properties, ''); 221 } 222 223 function handleVideoEvent(el, status) { 224 const container = el.closest('[data-video-id]'); 225 if (!container) { 226 return; 227 } 228 229 const videoId = container.getAttribute('data-video-id'); 230 if (!videoId) { 231 return; 232 } 233 234 let episodeTitle = ''; 235 const mediaObject = container.closest('.media-object'); 236 if (mediaObject) { 237 const headingElement = mediaObject.querySelector('.list-object__heading'); 238 if (headingElement) { 239 const anchorTag = headingElement.querySelector('a'); 240 if (anchorTag) { 241 episodeTitle = anchorTag.textContent.trim(); 242 } 243 } 244 } 245 246 const properties = { 247 video_id: videoId || '', 248 video_status: status || '', 249 page_name: document.title || '', 250 episode_title: episodeTitle || '', 251 episode_number: '', 252 follow: false 253 }; 254 255 trackEvent('watch_play', properties, videoId); 256 } 257 258 function initPlayerStateTracking() { 259 function findPlayerElement() { 260 return document.querySelector( 261 '.cna-podcast-player--dock-wrapper, ' + 262 '.podcast-player, ' + 263 '.media-player, ' + 264 '.player, ' + 265 '#player, ' + 266 '.audio-player, ' + 267 '.player-container' 268 ); 269 } 270 271 const playerElement = findPlayerElement(); 272 if (!playerElement) { 273 setTimeout(initPlayerStateTracking, 1000); 274 return; 275 } 276 277 const observer = new MutationObserver(mutations => { 278 mutations.forEach(mutation => { 279 if (mutation.type === 'attributes' && 280 (mutation.attributeName === 'class' || mutation.attributeName === 'style')) { 281 282 const isHidden = playerElement.classList.contains('hidden') || 283 playerElement.classList.contains('closed') || 284 window.getComputedStyle(playerElement).display === 'none' || 285 window.getComputedStyle(playerElement).visibility === 'hidden'; 286 287 playerState.isPlayerOpen = !isHidden; 288
289 if (isHidden) { 290 playerState.currentMediaId = null; 291 } 292 } 293 }); 294 }); 295 296 observer.observe(playerElement, { 297 attributes: true, 298 attributeFilter: ['class', 'style'] 299 }); 300 301 document.addEventListener('click', function (event) { 302 const closeButton = event.target.closest( 303 '.player-close, ' + 304 '.close-player, ' + 305 '.player__close, ' + 306 '.player-controls__close, ' + 307 '[data-close-player], ' + 308 '.close-icon, ' + 309 '.player .close, ' + 310 'button.close, ' + 311 '.player__control--close' 312 ); 313 314 if (closeButton) { 315 playerState.isPlayerOpen = false; 316 playerState.currentMediaId = null; 317 } 318 }, true); 319 320 document.addEventListener('keydown', function (event) { 321 if (event.key === 'Escape' && playerState.isPlayerOpen) { 322 setTimeout(() => { 323 const playerElement = findPlayerElement(); 324 const isVisible = playerElement && 325 !playerElement.classList.contains('hidden') && 326 !playerElement.classList.contains('closed') && 327 window.getComputedStyle(playerElement).display !== 'none' && 328 window.getComputedStyle(playerElement).visibility !== 'hidden'; 329 330 if (!isVisible) { 331 playerState.isPlayerOpen = false; 332 playerState.currentMediaId = null; 333 } 334 }, 100); 335 } 336 }); 337 } 338 339 function attachRadioListener(button) { 340 const self = this; 341 if (!button._hasMxRadioListener) { 342 button.addEventListener('click', function (event) { 343 handleRadioStart(event); 344 }); 345 button._hasMxRadioListener = true; 346 } 347 } 348 349 function initRadioTracking() { 350 const self = this; 351 352 // 1. First try direct DOM query 353 const listenButton = document.getElementById('listen-now--btn'); 354 if (listenButton) { 355 attachRadioListener(listenButton); 356 return; 357 } 358 359 // 2. Set up MutationObserver to detect when button is added to DOM 360 const observer = new MutationObserver(function (mutations) { 361 for (const mutation of mutations) { 362 if (mutation.type === 'childList') { 363 const listenButton = document.getElementById('listen-now--btn'); 364 if (listenButton) { 365 attachRadioListener(listenButton); 366 observer.disconnect(); // Stop observing once found 367 return; 368 } 369 } 370 } 371 }); 372 373 // Start observing the entire body for changes 374 observer.observe(document.body, { childList: true, subtree: true }); 375 376 // 3. Also set up click delegation as a fallback 377 document.body.addEventListener('click', function (event) { 378 // Use event delegation for any click on listen-now--btn 379 if (event.target.id === 'listen-now--btn' || 380 event.target.closest('#listen-now--btn')) { 381 handleRadioStart(event); 382 } 383 384 // Also handle the sticky audio button 385 if (event.target.id === 'sticky-audio-btn' || 386 event.target.closest('#sticky-audio-btn')) { 387 handleRadioStart(event); 388 } 389 }, false); 390 } 391 392 function initVideoTracking() { 393 // Select all Brightcove video elements 394 const players = document.querySelectorAll('.video-lightbox .video-js'); 395 396 players.forEach(function (el) { 397 // Initialize the player 398 const player = videojs(el.id); 399 400 let playEventHandled = false; 401 402 // Wait for the player to be ready 403 player.ready(function () { 404 405 this.on('play', function () { 406 if (!playEventHandled) { 407 handleVideoEvent(el, 'start'); 408 playEventHandled = true; 409 } 410 }); 411 this.on('ended', function () { 412 handleVideoEvent(el, 'complete'); 413 playEventHandled = false;
414 }); 415 // Get the lightbox element 416 const lightbox = el.closest('.video-lightbox'); 417 if (lightbox) { 418 // Create a MutationObserver to watch for class changes 419 const observer = new MutationObserver(function (mutations) { 420 mutations.forEach(function (mutation) { 421 if (mutation.attributeName === 'class') { 422 const hasActiveClass = lightbox.classList.contains('active'); 423 424 if (!hasActiveClass) { 425 // Lightbox closed: Reset flag 426 playEventHandled = false; 427 } 428 } 429 }); 430 }); 431 432 observer.observe(lightbox, { 433 attributes: true, 434 attributeFilter: ['class'] 435 }); 436 } 437 }); 438 }); 439 } 440 441 function trackMenuClicks() { 442 document.addEventListener('click', function (event) { 443 const linkElement = event.target.closest('a'); 444 if (!linkElement) return; 445 446 let menuType = null; 447 let menuSection = null; 448 449 if (linkElement.closest('.cnar-primary-menu-wrapper')) { 450 menuType = 'Top menu'; 451 menuSection = linkElement.textContent.trim(); 452 } 453 else if (linkElement.closest('.cnar-secondary-menu-wrapper')) { 454 menuType = 'All sections'; 455 menuSection = linkElement.textContent.trim(); 456 } 457 else if (linkElement.closest('.cnar-secondary-hamburger-wrapper')) { 458 menuType = 'Mobile ham menu'; 459 menuSection = linkElement.textContent.trim(); 460 } 461 else if (linkElement.closest('.hamberger.cnar-site-navigation-menu')) { 462 menuType = 'Quick links Web'; 463 menuSection = linkElement.textContent.trim(); 464 } 465 else { 466 return; 467 } 468 469 const pageUrl = window.location.href; 470 471 trackEvent('content_click', { 472 menu_type: menuType, 473 menu_section: menuSection, 474 page_url: pageUrl 475 }); 476 }); 477 } 478 479 function initTracking() { 480 initPlayerStateTracking(); 481 initRadioTracking(); 482 trackMenuClicks(); 483 484 document.addEventListener('click', function (event) { 485 const target = event.target; 486 487 const playControl = target.closest('.media-length, .play-podcast, .list-object__datetime-duration'); 488 if (playControl || 489 (target.tagName === 'svg' && target.closest('.media-length, .play-podcast, .list-object__datetime-duration')) || 490 (target.tagName === 'use' && target.closest('.media-length, .play-podcast, .list-object__datetime-duration'))) { 491 handlePodcastStart(event); 492 return; 493 } 494 495 const thumbnail = target.closest('.card-object__figure--video a, .list-object__heading-link, .h6__link'); 496 if (thumbnail || 497 (target.tagName === 'img' && target.closest('.card-object__figure--video a'))) { 498 handleThumbnailClick(event); 499 return; 500 } 501 }, true); 502 } 503 504 // Initialize tracking 505 initTracking(); 506 initVideoTracking(); 507 } 508 }; 509})(Drupal);
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.