PageSourceSearch

https://www.channelnewsasia.com/modules/custom/mc_cnar_dynamic_lis…/js/cnar_media_mixpanel_analytics.js?tley1p

js channelnewsasia.com collected 2026-09-24 08:26:09 UTC 17,024 bytes, 509 lines download raw bytes

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.