video-ad-sdk
Version:
VAST/VPAID SDK that allows video ads to be played on top of any player
207 lines (165 loc) • 5.19 kB
text/typescript
import debounce from 'lodash.debounce'
import {MutationObserver} from './helpers/MutationObserver'
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 sizeMutationAttributes = ['style', 'clientWidth', 'clientHeight']
const createResizeMO = (
target: HTMLElement,
callback: Callback
): MutationObserver => {
const observer = new MutationObserver((mutations) => {
for (let index = 0; index < mutations.length; index++) {
const {attributeName} = mutations[index]
if (attributeName && sizeMutationAttributes.includes(attributeName)) {
callback()
}
}
})
observer.observe(target, {
attributes: true,
characterData: false,
childList: true
})
return observer
}
const mutationHandlers = Symbol('mutationHandlers')
const observerKey = Symbol('mutationObserver')
interface ObservedHTMLElement extends HTMLElement {
[mutationHandlers]?: Callback[]
[observerKey]?: MutationObserver
}
const onMutation = (
target: ObservedHTMLElement,
callback: Callback
): Callback => {
if (!target[mutationHandlers]) {
target[mutationHandlers] = []
const execHandlers = (): void => {
target[mutationHandlers]?.forEach((handler) => handler())
}
target[observerKey] = createResizeMO(target, execHandlers)
}
target[mutationHandlers]?.push(callback)
return () => {
target[mutationHandlers] = target[mutationHandlers]?.filter(
(handler) => handler !== callback
)
if (target[mutationHandlers]?.length === 0) {
target[observerKey]?.disconnect()
delete target[mutationHandlers]
delete target[observerKey]
}
}
}
const createResizeElement = (callback: Callback): HTMLIFrameElement => {
const iframe = document.createElement('iframe')
iframe.setAttribute(
'style',
'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; border: 0; overflow: hidden; pointer-events: none; z-index: -1;'
)
iframe.setAttribute('type', 'text/html')
iframe.setAttribute('loading', 'eager')
iframe.onload = () => {
if (iframe.contentWindow) {
iframe.contentWindow.addEventListener('resize', callback)
}
}
iframe.src = 'about:blank'
return iframe
}
const resizeHandlers: unique symbol = Symbol('resizeHandlers')
const resizeElement: unique symbol = Symbol('resizeElement')
interface ResizableHTMLElement extends HTMLElement {
[resizeHandlers]?: Callback[]
[resizeElement]?: HTMLIFrameElement
}
// Original code http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/
const onResize = (
target: ResizableHTMLElement,
callback: Callback
): Callback => {
if (!target[resizeHandlers]) {
target[resizeHandlers] = []
const execHandlers = (): void => {
target[resizeHandlers]?.forEach((handler) => handler())
}
target[resizeElement] = createResizeElement(execHandlers)
if (getComputedStyle(target).position === 'static') {
target.style.position = 'relative'
}
target.appendChild(target[resizeElement] as HTMLIFrameElement)
}
target[resizeHandlers]?.push(callback)
return () => {
target[resizeHandlers] = target[resizeHandlers]?.filter(
(handler) => handler !== callback
)
if (target[resizeHandlers]?.length === 0) {
target.removeChild(target[resizeElement] as HTMLIFrameElement)
delete target[resizeHandlers]
delete target[resizeElement]
}
}
}
/**
* onElementResize callback will be called whenever the target element resized.
* Note: called with no params
*
* @ignore
*/
type ResizeCallback = () => void
interface ResizeObserverOptions {
/**
* Sets a debounce threshold for the callback. Defaults to 20 milliseconds
*/
threshold?: number
}
/**
* Helper function to know if an element has been resized.
*
* @ignore
*
* @param target The element that we want to observe.
* @param callback The callback that handles the resize.
* @param options Options Map.
*
* @returns Unsubscribe function.
*/
export const onElementResize = (
target: HTMLElement,
callback: ResizeCallback,
{threshold = 20}: ResizeObserverOptions = {}
): Callback => {
validate(target, callback)
const makeSizeId = ({
style,
clientHeight,
clientWidth
}: HTMLElement): string =>
[style.width, style.height, clientWidth, clientHeight].join('.')
let lastSize = makeSizeId(target)
const checkElementSize = (): void => {
const currentSize = makeSizeId(target)
if (currentSize !== lastSize) {
lastSize = currentSize
callback()
}
}
const checkElementHandler = debounce(checkElementSize, threshold)
const stopObservingMutations = MutationObserver
? onMutation(target, checkElementHandler)
: noop
const stopListeningToResize = onResize(target, checkElementHandler)
return () => {
stopObservingMutations()
stopListeningToResize()
}
}