snowplow/snowplow-javascript-tracker

View on GitHub
plugins/browser-plugin-media-tracking/src/findElem.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { LOG } from '@snowplow/tracker-core';
import { ElementConfig, StringConfig } from './config';
import { SEARCH_ERROR } from './constants';
import { SearchResult } from './types';
import { isHtmlAudioElement, isHtmlMediaElement, isHtmlVideoElement } from './helperFunctions';

/**
 * Waits for an HTML media element and invokes a callback with the element once found.
 *
 * - First, it tries to find the media element by id
 *   - If found and valid (`isHtmlMediaElement()`), the callback is called with the updated config.
 *   - If not found, a warning is logged and a `MutationObserver` is set up.
 *
 * - The `MutationObserver` watches for new elements added to the DOM. When the target media element is detected,
 *   the callback is invoked, and the observer disconnects.
 *
 * Useful for cases where the media element might not be in the DOM immediately.
 */
export function waitForElement(config: StringConfig, callback: (element: ElementConfig) => void) {
  const { el, err } = findMediaElement(config.video);
  if (err) {
    LOG.info(`${err}. Waiting for element to be added to the DOM.`);
  }

  if (isHtmlMediaElement(el)) {
    callback({ ...config, video: el });
  } else {
    let observer = new MutationObserver((mutations) => {
      mutations.forEach((mut) => {
        if (!mut.addedNodes) {
          return;
        }
        for (let node of Object.values(mut.addedNodes)) {
          if (isHtmlMediaElement(node) && node.id === config.video) {
            callback({ ...config, video: node });
            observer.disconnect();
          }
        }
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }
}

export function findMediaElement(id: string): SearchResult {
  let el: HTMLElement | null = document.getElementById(id);

  if (!el) return { err: SEARCH_ERROR.NOT_FOUND };
  if (isHtmlAudioElement(el)) return { el };

  if (isHtmlVideoElement(el)) {
    // Plyr loads in an initial blank video with currentSrc as https://cdn.plyr.io/static/blank.mp4
    // so we need to check until currentSrc updates.
    if (el.currentSrc === 'https://cdn.plyr.io/static/blank.mp4' && el.readyState === 0) {
      return { err: SEARCH_ERROR.PLYR_CURRENTSRC };
    }
    return { el };
  }

  return findMediaElementChild(el);
}

function findMediaElementChild(el: Element): SearchResult {
  for (let tag of ['VIDEO', 'AUDIO']) {
    let descendentTags = el.getElementsByTagName(tag);
    if (descendentTags.length === 1) {
      const el = descendentTags[0];
      if (isHtmlAudioElement(el) || isHtmlVideoElement(el)) {
        return { el };
      }
    } else if (descendentTags.length === 2 && tag === 'VIDEO' && isHtmlVideoElement(descendentTags[0])) {
      // Special JWPlayer case where two video elements are used for cover-video effect.
      // In that case, we select the first video element.
      return { el: descendentTags[0] };
    } else if (descendentTags.length > 1) {
      return { err: SEARCH_ERROR.MULTIPLE_ELEMENTS };
    }
  }
  return { err: SEARCH_ERROR.NOT_FOUND };
}