UNPKG

video-ad-sdk

Version:

VAST/VPAID SDK that allows video ads to be played on top of any player

177 lines (143 loc) 4.59 kB
import debounce from 'lodash.debounce' import {IntersectionObserver} from './helpers/IntersectionObserver' type Callback = () => void const validate = (target: HTMLElement, callback: Callback): void => { if (!(target instanceof Element)) { throw new TypeError('Target is not an Element node') } if (!(callback instanceof Function)) { throw new TypeError('Callback is not a function') } } const noop = (): void => {} const intersectionHandlers = Symbol('intersectionHandlers') const observerKey = Symbol('intersectionObserver') interface ObservedHTMLElement extends HTMLElement { [intersectionHandlers]?: IntersectionObserverCallback[] [observerKey]?: IntersectionObserver } const THRESHOLDS_COUNT = 11 const onIntersection = ( target: ObservedHTMLElement, callback: IntersectionObserverCallback ): Callback => { if (!target[intersectionHandlers]) { target[intersectionHandlers] = [] const execHandlers = ( entries: IntersectionObserverEntry[], observer: IntersectionObserver ): void => { target[intersectionHandlers]?.forEach((handler) => handler(entries, observer) ) } const options = { root: null, rootMargin: '0px', threshold: [...new Array(THRESHOLDS_COUNT)].map( (_item, index) => index / (THRESHOLDS_COUNT - 1) ) } target[observerKey] = new IntersectionObserver(execHandlers, options) target[observerKey]?.observe(target) } target[intersectionHandlers]?.push(callback) return () => { target[intersectionHandlers] = target[intersectionHandlers]?.filter( (handler) => handler !== callback ) if (target[intersectionHandlers]?.length === 0) { target[observerKey]?.disconnect() delete target[intersectionHandlers] delete target[observerKey] } } } let visibilityHandlers: Callback[] = [] const onVisibilityChange = ( _target: HTMLElement, callback: Callback ): Callback => { const execHandlers = (): void => { if (visibilityHandlers) { visibilityHandlers.forEach((handler) => handler()) } } visibilityHandlers.push(callback) if (visibilityHandlers.length === 1) { document.addEventListener('visibilitychange', execHandlers) } return () => { visibilityHandlers = visibilityHandlers.filter( (handler) => handler !== callback ) if (visibilityHandlers.length === 0) { document.removeEventListener('visibilitychange', execHandlers) } } } let lastIntersectionEntries: IntersectionObserverEntry[] = [] /** * onElementVisibilityChange callback called whenever the target element change the visibility. * * @ignore * @param isVisible true if the target element is visible and false otherwise. */ type VisibilityCallback = (isVisible?: boolean) => void interface VisibilityObserverOptions { /** * Sets a debounce threshold for the callback. Defaults to 100 milliseconds. */ threshold?: number /** * Offset fraction. Percentage of the element that needs to be hidden to be considered not visible. * Defaults to 0.4 */ viewabilityOffset?: number } /** * Helper function to know if the visibility of an element has changed. * * @ignore * * @param target The element that we want to observe. * @param callback The callback that handles the visibility change. * @param options Options Map. * * @returns Unsubscribe function. */ export const onElementVisibilityChange = ( target: HTMLElement, callback: VisibilityCallback, {threshold = 100, viewabilityOffset = 0.4}: VisibilityObserverOptions = {} ): Callback => { validate(target, callback) if (!IntersectionObserver) { // NOTE: visibility is not determined callback(undefined) return noop } let lastIsInViewport = false const checkVisibility = (entries: IntersectionObserverEntry[]): void => { entries.forEach((entry) => { if (entry.target === target) { const isInViewport = !document.hidden && entry.intersectionRatio > viewabilityOffset if (isInViewport !== lastIsInViewport) { lastIsInViewport = isInViewport callback(isInViewport) } } }) lastIntersectionEntries = entries } const visibilityHandler = debounce(checkVisibility, threshold) const stopObservingIntersection = onIntersection(target, visibilityHandler) const stopListeningToVisibilityChange = onVisibilityChange(target, (): void => visibilityHandler(lastIntersectionEntries) ) return () => { stopObservingIntersection() stopListeningToVisibilityChange() } }