Skip to content

Instantly share code, notes, and snippets.

@amn
Created December 10, 2018 20:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amn/9de839183d137efaa4e7d1103f24c042 to your computer and use it in GitHub Desktop.
Save amn/9de839183d137efaa4e7d1103f24c042 to your computer and use it in GitHub Desktop.
An implementation of IntersectionObserver class, after http://w3c.github.io/IntersectionObserver, with some deviations
/** An implementation of the IntersectionObserver class, as specified by http://w3c.github.io/IntersectionObserver. There are some open issues with the later specification, but an attempt is made here to follow it to the best of ability, unless it is found too difficult to interpret -- the motivation is, after all, to have a working implementation, and as certain elements of specification weren't clarifying in themselves, these parts of implementation may turn out to be lacking or even incorrect.
This was initially written to plug in in user agents that do not provide their own implementation, like Safari on iOS 9.3.5. Hence, the used syntax flavour is such as to work in said user agent. */
IntersectionObserver = (function() {
/** Find intersection product of two rectangles `a` and `b`, itself a rectangle. Essentially, this function is the logical AND operation for rectangles. A rectangle with zero width and/or height is returned if `a` and `b` are adjacent but do not overlap in that axis. No rectangle is returned if rectangles do not overlap and aren't adjacent in any axis. */
function intersection_product(a, b) {
var max_left = Math.max(a.x, b.x), min_right = Math.min(a.right, b.right);
if(min_right >= max_left) {
var max_top = Math.max(a.y, b.y), min_bottom = Math.min(a.bottom, b.bottom);
if(min_bottom >= max_top) return new DOMRect(max_left, max_top, min_right - max_left, min_bottom - max_top);
}
}
/** Attempt to find an element that is a "containing block" to the specified `element`, faithfully to https://www.w3.org/TR/css-position-3/#def-cb. Is a liberal interpretation of the specification, however, because determining the "containing block" is hard or even impossible with current Web and language APIs alone, as they do not collectively allow, in a *forward compatible* way at least, to determine whether an element is an "inline-level" or "block-level" element. */
function containing_block_element(element) {
if(element == element.ownerDocument.documentElement) return;
switch(getComputedStyle(element).position) {
case "relative":
case "static":
case "sticky":
for(var ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
if(getComputedStyle(ancestor).display == "inline") continue;
return ancestor;
}
break;
case "absolute":
for(var ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
if(getComputedStyle(ancestor).position == "static") continue;
return ancestor;
}
break;
}
return element.ownerDocument.documentElement;
}
/** Compute the intersection product of a `target` element with a `root` element. Per https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo. This implementation is currently insufficient and works only in some cases. */
function compute_intersection(target, root, rc) {
if(!rc) rc = target.getBoundingClientRect();
for(var container = containing_block_element(target); container != root; container = containing_block_element(container)) {
rc = intersection_product(rc, container.getBoundingClientRect());
if(!rc) break;
}
return rc;
}
/** Notify all observers with an intersection root in document specified with `document`, by draining each observer's entry queue. Per https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo. */
function notify_observers(document) {
var document_slots = slots.get(document);
document_slots.task_queued_flag = false;
for(var observer of document_slots.observers) {
var observer_slots = slots.get(observer);
if(observer_slots.queued.length == 0) continue;
var queue = Array.from(observer_slots.queued);
observer_slots.queued.length = 0;
try {
observer_slots.callback(queue, observer);
} catch(error) {
if(dispatchEvent(new ErrorEvent("error", error))) console.error(error);
}
}
}
/** Return the so called observer root bounds rectangle. */
function root_isect_rect(root) {
return root ? root.getBoundingClientRect() : new DOMRect(0, 0, innerWidth, innerHeight);
}
/** Queue an observer entry with specified parameters. Per https://w3c.github.io/IntersectionObserver/#queue-intersection-observer-entry-algo. */
function queue_entry(observer, document, time, rootBounds, boundingClientRect, intersectionRect, isIntersecting, target, intersectionRatio) {
slots.get(observer).queued.push(new IntersectionObserverEntry({ time, rootBounds, boundingClientRect, intersectionRect, isIntersecting, target, intersectionRatio }));
queue_task(document);
}
/** Queue a task where callbacks for all observers that have their intersection roots in `document`, are called. Per https://w3c.github.io/IntersectionObserver/#queue-intersection-observer-task. */
function queue_task(document) {
var document_slots = slots.get(document);
if(document_slots.task_queued_flag) return;
document_slots.task_queued_flag = true;
setTimeout(notify_observers, 0, document);
}
/** Execute the "update intersection observations" procedure, as per https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo. */
function update(document, time) {
for(var observer of slots.get(document).observers) {
var rootBounds = root_isect_rect(observer.root);
for(var target of slots.get(observer).targets)
if(!update_target(target, document, time, observer, rootBounds))
continue;
}
}
/** A reusable part of the above procedure, pertaining to a single observation target. */
function update_target(target, document, time, observer, rootBounds) {
/*if(observer.root) { /// This is commented out because the specification hasn't been completely understood (yet).
if(target.ownerDocument != observer.root.ownerDocument || !(function isect_root_contains_target_block() {
for(var element = containing_block_element(target); element; element = containing_block_element(element)) {
if(element == observer.root) return true;
}
})()) return;
}*/
var boundingClientRect = target.getBoundingClientRect(), targetArea = boundingClientRect.width * boundingClientRect.height;
var intersectionRect = compute_intersection(target, observer.root, boundingClientRect), intersectionArea = intersectionRect ? (intersectionRect.width * intersectionRect.height) : 0;
var isIntersecting = (intersectionRect != null);
var intersectionRatio = targetArea ? (intersectionArea / targetArea) : (isIntersecting ? 1 : 0);
var thresholdIndex = (intersectionRatio < observer.thresholds[observer.thresholds.length - 1]) ? observer.thresholds.findIndex(function(value) { return value > intersectionRatio; }) : observer.thresholds.length;
var registration = slots.get(target).registrations.find(function(registration) { return registration.observer == observer; });
if(thresholdIndex != registration.previousThresholdIndex || isIntersecting != registration.previousIsIntersecting) queue_entry(observer, document, time, rootBounds, boundingClientRect, intersectionRect || emptyRect, isIntersecting, target, intersectionRatio);
registration.previousThresholdIndex = thresholdIndex;
registration.previousIsIntersecting = isIntersecting;
return true;
}
/** The `slots` weak-key map is a device that allows associating any object with a set of properties, without polluting the object and exposing the properties to client code. In other words, such properties are inherently private and invisible outside the scope of declaration of `slots`. */
var slots = new WeakMap(), emptyRect = new DOMRect();
return class {
constructor(callback, options) {
slots.set(this, { callback, queued: [], targets: [] });
var document_slots = slots.get(document);
if(!document_slots) slots.set(document, document_slots = { observers: [] });
document_slots.observers.push(this);
this.root = options.root;
var thresholds = options.threshold;
if(thresholds.some(function(value) { return value < 0.0 || value > 1.0; })) throw new RangeError("Some threshold values are out of permitted range");
thresholds.sort();
if(thresholds.length == 0) thresholds.push(0);
this.thresholds = thresholds;
}
observe(target) {
var observer_slots = slots.get(this);
if(observer_slots.targets.includes(target)) return;
var registration = { observer: this, previousThresholdIndex: -1, previousIsInterseting: false };
var target_slots = slots.get(target);
if(!target_slots) slots.set(target, target_slots = { registrations: [], scroll_handlers: new Map() });
target_slots.registrations.push(registration);
observer_slots.targets.push(target);
function document(root) { return root ? root.ownerDocument : top.document; }
update(document(this.root), performance.now());
(function(observer) {
function on_scroll_event(ev) {
update_target(target, document(observer.root), performance.now(), observer, root_isect_rect(observer.root));
}
for(var container = containing_block_element(target); container; container = containing_block_element(container)) {
container.addEventListener("scroll", on_scroll_event);
}
})(this);
}
};
})();
IntersectionObserverEntry = class {
constructor(init) {
/// A strange quirk of IntersectionObserver spec. is that it is left unclear how `intersectionRatio` property works -- can it be provided in the constructor with the `init` object, is it a getter, or can it be implemented as either?
for(var name of [ "time", "rootBounds", "boundingClientRect", "intersectionRect", "isIntersecting", "intersectionRatio", "target" ])
this[name] = init[name];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment