PageSourceSearch

https://www.channelnewsasia.com/modules/custom/mc_cnar_dynamic_listing/js/cnar_media_adobe_analytics.js?tley1p

js channelnewsasia.com collected 2026-09-24 08:25:59 UTC 13,196 bytes, 377 lines download raw bytes

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.