resize-observer
Version:
An implementation and polyfill of the Resize Observer draft.
211 lines (180 loc) • 6.39 kB
text/typescript
import { ResizeObservation } from './ResizeObservation';
import { ResizeObserverCallback } from './ResizeObserverCallback';
import { ResizeObserverEntry } from './ResizeObserverEntry';
const resizeObservers = [] as ResizeObserver[];
class ResizeObserver {
/** @internal */
public $$callback: ResizeObserverCallback;
/** @internal */
public $$observationTargets = [] as ResizeObservation[];
/** @internal */
public $$activeTargets = [] as ResizeObservation[];
/** @internal */
public $$skippedTargets = [] as ResizeObservation[];
constructor(callback: ResizeObserverCallback) {
const message = callbackGuard(callback);
if (message) {
throw TypeError(message);
}
this.$$callback = callback;
}
public observe(target: Element) {
const message = targetGuard('observe', target);
if (message) {
throw TypeError(message);
}
const index = findTargetIndex(this.$$observationTargets, target);
if (index >= 0) {
return;
}
this.$$observationTargets.push(new ResizeObservation(target));
registerResizeObserver(this);
}
public unobserve(target: Element) {
const message = targetGuard('unobserve', target);
if (message) {
throw TypeError(message);
}
const index = findTargetIndex(this.$$observationTargets, target);
if (index < 0) {
return;
}
this.$$observationTargets.splice(index, 1);
if (this.$$observationTargets.length === 0) {
deregisterResizeObserver(this);
}
}
public disconnect() {
this.$$observationTargets = [];
this.$$activeTargets = [];
deregisterResizeObserver(this);
}
}
function registerResizeObserver(resizeObserver: ResizeObserver) {
const index = resizeObservers.indexOf(resizeObserver);
if (index < 0) {
resizeObservers.push(resizeObserver);
startLoop();
}
}
function deregisterResizeObserver(resizeObserver: ResizeObserver) {
const index = resizeObservers.indexOf(resizeObserver);
if (index >= 0) {
resizeObservers.splice(index, 1);
checkStopLoop();
}
}
function callbackGuard(callback: ResizeObserverCallback) {
if (typeof(callback) === 'undefined') {
return `Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.`;
}
if (typeof(callback) !== 'function') {
return `Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.`;
}
}
function targetGuard(functionName: string, target: Element | null | undefined) {
if (typeof(target) === 'undefined') {
return `Failed to execute '${functionName}' on 'ResizeObserver': 1 argument required, but only 0 present.`;
}
if (!(target && target.nodeType === (window as any).Node.ELEMENT_NODE)) {
return `Failed to execute '${functionName}' on 'ResizeObserver': parameter 1 is not of type 'Element'.`;
}
}
function findTargetIndex(collection: ResizeObservation[], target: Element) {
for (let index = 0; index < collection.length; index += 1) {
if (collection[index].target === target) {
return index;
}
}
return -1;
}
const gatherActiveObservationsAtDepth = (depth: number): void => {
resizeObservers.forEach((ro) => {
ro.$$activeTargets = [];
ro.$$skippedTargets = [];
ro.$$observationTargets.forEach((ot) => {
if (ot.isActive()) {
const targetDepth = calculateDepthForNode(ot.target);
if (targetDepth > depth) {
ro.$$activeTargets.push(ot);
} else {
ro.$$skippedTargets.push(ot);
}
}
});
});
};
const hasActiveObservations = (): boolean =>
resizeObservers.some((ro) => !!ro.$$activeTargets.length);
const hasSkippedObservations = (): boolean =>
resizeObservers.some((ro) => !!ro.$$skippedTargets.length);
const broadcastActiveObservations = (): number => {
let shallowestTargetDepth = Infinity;
resizeObservers.forEach((ro) => {
if (!ro.$$activeTargets.length) {
return;
}
const entries = [] as ResizeObserverEntry[];
ro.$$activeTargets.forEach((obs) => {
const entry = new ResizeObserverEntry(obs.target);
entries.push(entry);
obs.$$broadcastWidth = entry.contentRect.width;
obs.$$broadcastHeight = entry.contentRect.height;
const targetDepth = calculateDepthForNode(obs.target);
if (targetDepth < shallowestTargetDepth) {
shallowestTargetDepth = targetDepth;
}
});
ro.$$callback(entries, ro);
ro.$$activeTargets = [];
});
return shallowestTargetDepth;
};
const deliverResizeLoopErrorNotification = () => {
const errorEvent = new (window as any).ErrorEvent('ResizeLoopError', {
message: 'ResizeObserver loop completed with undelivered notifications.',
});
window.dispatchEvent(errorEvent);
};
const calculateDepthForNode = (target: Node): number => {
let depth = 0;
while (target.parentNode) {
target = target.parentNode;
depth += 1;
}
return depth;
};
const notificationIteration = () => {
let depth = 0;
gatherActiveObservationsAtDepth(depth);
while (hasActiveObservations()) {
depth = broadcastActiveObservations();
gatherActiveObservationsAtDepth(depth);
}
if (hasSkippedObservations()) {
deliverResizeLoopErrorNotification();
}
};
let animationFrameCancelToken: undefined | number;
const startLoop = () => {
if (animationFrameCancelToken) return;
runLoop();
};
const runLoop = () => {
animationFrameCancelToken = window.requestAnimationFrame(() => {
notificationIteration();
runLoop();
});
};
const checkStopLoop = () => {
if (animationFrameCancelToken && !resizeObservers.some((ro) => !!ro.$$observationTargets.length)) {
window.cancelAnimationFrame(animationFrameCancelToken);
animationFrameCancelToken = undefined;
}
};
const install = () =>
(window as any).ResizeObserver = ResizeObserver;
export {
install,
ResizeObserver,
};