1/** 2 * @file 3 * CNAR Media Adobe Analytics Tracking behavior. 4 * Handles tracking of audio and video content consumption with Adobe Analytics. 5 */ 6 7(function ($, Drupal, drupalSettings, cookies, once) { 8 'use strict'; 9 10 function getCookie(name) { 11 return cookies.get(name) || ''; 12 } 13 14 /** 15 * CNAR Media Adobe Analytics behavior. 16 * Tracks various media interactions and sends data to Adobe Analytics. 17 */ 18 Drupal.behaviors.cnarMediaAdobeAnalytics = { 19 _state: { 20 currentMediaId: null, 21 isMediaPlaying: false, 22 playerOpen: false, 23 }, 24 25 attach: function (context, settings) { 26 if (context !== document) { 27 return; 28 } 29 30 // Only run once 31 if ($(once('mediaAnalytics', 'html', context)).length) { 32 this.initPodcastTracking(context); 33 this.initRadioTracking(context); 34 this.initChapterTracking(context); 35 this.initPlayerStateTracking(context); 36 this.initVideoTracking(context); 37 } 38 }, 39 40 initPlayerStateTracking: function (context) { 41 const playerContainer = document.querySelector('.cna-podcast-player--dock-wrapper'); 42 if (!playerContainer) return; 43 44 const self = this; 45 const observer = new MutationObserver(mutations => { 46 mutations.forEach(mutation => { 47 if (mutation.attributeName === 'class') { 48 const isHidden = playerContainer.classList.contains('hidden'); 49 self._state.playerOpen = !isHidden; 50 if (isHidden) { 51 self._state.currentMediaId = null; 52 self._state.isMediaPlaying = false; 53 } 54 } 55 }); 56 }); 57 58 observer.observe(playerContainer, { attributes: true }); 59 }, 60 61 getUserDetails: function () { 62 const loginstatus = window.ssoMeConnect && typeof window.ssoMeConnect.isAuthenticate === 'function' ? window.ssoMeConnect.isAuthenticate() : false; 63 const ssoid = getCookie("ssoid") || 'NA'; 64 const loginsource = getCookie("sso_source") === "mediacorpidp" ? "mediacorp" : getCookie("sso_source") || 'NA'; 65 const usertype = loginstatus ? "Free" : "NA"; 66 const auth0id = loginstatus ? (getCookie("sso_user_id") || 'NA') : 'NA'; 67 68 return { loginstatus, ssoid, loginsource, usertype, auth0id }; 69 }, 70 71 findRailName: function (element) { 72 if (!element) return ''; 73 74 const parent = element.closest('.block-inline-blockdynamic-listing, .block--type-dynamic-listing'); 75 if (!parent) return ''; 76 77 const firstH2 = parent.querySelector('h2'); 78 if (firstH2) { 79 const link = firstH2.querySelector('a'); 80 return link ? link.textContent.trim() : firstH2.textContent.trim(); 81 } 82 83 const headingClasses = ['h2--block-heading', 'heading', 'h2']; 84 85 for (const className of headingClasses) { 86 const headingEl = parent.querySelector('.' + className); 87 if (headingEl) { 88 const link = headingEl.querySelector('a'); 89 return link ? link.textContent.trim() : headingEl.textContent.trim(); 90 } 91 } 92 93 return ''; 94 }, 95 96 trackMediaEvent: function (mediaCard, playerType = "PC") { 97 if (!mediaCard) return; 98 99 let mediaId = ''; 100 let contentId = 'NA'; 101 if (mediaCard.hasAttribute('data-podcast-id')) { 102 mediaId = mediaCard.getAttribute('data-podcast-id'); 103 } 104 if (mediaCard.hasAttribute('data-content-id')) { 105 contentId = mediaCard.getAttribute('data-content-id'); 106 } 107 108 if (!mediaId || (this._state.currentMediaId === mediaId && this._state.playerOpen)) return; 109 110 this._state.currentMediaId = mediaId; 111 112 const railName = this.findRailName(mediaCard); 113 const userDetails = this.getUserDetails(); 114 const player = document.querySelector('.cna-podcast-player--dock-wrapper:not(.hidden) audio.cna-audio-player-mediaelement'); 115 116 if (window._adobeUtility && typeof window._adobeUtility.adobeAnalyticsTrack === 'function') { 117 window._adobeUtility.adobeAnalyticsTrack('omniload', { 118 'content_id': contentId, 119 'id': mediaId, 120 'loginsource': userDetails.loginsource, 121 'loginstatus': userDetails.loginstatus, 122 'obj': player || {}, 123 'path': window.location.pathname, 124 'player': playerType, 125 'ssoid': userDetails.ssoid, 126 'type': "Audio", 127 'usertype': userDetails.usertype,
128 'railname': railName || "", 129 'auth0id': userDetails.auth0id 130 }); 131 } 132 }, 133 134 initPodcastTracking: function (context) { 135 const self = this; 136 const podcastSelectors = '.media-length--play-podcast, .list-object__datetime-duration .play-podcast, .list-object__datetime-duration'; 137 138 function handlePodcastClick(event) { 139 const target = event.currentTarget || event.target; 140 if (!target) return; 141 142 const podcastCard = target.closest('[data-podcast-id]') || target.closest('[data-program-id]'); 143 if (!podcastCard) return; 144 145 setTimeout(() => self.trackMediaEvent(podcastCard, "PC"), 100); 146 } 147 148 // Use modern once API to attach events 149 const podcastElements = context.querySelectorAll(podcastSelectors); 150 once('podcast-tracking', podcastElements).forEach(function (element) { 151 element.addEventListener('click', handlePodcastClick); 152 }); 153 154 // For dynamically added elements - use once on body for delegation 155 once('podcast-delegate', 'body', context).forEach(function (bodyElement) { 156 bodyElement.addEventListener('click', function (event) { 157 // Check if the clicked element matches our selectors 158 if (event.target.matches(podcastSelectors) || event.target.closest(podcastSelectors)) { 159 handlePodcastClick.call(event.target.closest(podcastSelectors), event); 160 } 161 }); 162 }); 163 }, 164 165 initRadioTracking: function (context) { 166 const self = this; 167 168 // 1. First try direct DOM query 169 const listenButton = document.getElementById('listen-now--btn'); 170 if (listenButton) { 171 this.attachRadioListener(listenButton); 172 return; 173 } 174 175 // 2. Set up MutationObserver to detect when button is added to DOM 176 const observer = new MutationObserver(function (mutations) { 177 for (const mutation of mutations) { 178 if (mutation.type === 'childList') { 179 const listenButton = document.getElementById('listen-now--btn'); 180 if (listenButton) { 181 self.attachRadioListener(listenButton); 182 observer.disconnect(); // Stop observing once found 183 return; 184 } 185 } 186 } 187 }); 188 189 // Start observing the entire body for changes 190 observer.observe(document.body, { childList: true, subtree: true }); 191 192 // 3. Also set up click delegation as a fallback 193 document.body.addEventListener('click', function (event) { 194 // Use event delegation for any click on listen-now--btn 195 if (event.target.id === 'listen-now--btn' || 196 event.target.closest('#listen-now--btn')) { 197 self.handleRadioClick(event); 198 } 199 200 // Also handle the sticky audio button 201 if (event.target.id === 'sticky-audio-btn' || 202 event.target.closest('#sticky-audio-btn')) { 203 self.handleRadioClick(event); 204 } 205 }, false); 206 }, 207 208 // Helper method to attach listeners consistently 209 attachRadioListener: function (button) { 210 const self = this; 211 if (!button._hasRadioListener) { 212 button.addEventListener('click', function (event) { 213 self.handleRadioClick(event); 214 }); 215 button._hasRadioListener = true; 216 } 217 }, 218 219 // Method to handle the radio click event 220 handleRadioClick: function (event) { 221 const audioPlayer = document.getElementById('sticky-audio-player'); 222 if (!audioPlayer) return; 223 224 setTimeout(() => { 225 // Update state based on actual playback status 226 this._state.isMediaPlaying = !audioPlayer.paused; 227 228 // Only track if media is playing 229 if (!this._state.isMediaPlaying) return; 230 231 const mediaId = 171601; 232 this._state.currentMediaId = mediaId; 233 234 const userDetails = this.getUserDetails(); 235 236 if (window._adobeUtility && typeof window._adobeUtility.adobeAnalyticsTrack === 'function') { 237 window._adobeUtility.adobeAnalyticsTrack('omniload', { 238 'content_id': "NA", 239 'id': mediaId, 240 'loginsource': userDetails.loginsource, 241 'loginstatus': userDetails.loginstatus, 242 'obj': audioPlayer || {}, 243 'path': '/listen', 244 'player': "RD", 245 'ssoid': userDetails.ssoid, 246 'type': "Audio", 247 'usertype': userDetails.usertype, 248 'auth0id': userDetails.auth0id 249 }); 250 } 251 }, 300); 252 }, 253
254 initChapterTracking: function (context) { 255 const chapterSelectors = '.omny-chapters .chapter, .chapter-item, [data-chapter], [data-chapter-id], .chapters-list li, .media-controls-chapter-wrapper .chapter-item'; 256 257 function handleChapterClick(event) { 258 const chapterEl = this; 259 260 const player = document.querySelector('.cna-podcast-player--dock-wrapper:not(.hidden)'); 261 if (!player) return; 262 263 const mediaId = player.getAttribute('data-podcast-id'); 264 if (!mediaId) return; 265 266 let title = ''; 267 const titleEl = chapterEl.querySelector('.chapter-name, .chapter-title, .title'); 268 title = titleEl ? titleEl.textContent.trim() : chapterEl.textContent.trim(); 269 270 let position = 0; 271 const parent = chapterEl.parentElement; 272 if (parent) { 273 const siblings = Array.from(parent.children); 274 position = siblings.indexOf(chapterEl); 275 } 276 277 if (window._adobeUtility && typeof window._adobeUtility.adobeAnalyticsTrack === 'function') { 278 window._adobeUtility.adobeAnalyticsTrack('podcast-chapterclick', { 279 'mediaid': mediaId, 280 'chapter_title': title, 281 'chapter_position': position 282 }); 283 } 284 } 285 286 // Use modern once for chapter tracking delegation 287 once('chapter-tracking', 'body', context).forEach(function (bodyElement) { 288 bodyElement.addEventListener('click', function (event) { 289 // Check if the clicked element matches our chapter selectors 290 const chapterElement = event.target.closest(chapterSelectors); 291 if (chapterElement) { 292 handleChapterClick.call(chapterElement, event); 293 } 294 }); 295 }); 296 }, 297 298 handleVideoEvent: function (el) { 299 const container = el.closest('[data-video-id]'); 300 if (!container) { 301 return; 302 } 303 304 const videoId = container.getAttribute('data-video-id'); 305 if (!videoId) { 306 return; 307 } 308 309 const videoMediaId = container.getAttribute('media-id'); 310 const player = window.videojs.getPlayer(el.id); 311 312 const userDetails = this.getUserDetails(); 313 const railName = this.findRailName(container); 314 if (window._adobeUtility && typeof window._adobeUtility.adobeAnalyticsTrack === 'function') { 315 window._adobeUtility.adobeAnalyticsTrack('omniload', { 316 'content_id': "NA", 317 'id': videoMediaId, 318 'loginsource': userDetails.loginsource, 319 'loginstatus': userDetails.loginstatus, 320 'obj': player || {}, 321 'path': window.location.pathname, 322 'player': "BC6", 323 'ssoid': userDetails.ssoid, 324 'type': "Video", 325 'usertype': userDetails.usertype, 326 'railname': railName || "", 327 'auth0id': userDetails.auth0id 328 }); 329 } 330 }, 331 332 initVideoTracking: function () { 333 // Select all Brightcove video elements 334 const self = this; 335 const players = document.querySelectorAll('.video-lightbox .video-js'); 336 337 players.forEach(function (el) { 338 // Initialize the player 339 let player = window.videojs.getPlayer(el.id); 340 let playEventHandled = false; 341 342 // Wait for the player to be ready 343 player.ready(function () { 344 this.on('play', function () { 345 if (!playEventHandled) { 346 self.handleVideoEvent(el); 347 playEventHandled = true; 348 } 349 }); 350 }); 351 352 // Get the lightbox element 353 const lightbox = el.closest('.video-lightbox'); 354 if (lightbox) { 355 // Create a MutationObserver to watch for class changes 356 const observer = new MutationObserver(function (mutations) { 357 mutations.forEach(function (mutation) { 358 if (mutation.attributeName === 'class') { 359 const hasActiveClass = lightbox.classList.contains('active'); 360 361 if (!hasActiveClass) { 362 // Lightbox closed: Reset flag 363 playEventHandled = false; 364 } 365 } 366 }); 367 }); 368 369 observer.observe(lightbox, { 370 attributes: true, 371 attributeFilter: ['class'] 372 }); 373 } 374 }); 375 } 376 }; 377})(jQuery, Drupal, drupalSettings, window.Cookies, 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.