@playkit-js/kaltura-player-js
Version:
[](https://github.com/kaltura/kaltura-player-js/actions/workflows/run_canary.yaml) [ • 6.35 kB
text/typescript
import { EventManager, Utils } from '@playkit-js/playkit-js';
import { ViewabilityConfig } from '../../types';
/**
* A service class to observe viewability of elements in the view port.
*/
class ViewabilityManager {
private readonly _observer: IntersectionObserver;
private _targetsObserved: Utils.MultiMap<HTMLElement, _TargetObserveredBinding>;
private _config: ViewabilityConfig;
private _eventManager: EventManager;
private _visibilityTabChangeEventName!: string;
private _visibilityTabHiddenAttr!: string;
/**
* Whether the player browser tab is active or not
* @type {boolean}
* @private
*/
private _isTabVisible!: boolean;
/**
* @param {number} viewabilityConfig - the configuration needed to create the manager
* @constructor
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
constructor(viewabilityConfig: ViewabilityConfig = {}) {
viewabilityConfig.observedThresholds = viewabilityConfig.observedThresholds || DEFAULT_OBSERVED_THRESHOLDS;
viewabilityConfig.playerThreshold =
typeof viewabilityConfig.playerThreshold === 'number' ? viewabilityConfig.playerThreshold : DEFAULT_PLAYER_THRESHOLD;
this._config = viewabilityConfig;
this._eventManager = new EventManager();
this._targetsObserved = new Utils.MultiMap<HTMLElement, _TargetObserveredBinding>();
const options = {
threshold: viewabilityConfig.observedThresholds.map((val: number) => {
return val / 100;
})
};
this._observer = new window.IntersectionObserver(this._intersectionChangedHandler.bind(this), options);
this._initTabVisibility();
}
private _intersectionChangedHandler(entries: Array<IntersectionObserverEntry>): void {
entries.forEach((entry: IntersectionObserverEntry) => {
const targetObserveredBindings: Array<_TargetObserveredBinding> = this._targetsObserved.get(entry.target as HTMLElement);
targetObserveredBindings.forEach((targetObservedBinding: _TargetObserveredBinding) => {
const visible = entry.intersectionRatio >= targetObservedBinding.threshold;
targetObservedBinding.lastIntersectionRatio = entry.intersectionRatio;
if (visible !== targetObservedBinding.lastVisible) {
targetObservedBinding.lastVisible = visible;
targetObservedBinding.listener(visible, ViewabilityType.VIEWPORT);
}
});
});
}
private _handleTabVisibilityChange(): void {
this._isTabVisible = !document[this._visibilityTabHiddenAttr];
this._targetsObserved.getAll().forEach((targetObservedBinding: _TargetObserveredBinding) => {
if (targetObservedBinding.lastVisible) {
targetObservedBinding.listener(this._isTabVisible, ViewabilityType.TAB);
}
});
}
private _initTabVisibility(): void {
if (typeof document.hidden !== 'undefined') {
// Opera 12.10 and Firefox 18 and later support
this._visibilityTabHiddenAttr = 'hidden';
this._visibilityTabChangeEventName = 'visibilitychange';
}
if (this._visibilityTabHiddenAttr && this._visibilityTabChangeEventName) {
this._eventManager.listen(document, this._visibilityTabChangeEventName, this._handleTabVisibilityChange.bind(this));
this._isTabVisible = !document[this._visibilityTabHiddenAttr];
}
}
/**
* @param {HTMLElement} target - the targeted element to check its visibility
* @param {Function} listener - the callback to be invoked when visibility is changed (and when starting to observe). The callback is called with a boolean param representing the visibility state
* @param {?number} optionalThreshold - a number between 0 to 100 that represents the minimum visible percentage considered as visible
* @returns {void}
*/
public observe(target: HTMLElement, listener: ListenerType, optionalThreshold?: number): void {
if (!this._observer) return;
const threshold = typeof optionalThreshold === 'number' ? optionalThreshold : this._config.playerThreshold;
const newTargetObservedBinding = new _TargetObserveredBinding(threshold / 100, listener);
if (target) {
if (!this._targetsObserved.has(target)) {
this._observer.observe(target);
} else {
const lastIntersectionRatio = this._targetsObserved.get(target)[0].lastIntersectionRatio;
// if observer has already fired the initial callback due to previous observing then we need to invoke it to the new observer manually
if (lastIntersectionRatio !== undefined) {
newTargetObservedBinding.lastIntersectionRatio = lastIntersectionRatio;
newTargetObservedBinding.listener(
this._isTabVisible && lastIntersectionRatio >= newTargetObservedBinding.threshold,
ViewabilityType.VIEWPORT
);
}
}
this._targetsObserved.push(target, newTargetObservedBinding);
}
}
/**
* Remove the listener from the target
* @param {HTMLElement} target - the targeted element to remove the listener
* @param {Function} listener - the callback function to be removed
* @returns {void}
*/
public unObserve(target: HTMLElement, listener: _TargetObserveredBinding): void {
if (!this._observer) return;
this._targetsObserved.remove(target, listener);
if (!this._targetsObserved.has(target)) {
this._observer.unobserve(target);
}
}
/**
* cleans all memory allocations.
* @override
*/
public destroy(): void {
if (!this._observer) return;
this._eventManager.destroy();
this._observer.disconnect();
this._targetsObserved.clear();
}
}
const ViewabilityType = {
VIEWPORT: 'viewport',
TAB: 'tab'
} as const;
type ListenerType = (visible: boolean, reason: string) => any;
class _TargetObserveredBinding {
public lastVisible!: boolean;
public lastIntersectionRatio!: number;
public threshold: number;
public listener: ListenerType;
constructor(threshold: number, listener: ListenerType) {
this.threshold = threshold;
this.listener = listener;
}
}
const VISIBILITY_CHANGE = 'visibilitychange';
const DEFAULT_OBSERVED_THRESHOLDS: Array<number> = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
const DEFAULT_PLAYER_THRESHOLD: number = 50;
export { ViewabilityManager, VISIBILITY_CHANGE, ViewabilityType, DEFAULT_OBSERVED_THRESHOLDS, DEFAULT_PLAYER_THRESHOLD };