inteobs
Version:
A ponyfill based on the w3c/IntersectionObserver polyfill
416 lines (341 loc) • 12 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var hasSupport = typeof window !== 'undefined' && 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype;
exports.default = hasSupport ? window.IntersectionObserver : IntersectionObserver;
var registry = [];
function IntersectionObserverEntry(entry) {
this.time = entry.time;
this.target = entry.target;
this.rootBounds = entry.rootBounds;
this.boundingClientRect = entry.boundingClientRect;
this.intersectionRect = entry.intersectionRect || getEmptyRect();
this.isIntersecting = !!entry.intersectionRect;
var targetRect = this.boundingClientRect;
var targetArea = targetRect.width * targetRect.height;
var intersectionRect = this.intersectionRect;
var intersectionArea = intersectionRect.width * intersectionRect.height;
if (targetArea) {
this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
} else {
this.intersectionRatio = this.isIntersecting ? 1 : 0;
}
}
function IntersectionObserver(callback, opt_options) {
var options = opt_options || {};
if (typeof callback != 'function') {
throw new Error('callback must be a function');
}
if (options.root && options.root.nodeType != 1) {
throw new Error('root must be an Element');
}
this._checkForIntersections = throttle(this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
this._callback = callback;
this._observationTargets = [];
this._queuedEntries = [];
this._rootMarginValues = this._parseRootMargin(options.rootMargin);
this.thresholds = this._initThresholds(options.threshold);
this.root = options.root || null;
this.rootMargin = this._rootMarginValues.map(function (margin) {
return margin.value + margin.unit;
}).join(' ');
}
IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
IntersectionObserver.prototype.POLL_INTERVAL = null;
IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true;
IntersectionObserver.prototype.observe = function (target) {
var isTargetAlreadyObserved = this._observationTargets.some(function (item) {
return item.element == target;
});
if (isTargetAlreadyObserved) {
return;
}
if (!(target && target.nodeType == 1)) {
throw new Error('target must be an Element');
}
this._registerInstance();
this._observationTargets.push({ element: target, entry: null });
this._monitorIntersections();
this._checkForIntersections();
};
IntersectionObserver.prototype.unobserve = function (target) {
this._observationTargets = this._observationTargets.filter(function (item) {
return item.element != target;
});
if (!this._observationTargets.length) {
this._unmonitorIntersections();
this._unregisterInstance();
}
};
IntersectionObserver.prototype.disconnect = function () {
this._observationTargets = [];
this._unmonitorIntersections();
this._unregisterInstance();
};
IntersectionObserver.prototype.takeRecords = function () {
var records = this._queuedEntries.slice();
this._queuedEntries = [];
return records;
};
IntersectionObserver.prototype._initThresholds = function (opt_threshold) {
var threshold = opt_threshold || [0];
if (!Array.isArray(threshold)) threshold = [threshold];
return threshold.sort().filter(function (t, i, a) {
if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
throw new Error('threshold must be a number between 0 and 1 inclusively');
}
return t !== a[i - 1];
});
};
IntersectionObserver.prototype._parseRootMargin = function (opt_rootMargin) {
var marginString = opt_rootMargin || '0px';
var margins = marginString.split(/\s+/).map(function (margin) {
var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
if (!parts) {
throw new Error('rootMargin must be specified in pixels or percent');
}
return { value: parseFloat(parts[1]), unit: parts[2] };
});
margins[1] = margins[1] || margins[0];
margins[2] = margins[2] || margins[0];
margins[3] = margins[3] || margins[1];
return margins;
};
IntersectionObserver.prototype._monitorIntersections = function () {
if (!this._monitoringIntersections) {
this._monitoringIntersections = true;
if (this.POLL_INTERVAL) {
this._monitoringInterval = setInterval(this._checkForIntersections, this.POLL_INTERVAL);
} else {
addEvent(window, 'resize', this._checkForIntersections, true);
addEvent(document, 'scroll', this._checkForIntersections, true);
if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
this._domObserver = new MutationObserver(this._checkForIntersections);
this._domObserver.observe(document, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
}
}
}
};
IntersectionObserver.prototype._unmonitorIntersections = function () {
if (this._monitoringIntersections) {
this._monitoringIntersections = false;
clearInterval(this._monitoringInterval);
this._monitoringInterval = null;
removeEvent(window, 'resize', this._checkForIntersections, true);
removeEvent(document, 'scroll', this._checkForIntersections, true);
if (this._domObserver) {
this._domObserver.disconnect();
this._domObserver = null;
}
}
};
IntersectionObserver.prototype._checkForIntersections = function () {
var rootIsInDom = this._rootIsInDom();
var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
this._observationTargets.forEach(function (item) {
var target = item.element;
var targetRect = getBoundingClientRect(target);
var rootContainsTarget = this._rootContainsTarget(target);
var oldEntry = item.entry;
var intersectionRect = rootIsInDom && rootContainsTarget && this._computeTargetAndRootIntersection(target, rootRect);
var newEntry = item.entry = new IntersectionObserverEntry({
time: now(),
target: target,
boundingClientRect: targetRect,
rootBounds: rootRect,
intersectionRect: intersectionRect
});
if (!oldEntry) {
this._queuedEntries.push(newEntry);
} else if (rootIsInDom && rootContainsTarget) {
if (this._hasCrossedThreshold(oldEntry, newEntry)) {
this._queuedEntries.push(newEntry);
}
} else {
if (oldEntry && oldEntry.isIntersecting) {
this._queuedEntries.push(newEntry);
}
}
}, this);
if (this._queuedEntries.length) {
this._callback(this.takeRecords(), this);
}
};
IntersectionObserver.prototype._computeTargetAndRootIntersection = function (target, rootRect) {
if (window.getComputedStyle(target).display == 'none') return;
var targetRect = getBoundingClientRect(target);
var intersectionRect = targetRect;
var parent = getParentNode(target);
var atRoot = false;
while (!atRoot) {
var parentRect = null;
var parentComputedStyle = parent.nodeType == 1 ? window.getComputedStyle(parent) : {};
if (parentComputedStyle.display == 'none') return;
if (parent == this.root || parent == document) {
atRoot = true;
parentRect = rootRect;
} else {
if (parent != document.body && parent != document.documentElement && parentComputedStyle.overflow != 'visible') {
parentRect = getBoundingClientRect(parent);
}
}
if (parentRect) {
intersectionRect = computeRectIntersection(parentRect, intersectionRect);
if (!intersectionRect) break;
}
parent = getParentNode(parent);
}
return intersectionRect;
};
IntersectionObserver.prototype._getRootRect = function () {
var rootRect;
if (this.root) {
rootRect = getBoundingClientRect(this.root);
} else {
var html = document.documentElement;
var body = document.body;
rootRect = {
top: 0,
left: 0,
right: html.clientWidth || body.clientWidth,
width: html.clientWidth || body.clientWidth,
bottom: html.clientHeight || body.clientHeight,
height: html.clientHeight || body.clientHeight
};
}
return this._expandRectByRootMargin(rootRect);
};
IntersectionObserver.prototype._expandRectByRootMargin = function (rect) {
var margins = this._rootMarginValues.map(function (margin, i) {
return margin.unit == 'px' ? margin.value : margin.value * (i % 2 ? rect.width : rect.height) / 100;
});
var newRect = {
top: rect.top - margins[0],
right: rect.right + margins[1],
bottom: rect.bottom + margins[2],
left: rect.left - margins[3]
};
newRect.width = newRect.right - newRect.left;
newRect.height = newRect.bottom - newRect.top;
return newRect;
};
IntersectionObserver.prototype._hasCrossedThreshold = function (oldEntry, newEntry) {
var oldRatio = oldEntry && oldEntry.isIntersecting ? oldEntry.intersectionRatio || 0 : -1;
var newRatio = newEntry.isIntersecting ? newEntry.intersectionRatio || 0 : -1;
if (oldRatio === newRatio) return;
for (var i = 0; i < this.thresholds.length; i++) {
var threshold = this.thresholds[i];
if (threshold == oldRatio || threshold == newRatio || threshold < oldRatio !== threshold < newRatio) {
return true;
}
}
};
IntersectionObserver.prototype._rootIsInDom = function () {
return !this.root || containsDeep(document, this.root);
};
IntersectionObserver.prototype._rootContainsTarget = function (target) {
return containsDeep(this.root || document, target);
};
IntersectionObserver.prototype._registerInstance = function () {
if (registry.indexOf(this) < 0) {
registry.push(this);
}
};
IntersectionObserver.prototype._unregisterInstance = function () {
var index = registry.indexOf(this);
if (index != -1) registry.splice(index, 1);
};
function now() {
return window.performance && performance.now && performance.now();
}
function throttle(fn, timeout) {
var timer = null;
return function () {
if (!timer) {
timer = setTimeout(function () {
fn();
timer = null;
}, timeout);
}
};
}
function addEvent(node, event, fn, opt_useCapture) {
if (typeof node.addEventListener == 'function') {
node.addEventListener(event, fn, opt_useCapture || false);
} else if (typeof node.attachEvent == 'function') {
node.attachEvent('on' + event, fn);
}
}
function removeEvent(node, event, fn, opt_useCapture) {
if (typeof node.removeEventListener == 'function') {
node.removeEventListener(event, fn, opt_useCapture || false);
} else if (typeof node.detatchEvent == 'function') {
node.detatchEvent('on' + event, fn);
}
}
function computeRectIntersection(rect1, rect2) {
var top = Math.max(rect1.top, rect2.top);
var bottom = Math.min(rect1.bottom, rect2.bottom);
var left = Math.max(rect1.left, rect2.left);
var right = Math.min(rect1.right, rect2.right);
var width = right - left;
var height = bottom - top;
return width >= 0 && height >= 0 && {
top: top,
bottom: bottom,
left: left,
right: right,
width: width,
height: height
};
}
function getBoundingClientRect(el) {
var rect;
try {
rect = el.getBoundingClientRect();
} catch (err) {}
if (!rect) return getEmptyRect();
if (!(rect.width && rect.height)) {
rect = {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.right - rect.left,
height: rect.bottom - rect.top
};
}
return rect;
}
function getEmptyRect() {
return {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 0,
height: 0
};
}
function containsDeep(parent, child) {
var node = child;
while (node) {
if (node == parent) return true;
node = getParentNode(node);
}
return false;
}
function getParentNode(node) {
var parent = node.parentNode;
if (parent && parent.nodeType == 11 && parent.host) {
return parent.host;
}
return parent;
}
module.exports = exports.default;