Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / intersection-observer / intersection-observer.js
blob871140babdfe04912b936c89eedf282a8bbad53f
1 /**
2  * Copyright 2016 Google Inc. All Rights Reserved.
3  *
4  * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
5  *
6  *  https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
7  *
8  */
9 (function() {
10 'use strict';
12 // Exit early if we're not running in a browser.
13 if (typeof window !== 'object') {
14   return;
17 // Exit early if all IntersectionObserver and IntersectionObserverEntry
18 // features are natively supported.
19 if ('IntersectionObserver' in window &&
20     'IntersectionObserverEntry' in window &&
21     'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
23   // Minimal polyfill for Edge 15's lack of `isIntersecting`
24   // See: https://github.com/w3c/IntersectionObserver/issues/211
25   if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
26     Object.defineProperty(window.IntersectionObserverEntry.prototype,
27       'isIntersecting', {
28       get: function () {
29         return this.intersectionRatio > 0;
30       }
31     });
32   }
33   return;
36 /**
37  * Returns the embedding frame element, if any.
38  * @param {!Document} doc
39  * @return {!Element}
40  */
41 function getFrameElement(doc) {
42   try {
43     return doc.defaultView && doc.defaultView.frameElement || null;
44   } catch (e) {
45     // Ignore the error.
46     return null;
47   }
50 /**
51  * A local reference to the root document.
52  */
53 var document = (function(startDoc) {
54   var doc = startDoc;
55   var frame = getFrameElement(doc);
56   while (frame) {
57     doc = frame.ownerDocument;
58     frame = getFrameElement(doc);
59   }
60   return doc;
61 })(window.document);
63 /**
64  * An IntersectionObserver registry. This registry exists to hold a strong
65  * reference to IntersectionObserver instances currently observing a target
66  * element. Without this registry, instances without another reference may be
67  * garbage collected.
68  */
69 var registry = [];
71 /**
72  * The signal updater for cross-origin intersection. When not null, it means
73  * that the polyfill is configured to work in a cross-origin mode.
74  * @type {function(DOMRect|ClientRect, DOMRect|ClientRect)}
75  */
76 var crossOriginUpdater = null;
78 /**
79  * The current cross-origin intersection. Only used in the cross-origin mode.
80  * @type {DOMRect|ClientRect}
81  */
82 var crossOriginRect = null;
85 /**
86  * Creates the global IntersectionObserverEntry constructor.
87  * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
88  * @param {Object} entry A dictionary of instance properties.
89  * @constructor
90  */
91 function IntersectionObserverEntry(entry) {
92   this.time = entry.time;
93   this.target = entry.target;
94   this.rootBounds = ensureDOMRect(entry.rootBounds);
95   this.boundingClientRect = ensureDOMRect(entry.boundingClientRect);
96   this.intersectionRect = ensureDOMRect(entry.intersectionRect || getEmptyRect());
97   this.isIntersecting = !!entry.intersectionRect;
99   // Calculates the intersection ratio.
100   var targetRect = this.boundingClientRect;
101   var targetArea = targetRect.width * targetRect.height;
102   var intersectionRect = this.intersectionRect;
103   var intersectionArea = intersectionRect.width * intersectionRect.height;
105   // Sets intersection ratio.
106   if (targetArea) {
107     // Round the intersection ratio to avoid floating point math issues:
108     // https://github.com/w3c/IntersectionObserver/issues/324
109     this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
110   } else {
111     // If area is zero and is intersecting, sets to 1, otherwise to 0
112     this.intersectionRatio = this.isIntersecting ? 1 : 0;
113   }
118  * Creates the global IntersectionObserver constructor.
119  * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
120  * @param {Function} callback The function to be invoked after intersection
121  *     changes have queued. The function is not invoked if the queue has
122  *     been emptied by calling the `takeRecords` method.
123  * @param {Object=} opt_options Optional configuration options.
124  * @constructor
125  */
126 function IntersectionObserver(callback, opt_options) {
128   var options = opt_options || {};
130   if (typeof callback != 'function') {
131     throw new Error('callback must be a function');
132   }
134   if (
135     options.root &&
136     options.root.nodeType != 1 &&
137     options.root.nodeType != 9
138   ) {
139     throw new Error('root must be a Document or Element');
140   }
142   // Binds and throttles `this._checkForIntersections`.
143   this._checkForIntersections = throttle(
144       this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
146   // Private properties.
147   this._callback = callback;
148   this._observationTargets = [];
149   this._queuedEntries = [];
150   this._rootMarginValues = this._parseRootMargin(options.rootMargin);
152   // Public properties.
153   this.thresholds = this._initThresholds(options.threshold);
154   this.root = options.root || null;
155   this.rootMargin = this._rootMarginValues.map(function(margin) {
156     return margin.value + margin.unit;
157   }).join(' ');
159   /** @private @const {!Array<!Document>} */
160   this._monitoringDocuments = [];
161   /** @private @const {!Array<function()>} */
162   this._monitoringUnsubscribes = [];
167  * The minimum interval within which the document will be checked for
168  * intersection changes.
169  */
170 IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
174  * The frequency in which the polyfill polls for intersection changes.
175  * this can be updated on a per instance basis and must be set prior to
176  * calling `observe` on the first target.
177  */
178 IntersectionObserver.prototype.POLL_INTERVAL = null;
181  * Use a mutation observer on the root element
182  * to detect intersection changes.
183  */
184 IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true;
188  * Sets up the polyfill in the cross-origin mode. The result is the
189  * updater function that accepts two arguments: `boundingClientRect` and
190  * `intersectionRect` - just as these fields would be available to the
191  * parent via `IntersectionObserverEntry`. This function should be called
192  * each time the iframe receives intersection information from the parent
193  * window, e.g. via messaging.
194  * @return {function(DOMRect|ClientRect, DOMRect|ClientRect)}
195  */
196 IntersectionObserver._setupCrossOriginUpdater = function() {
197   if (!crossOriginUpdater) {
198     /**
199      * @param {DOMRect|ClientRect} boundingClientRect
200      * @param {DOMRect|ClientRect} intersectionRect
201      */
202     crossOriginUpdater = function(boundingClientRect, intersectionRect) {
203       if (!boundingClientRect || !intersectionRect) {
204         crossOriginRect = getEmptyRect();
205       } else {
206         crossOriginRect = convertFromParentRect(boundingClientRect, intersectionRect);
207       }
208       registry.forEach(function(observer) {
209         observer._checkForIntersections();
210       });
211     };
212   }
213   return crossOriginUpdater;
218  * Resets the cross-origin mode.
219  */
220 IntersectionObserver._resetCrossOriginUpdater = function() {
221   crossOriginUpdater = null;
222   crossOriginRect = null;
227  * Starts observing a target element for intersection changes based on
228  * the thresholds values.
229  * @param {Element} target The DOM element to observe.
230  */
231 IntersectionObserver.prototype.observe = function(target) {
232   var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
233     return item.element == target;
234   });
236   if (isTargetAlreadyObserved) {
237     return;
238   }
240   if (!(target && target.nodeType == 1)) {
241     throw new Error('target must be an Element');
242   }
244   this._registerInstance();
245   this._observationTargets.push({element: target, entry: null});
246   this._monitorIntersections(target.ownerDocument);
247   this._checkForIntersections();
252  * Stops observing a target element for intersection changes.
253  * @param {Element} target The DOM element to observe.
254  */
255 IntersectionObserver.prototype.unobserve = function(target) {
256   this._observationTargets =
257       this._observationTargets.filter(function(item) {
258         return item.element != target;
259       });
260   this._unmonitorIntersections(target.ownerDocument);
261   if (this._observationTargets.length == 0) {
262     this._unregisterInstance();
263   }
268  * Stops observing all target elements for intersection changes.
269  */
270 IntersectionObserver.prototype.disconnect = function() {
271   this._observationTargets = [];
272   this._unmonitorAllIntersections();
273   this._unregisterInstance();
278  * Returns any queue entries that have not yet been reported to the
279  * callback and clears the queue. This can be used in conjunction with the
280  * callback to obtain the absolute most up-to-date intersection information.
281  * @return {Array} The currently queued entries.
282  */
283 IntersectionObserver.prototype.takeRecords = function() {
284   var records = this._queuedEntries.slice();
285   this._queuedEntries = [];
286   return records;
291  * Accepts the threshold value from the user configuration object and
292  * returns a sorted array of unique threshold values. If a value is not
293  * between 0 and 1 and error is thrown.
294  * @private
295  * @param {Array|number=} opt_threshold An optional threshold value or
296  *     a list of threshold values, defaulting to [0].
297  * @return {Array} A sorted list of unique and valid threshold values.
298  */
299 IntersectionObserver.prototype._initThresholds = function(opt_threshold) {
300   var threshold = opt_threshold || [0];
301   if (!Array.isArray(threshold)) threshold = [threshold];
303   return threshold.sort().filter(function(t, i, a) {
304     if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
305       throw new Error('threshold must be a number between 0 and 1 inclusively');
306     }
307     return t !== a[i - 1];
308   });
313  * Accepts the rootMargin value from the user configuration object
314  * and returns an array of the four margin values as an object containing
315  * the value and unit properties. If any of the values are not properly
316  * formatted or use a unit other than px or %, and error is thrown.
317  * @private
318  * @param {string=} opt_rootMargin An optional rootMargin value,
319  *     defaulting to '0px'.
320  * @return {Array<Object>} An array of margin objects with the keys
321  *     value and unit.
322  */
323 IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) {
324   var marginString = opt_rootMargin || '0px';
325   var margins = marginString.split(/\s+/).map(function(margin) {
326     var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
327     if (!parts) {
328       throw new Error('rootMargin must be specified in pixels or percent');
329     }
330     return {value: parseFloat(parts[1]), unit: parts[2]};
331   });
333   // Handles shorthand.
334   margins[1] = margins[1] || margins[0];
335   margins[2] = margins[2] || margins[0];
336   margins[3] = margins[3] || margins[1];
338   return margins;
343  * Starts polling for intersection changes if the polling is not already
344  * happening, and if the page's visibility state is visible.
345  * @param {!Document} doc
346  * @private
347  */
348 IntersectionObserver.prototype._monitorIntersections = function(doc) {
349   var win = doc.defaultView;
350   if (!win) {
351     // Already destroyed.
352     return;
353   }
354   if (this._monitoringDocuments.indexOf(doc) != -1) {
355     // Already monitoring.
356     return;
357   }
359   // Private state for monitoring.
360   var callback = this._checkForIntersections;
361   var monitoringInterval = null;
362   var domObserver = null;
364   // If a poll interval is set, use polling instead of listening to
365   // resize and scroll events or DOM mutations.
366   if (this.POLL_INTERVAL) {
367     monitoringInterval = win.setInterval(callback, this.POLL_INTERVAL);
368   } else {
369     addEvent(win, 'resize', callback, true);
370     addEvent(doc, 'scroll', callback, true);
371     if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in win) {
372       domObserver = new win.MutationObserver(callback);
373       domObserver.observe(doc, {
374         attributes: true,
375         childList: true,
376         characterData: true,
377         subtree: true
378       });
379     }
380   }
382   this._monitoringDocuments.push(doc);
383   this._monitoringUnsubscribes.push(function() {
384     // Get the window object again. When a friendly iframe is destroyed, it
385     // will be null.
386     var win = doc.defaultView;
388     if (win) {
389       if (monitoringInterval) {
390         win.clearInterval(monitoringInterval);
391       }
392       removeEvent(win, 'resize', callback, true);
393     }
395     removeEvent(doc, 'scroll', callback, true);
396     if (domObserver) {
397       domObserver.disconnect();
398     }
399   });
401   // Also monitor the parent.
402   var rootDoc =
403     (this.root && (this.root.ownerDocument || this.root)) || document;
404   if (doc != rootDoc) {
405     var frame = getFrameElement(doc);
406     if (frame) {
407       this._monitorIntersections(frame.ownerDocument);
408     }
409   }
414  * Stops polling for intersection changes.
415  * @param {!Document} doc
416  * @private
417  */
418 IntersectionObserver.prototype._unmonitorIntersections = function(doc) {
419   var index = this._monitoringDocuments.indexOf(doc);
420   if (index == -1) {
421     return;
422   }
424   var rootDoc =
425     (this.root && (this.root.ownerDocument || this.root)) || document;
427   // Check if any dependent targets are still remaining.
428   var hasDependentTargets =
429       this._observationTargets.some(function(item) {
430         var itemDoc = item.element.ownerDocument;
431         // Target is in this context.
432         if (itemDoc == doc) {
433           return true;
434         }
435         // Target is nested in this context.
436         while (itemDoc && itemDoc != rootDoc) {
437           var frame = getFrameElement(itemDoc);
438           itemDoc = frame && frame.ownerDocument;
439           if (itemDoc == doc) {
440             return true;
441           }
442         }
443         return false;
444       });
445   if (hasDependentTargets) {
446     return;
447   }
449   // Unsubscribe.
450   var unsubscribe = this._monitoringUnsubscribes[index];
451   this._monitoringDocuments.splice(index, 1);
452   this._monitoringUnsubscribes.splice(index, 1);
453   unsubscribe();
455   // Also unmonitor the parent.
456   if (doc != rootDoc) {
457     var frame = getFrameElement(doc);
458     if (frame) {
459       this._unmonitorIntersections(frame.ownerDocument);
460     }
461   }
466  * Stops polling for intersection changes.
467  * @param {!Document} doc
468  * @private
469  */
470 IntersectionObserver.prototype._unmonitorAllIntersections = function() {
471   var unsubscribes = this._monitoringUnsubscribes.slice(0);
472   this._monitoringDocuments.length = 0;
473   this._monitoringUnsubscribes.length = 0;
474   for (var i = 0; i < unsubscribes.length; i++) {
475     unsubscribes[i]();
476   }
481  * Scans each observation target for intersection changes and adds them
482  * to the internal entries queue. If new entries are found, it
483  * schedules the callback to be invoked.
484  * @private
485  */
486 IntersectionObserver.prototype._checkForIntersections = function() {
487   if (!this.root && crossOriginUpdater && !crossOriginRect) {
488     // Cross origin monitoring, but no initial data available yet.
489     return;
490   }
492   var rootIsInDom = this._rootIsInDom();
493   var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
495   this._observationTargets.forEach(function(item) {
496     var target = item.element;
497     var targetRect = getBoundingClientRect(target);
498     var rootContainsTarget = this._rootContainsTarget(target);
499     var oldEntry = item.entry;
500     var intersectionRect = rootIsInDom && rootContainsTarget &&
501         this._computeTargetAndRootIntersection(target, targetRect, rootRect);
503     var rootBounds = null;
504     if (!this._rootContainsTarget(target)) {
505       rootBounds = getEmptyRect();
506     } else if (!crossOriginUpdater || this.root) {
507       rootBounds = rootRect;
508     }
510     var newEntry = item.entry = new IntersectionObserverEntry({
511       time: now(),
512       target: target,
513       boundingClientRect: targetRect,
514       rootBounds: rootBounds,
515       intersectionRect: intersectionRect
516     });
518     if (!oldEntry) {
519       this._queuedEntries.push(newEntry);
520     } else if (rootIsInDom && rootContainsTarget) {
521       // If the new entry intersection ratio has crossed any of the
522       // thresholds, add a new entry.
523       if (this._hasCrossedThreshold(oldEntry, newEntry)) {
524         this._queuedEntries.push(newEntry);
525       }
526     } else {
527       // If the root is not in the DOM or target is not contained within
528       // root but the previous entry for this target had an intersection,
529       // add a new record indicating removal.
530       if (oldEntry && oldEntry.isIntersecting) {
531         this._queuedEntries.push(newEntry);
532       }
533     }
534   }, this);
536   if (this._queuedEntries.length) {
537     this._callback(this.takeRecords(), this);
538   }
543  * Accepts a target and root rect computes the intersection between then
544  * following the algorithm in the spec.
545  * TODO(philipwalton): at this time clip-path is not considered.
546  * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
547  * @param {Element} target The target DOM element
548  * @param {Object} targetRect The bounding rect of the target.
549  * @param {Object} rootRect The bounding rect of the root after being
550  *     expanded by the rootMargin value.
551  * @return {?Object} The final intersection rect object or undefined if no
552  *     intersection is found.
553  * @private
554  */
555 IntersectionObserver.prototype._computeTargetAndRootIntersection =
556     function(target, targetRect, rootRect) {
557   // If the element isn't displayed, an intersection can't happen.
558   if (window.getComputedStyle(target).display == 'none') return;
560   var intersectionRect = targetRect;
561   var parent = getParentNode(target);
562   var atRoot = false;
564   while (!atRoot && parent) {
565     var parentRect = null;
566     var parentComputedStyle = parent.nodeType == 1 ?
567         window.getComputedStyle(parent) : {};
569     // If the parent isn't displayed, an intersection can't happen.
570     if (parentComputedStyle.display == 'none') return null;
572     if (parent == this.root || parent.nodeType == /* DOCUMENT */ 9) {
573       atRoot = true;
574       if (parent == this.root || parent == document) {
575         if (crossOriginUpdater && !this.root) {
576           if (!crossOriginRect ||
577               crossOriginRect.width == 0 && crossOriginRect.height == 0) {
578             // A 0-size cross-origin intersection means no-intersection.
579             parent = null;
580             parentRect = null;
581             intersectionRect = null;
582           } else {
583             parentRect = crossOriginRect;
584           }
585         } else {
586           parentRect = rootRect;
587         }
588       } else {
589         // Check if there's a frame that can be navigated to.
590         var frame = getParentNode(parent);
591         var frameRect = frame && getBoundingClientRect(frame);
592         var frameIntersect =
593             frame &&
594             this._computeTargetAndRootIntersection(frame, frameRect, rootRect);
595         if (frameRect && frameIntersect) {
596           parent = frame;
597           parentRect = convertFromParentRect(frameRect, frameIntersect);
598         } else {
599           parent = null;
600           intersectionRect = null;
601         }
602       }
603     } else {
604       // If the element has a non-visible overflow, and it's not the <body>
605       // or <html> element, update the intersection rect.
606       // Note: <body> and <html> cannot be clipped to a rect that's not also
607       // the document rect, so no need to compute a new intersection.
608       var doc = parent.ownerDocument;
609       if (parent != doc.body &&
610           parent != doc.documentElement &&
611           parentComputedStyle.overflow != 'visible') {
612         parentRect = getBoundingClientRect(parent);
613       }
614     }
616     // If either of the above conditionals set a new parentRect,
617     // calculate new intersection data.
618     if (parentRect) {
619       intersectionRect = computeRectIntersection(parentRect, intersectionRect);
620     }
621     if (!intersectionRect) break;
622     parent = parent && getParentNode(parent);
623   }
624   return intersectionRect;
629  * Returns the root rect after being expanded by the rootMargin value.
630  * @return {ClientRect} The expanded root rect.
631  * @private
632  */
633 IntersectionObserver.prototype._getRootRect = function() {
634   var rootRect;
635   if (this.root && !isDoc(this.root)) {
636     rootRect = getBoundingClientRect(this.root);
637   } else {
638     // Use <html>/<body> instead of window since scroll bars affect size.
639     var doc = isDoc(this.root) ? this.root : document;
640     var html = doc.documentElement;
641     var body = doc.body;
642     rootRect = {
643       top: 0,
644       left: 0,
645       right: html.clientWidth || body.clientWidth,
646       width: html.clientWidth || body.clientWidth,
647       bottom: html.clientHeight || body.clientHeight,
648       height: html.clientHeight || body.clientHeight
649     };
650   }
651   return this._expandRectByRootMargin(rootRect);
656  * Accepts a rect and expands it by the rootMargin value.
657  * @param {DOMRect|ClientRect} rect The rect object to expand.
658  * @return {ClientRect} The expanded rect.
659  * @private
660  */
661 IntersectionObserver.prototype._expandRectByRootMargin = function(rect) {
662   var margins = this._rootMarginValues.map(function(margin, i) {
663     return margin.unit == 'px' ? margin.value :
664         margin.value * (i % 2 ? rect.width : rect.height) / 100;
665   });
666   var newRect = {
667     top: rect.top - margins[0],
668     right: rect.right + margins[1],
669     bottom: rect.bottom + margins[2],
670     left: rect.left - margins[3]
671   };
672   newRect.width = newRect.right - newRect.left;
673   newRect.height = newRect.bottom - newRect.top;
675   return newRect;
680  * Accepts an old and new entry and returns true if at least one of the
681  * threshold values has been crossed.
682  * @param {?IntersectionObserverEntry} oldEntry The previous entry for a
683  *    particular target element or null if no previous entry exists.
684  * @param {IntersectionObserverEntry} newEntry The current entry for a
685  *    particular target element.
686  * @return {boolean} Returns true if a any threshold has been crossed.
687  * @private
688  */
689 IntersectionObserver.prototype._hasCrossedThreshold =
690     function(oldEntry, newEntry) {
692   // To make comparing easier, an entry that has a ratio of 0
693   // but does not actually intersect is given a value of -1
694   var oldRatio = oldEntry && oldEntry.isIntersecting ?
695       oldEntry.intersectionRatio || 0 : -1;
696   var newRatio = newEntry.isIntersecting ?
697       newEntry.intersectionRatio || 0 : -1;
699   // Ignore unchanged ratios
700   if (oldRatio === newRatio) return;
702   for (var i = 0; i < this.thresholds.length; i++) {
703     var threshold = this.thresholds[i];
705     // Return true if an entry matches a threshold or if the new ratio
706     // and the old ratio are on the opposite sides of a threshold.
707     if (threshold == oldRatio || threshold == newRatio ||
708         threshold < oldRatio !== threshold < newRatio) {
709       return true;
710     }
711   }
716  * Returns whether or not the root element is an element and is in the DOM.
717  * @return {boolean} True if the root element is an element and is in the DOM.
718  * @private
719  */
720 IntersectionObserver.prototype._rootIsInDom = function() {
721   return !this.root || containsDeep(document, this.root);
726  * Returns whether or not the target element is a child of root.
727  * @param {Element} target The target element to check.
728  * @return {boolean} True if the target element is a child of root.
729  * @private
730  */
731 IntersectionObserver.prototype._rootContainsTarget = function(target) {
732   var rootDoc =
733     (this.root && (this.root.ownerDocument || this.root)) || document;
734   return (
735     containsDeep(rootDoc, target) &&
736     (!this.root || rootDoc == target.ownerDocument)
737   );
742  * Adds the instance to the global IntersectionObserver registry if it isn't
743  * already present.
744  * @private
745  */
746 IntersectionObserver.prototype._registerInstance = function() {
747   if (registry.indexOf(this) < 0) {
748     registry.push(this);
749   }
754  * Removes the instance from the global IntersectionObserver registry.
755  * @private
756  */
757 IntersectionObserver.prototype._unregisterInstance = function() {
758   var index = registry.indexOf(this);
759   if (index != -1) registry.splice(index, 1);
764  * Returns the result of the performance.now() method or null in browsers
765  * that don't support the API.
766  * @return {number} The elapsed time since the page was requested.
767  */
768 function now() {
769   return window.performance && performance.now && performance.now();
774  * Throttles a function and delays its execution, so it's only called at most
775  * once within a given time period.
776  * @param {Function} fn The function to throttle.
777  * @param {number} timeout The amount of time that must pass before the
778  *     function can be called again.
779  * @return {Function} The throttled function.
780  */
781 function throttle(fn, timeout) {
782   var timer = null;
783   return function () {
784     if (!timer) {
785       timer = setTimeout(function() {
786         fn();
787         timer = null;
788       }, timeout);
789     }
790   };
795  * Adds an event handler to a DOM node ensuring cross-browser compatibility.
796  * @param {Node} node The DOM node to add the event handler to.
797  * @param {string} event The event name.
798  * @param {Function} fn The event handler to add.
799  * @param {boolean} opt_useCapture Optionally adds the even to the capture
800  *     phase. Note: this only works in modern browsers.
801  */
802 function addEvent(node, event, fn, opt_useCapture) {
803   if (typeof node.addEventListener == 'function') {
804     node.addEventListener(event, fn, opt_useCapture || false);
805   }
806   else if (typeof node.attachEvent == 'function') {
807     node.attachEvent('on' + event, fn);
808   }
813  * Removes a previously added event handler from a DOM node.
814  * @param {Node} node The DOM node to remove the event handler from.
815  * @param {string} event The event name.
816  * @param {Function} fn The event handler to remove.
817  * @param {boolean} opt_useCapture If the event handler was added with this
818  *     flag set to true, it should be set to true here in order to remove it.
819  */
820 function removeEvent(node, event, fn, opt_useCapture) {
821   if (typeof node.removeEventListener == 'function') {
822     node.removeEventListener(event, fn, opt_useCapture || false);
823   }
824   else if (typeof node.detatchEvent == 'function') {
825     node.detatchEvent('on' + event, fn);
826   }
831  * Returns the intersection between two rect objects.
832  * @param {Object} rect1 The first rect.
833  * @param {Object} rect2 The second rect.
834  * @return {?Object|?ClientRect} The intersection rect or undefined if no
835  *     intersection is found.
836  */
837 function computeRectIntersection(rect1, rect2) {
838   var top = Math.max(rect1.top, rect2.top);
839   var bottom = Math.min(rect1.bottom, rect2.bottom);
840   var left = Math.max(rect1.left, rect2.left);
841   var right = Math.min(rect1.right, rect2.right);
842   var width = right - left;
843   var height = bottom - top;
845   return (width >= 0 && height >= 0) && {
846     top: top,
847     bottom: bottom,
848     left: left,
849     right: right,
850     width: width,
851     height: height
852   } || null;
857  * Shims the native getBoundingClientRect for compatibility with older IE.
858  * @param {Element} el The element whose bounding rect to get.
859  * @return {DOMRect|ClientRect} The (possibly shimmed) rect of the element.
860  */
861 function getBoundingClientRect(el) {
862   var rect;
864   try {
865     rect = el.getBoundingClientRect();
866   } catch (err) {
867     // Ignore Windows 7 IE11 "Unspecified error"
868     // https://github.com/w3c/IntersectionObserver/pull/205
869   }
871   if (!rect) return getEmptyRect();
873   // Older IE
874   if (!(rect.width && rect.height)) {
875     rect = {
876       top: rect.top,
877       right: rect.right,
878       bottom: rect.bottom,
879       left: rect.left,
880       width: rect.right - rect.left,
881       height: rect.bottom - rect.top
882     };
883   }
884   return rect;
889  * Returns an empty rect object. An empty rect is returned when an element
890  * is not in the DOM.
891  * @return {ClientRect} The empty rect.
892  */
893 function getEmptyRect() {
894   return {
895     top: 0,
896     bottom: 0,
897     left: 0,
898     right: 0,
899     width: 0,
900     height: 0
901   };
906  * Ensure that the result has all of the necessary fields of the DOMRect.
907  * Specifically this ensures that `x` and `y` fields are set.
909  * @param {?DOMRect|?ClientRect} rect
910  * @return {?DOMRect}
911  */
912 function ensureDOMRect(rect) {
913   // A `DOMRect` object has `x` and `y` fields.
914   if (!rect || 'x' in rect) {
915     return rect;
916   }
917   // A IE's `ClientRect` type does not have `x` and `y`. The same is the case
918   // for internally calculated Rect objects. For the purposes of
919   // `IntersectionObserver`, it's sufficient to simply mirror `left` and `top`
920   // for these fields.
921   return {
922     top: rect.top,
923     y: rect.top,
924     bottom: rect.bottom,
925     left: rect.left,
926     x: rect.left,
927     right: rect.right,
928     width: rect.width,
929     height: rect.height
930   };
935  * Inverts the intersection and bounding rect from the parent (frame) BCR to
936  * the local BCR space.
937  * @param {DOMRect|ClientRect} parentBoundingRect The parent's bound client rect.
938  * @param {DOMRect|ClientRect} parentIntersectionRect The parent's own intersection rect.
939  * @return {ClientRect} The local root bounding rect for the parent's children.
940  */
941 function convertFromParentRect(parentBoundingRect, parentIntersectionRect) {
942   var top = parentIntersectionRect.top - parentBoundingRect.top;
943   var left = parentIntersectionRect.left - parentBoundingRect.left;
944   return {
945     top: top,
946     left: left,
947     height: parentIntersectionRect.height,
948     width: parentIntersectionRect.width,
949     bottom: top + parentIntersectionRect.height,
950     right: left + parentIntersectionRect.width
951   };
956  * Checks to see if a parent element contains a child element (including inside
957  * shadow DOM).
958  * @param {Node} parent The parent element.
959  * @param {Node} child The child element.
960  * @return {boolean} True if the parent node contains the child node.
961  */
962 function containsDeep(parent, child) {
963   var node = child;
964   while (node) {
965     if (node == parent) return true;
967     node = getParentNode(node);
968   }
969   return false;
974  * Gets the parent node of an element or its host element if the parent node
975  * is a shadow root.
976  * @param {Node} node The node whose parent to get.
977  * @return {Node|null} The parent node or null if no parent exists.
978  */
979 function getParentNode(node) {
980   var parent = node.parentNode;
982   if (node.nodeType == /* DOCUMENT */ 9 && node != document) {
983     // If this node is a document node, look for the embedding frame.
984     return getFrameElement(node);
985   }
987   // If the parent has element that is assigned through shadow root slot
988   if (parent && parent.assignedSlot) {
989     parent = parent.assignedSlot.parentNode
990   }
992   if (parent && parent.nodeType == 11 && parent.host) {
993     // If the parent is a shadow root, return the host element.
994     return parent.host;
995   }
997   return parent;
1001  * Returns true if `node` is a Document.
1002  * @param {!Node} node
1003  * @returns {boolean}
1004  */
1005 function isDoc(node) {
1006   return node && node.nodeType === 9;
1010 // Exposes the constructors globally.
1011 window.IntersectionObserver = IntersectionObserver;
1012 window.IntersectionObserverEntry = IntersectionObserverEntry;
1014 }());