intersection-observer.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. /**
  2. * Copyright 2016 Google Inc. All Rights Reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. (function(window, document) {
  17. 'use strict';
  18. // Exits early if all IntersectionObserver and IntersectionObserverEntry
  19. // features are natively supported.
  20. if ('IntersectionObserver' in window &&
  21. 'IntersectionObserverEntry' in window &&
  22. '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;
  34. }
  35. /**
  36. * An IntersectionObserver registry. This registry exists to hold a strong
  37. * reference to IntersectionObserver instances currently observering a target
  38. * element. Without this registry, instances without another reference may be
  39. * garbage collected.
  40. */
  41. var registry = [];
  42. /**
  43. * Creates the global IntersectionObserverEntry constructor.
  44. * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
  45. * @param {Object} entry A dictionary of instance properties.
  46. * @constructor
  47. */
  48. function IntersectionObserverEntry(entry) {
  49. this.time = entry.time;
  50. this.target = entry.target;
  51. this.rootBounds = entry.rootBounds;
  52. this.boundingClientRect = entry.boundingClientRect;
  53. this.intersectionRect = entry.intersectionRect || getEmptyRect();
  54. this.isIntersecting = !!entry.intersectionRect;
  55. // Calculates the intersection ratio.
  56. var targetRect = this.boundingClientRect;
  57. var targetArea = targetRect.width * targetRect.height;
  58. var intersectionRect = this.intersectionRect;
  59. var intersectionArea = intersectionRect.width * intersectionRect.height;
  60. // Sets intersection ratio.
  61. if (targetArea) {
  62. this.intersectionRatio = intersectionArea / targetArea;
  63. } else {
  64. // If area is zero and is intersecting, sets to 1, otherwise to 0
  65. this.intersectionRatio = this.isIntersecting ? 1 : 0;
  66. }
  67. }
  68. /**
  69. * Creates the global IntersectionObserver constructor.
  70. * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
  71. * @param {Function} callback The function to be invoked after intersection
  72. * changes have queued. The function is not invoked if the queue has
  73. * been emptied by calling the `takeRecords` method.
  74. * @param {Object=} opt_options Optional configuration options.
  75. * @constructor
  76. */
  77. function IntersectionObserver(callback, opt_options) {
  78. var options = opt_options || {};
  79. if (typeof callback != 'function') {
  80. throw new Error('callback must be a function');
  81. }
  82. if (options.root && options.root.nodeType != 1) {
  83. throw new Error('root must be an Element');
  84. }
  85. // Binds and throttles `this._checkForIntersections`.
  86. this._checkForIntersections = throttle(
  87. this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
  88. // Private properties.
  89. this._callback = callback;
  90. this._observationTargets = [];
  91. this._queuedEntries = [];
  92. this._rootMarginValues = this._parseRootMargin(options.rootMargin);
  93. // Public properties.
  94. this.thresholds = this._initThresholds(options.threshold);
  95. this.root = options.root || null;
  96. this.rootMargin = this._rootMarginValues.map(function(margin) {
  97. return margin.value + margin.unit;
  98. }).join(' ');
  99. }
  100. /**
  101. * The minimum interval within which the document will be checked for
  102. * intersection changes.
  103. */
  104. IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
  105. /**
  106. * The frequency in which the polyfill polls for intersection changes.
  107. * this can be updated on a per instance basis and must be set prior to
  108. * calling `observe` on the first target.
  109. */
  110. IntersectionObserver.prototype.POLL_INTERVAL = null;
  111. /**
  112. * Starts observing a target element for intersection changes based on
  113. * the thresholds values.
  114. * @param {Element} target The DOM element to observe.
  115. */
  116. IntersectionObserver.prototype.observe = function(target) {
  117. var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
  118. return item.element == target;
  119. });
  120. if (isTargetAlreadyObserved) {
  121. return;
  122. }
  123. if (!(target && target.nodeType == 1)) {
  124. throw new Error('target must be an Element');
  125. }
  126. this._registerInstance();
  127. this._observationTargets.push({element: target, entry: null});
  128. this._monitorIntersections();
  129. this._checkForIntersections();
  130. };
  131. /**
  132. * Stops observing a target element for intersection changes.
  133. * @param {Element} target The DOM element to observe.
  134. */
  135. IntersectionObserver.prototype.unobserve = function(target) {
  136. this._observationTargets =
  137. this._observationTargets.filter(function(item) {
  138. return item.element != target;
  139. });
  140. if (!this._observationTargets.length) {
  141. this._unmonitorIntersections();
  142. this._unregisterInstance();
  143. }
  144. };
  145. /**
  146. * Stops observing all target elements for intersection changes.
  147. */
  148. IntersectionObserver.prototype.disconnect = function() {
  149. this._observationTargets = [];
  150. this._unmonitorIntersections();
  151. this._unregisterInstance();
  152. };
  153. /**
  154. * Returns any queue entries that have not yet been reported to the
  155. * callback and clears the queue. This can be used in conjunction with the
  156. * callback to obtain the absolute most up-to-date intersection information.
  157. * @return {Array} The currently queued entries.
  158. */
  159. IntersectionObserver.prototype.takeRecords = function() {
  160. var records = this._queuedEntries.slice();
  161. this._queuedEntries = [];
  162. return records;
  163. };
  164. /**
  165. * Accepts the threshold value from the user configuration object and
  166. * returns a sorted array of unique threshold values. If a value is not
  167. * between 0 and 1 and error is thrown.
  168. * @private
  169. * @param {Array|number=} opt_threshold An optional threshold value or
  170. * a list of threshold values, defaulting to [0].
  171. * @return {Array} A sorted list of unique and valid threshold values.
  172. */
  173. IntersectionObserver.prototype._initThresholds = function(opt_threshold) {
  174. var threshold = opt_threshold || [0];
  175. if (!Array.isArray(threshold)) threshold = [threshold];
  176. return threshold.sort().filter(function(t, i, a) {
  177. if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
  178. throw new Error('threshold must be a number between 0 and 1 inclusively');
  179. }
  180. return t !== a[i - 1];
  181. });
  182. };
  183. /**
  184. * Accepts the rootMargin value from the user configuration object
  185. * and returns an array of the four margin values as an object containing
  186. * the value and unit properties. If any of the values are not properly
  187. * formatted or use a unit other than px or %, and error is thrown.
  188. * @private
  189. * @param {string=} opt_rootMargin An optional rootMargin value,
  190. * defaulting to '0px'.
  191. * @return {Array<Object>} An array of margin objects with the keys
  192. * value and unit.
  193. */
  194. IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) {
  195. var marginString = opt_rootMargin || '0px';
  196. var margins = marginString.split(/\s+/).map(function(margin) {
  197. var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
  198. if (!parts) {
  199. throw new Error('rootMargin must be specified in pixels or percent');
  200. }
  201. return {value: parseFloat(parts[1]), unit: parts[2]};
  202. });
  203. // Handles shorthand.
  204. margins[1] = margins[1] || margins[0];
  205. margins[2] = margins[2] || margins[0];
  206. margins[3] = margins[3] || margins[1];
  207. return margins;
  208. };
  209. /**
  210. * Starts polling for intersection changes if the polling is not already
  211. * happening, and if the page's visibilty state is visible.
  212. * @private
  213. */
  214. IntersectionObserver.prototype._monitorIntersections = function() {
  215. if (!this._monitoringIntersections) {
  216. this._monitoringIntersections = true;
  217. // If a poll interval is set, use polling instead of listening to
  218. // resize and scroll events or DOM mutations.
  219. if (this.POLL_INTERVAL) {
  220. this._monitoringInterval = setInterval(
  221. this._checkForIntersections, this.POLL_INTERVAL);
  222. }
  223. else {
  224. addEvent(window, 'resize', this._checkForIntersections, true);
  225. addEvent(document, 'scroll', this._checkForIntersections, true);
  226. if ('MutationObserver' in window) {
  227. this._domObserver = new MutationObserver(this._checkForIntersections);
  228. this._domObserver.observe(document, {
  229. attributes: true,
  230. childList: true,
  231. characterData: true,
  232. subtree: true
  233. });
  234. }
  235. }
  236. }
  237. };
  238. /**
  239. * Stops polling for intersection changes.
  240. * @private
  241. */
  242. IntersectionObserver.prototype._unmonitorIntersections = function() {
  243. if (this._monitoringIntersections) {
  244. this._monitoringIntersections = false;
  245. clearInterval(this._monitoringInterval);
  246. this._monitoringInterval = null;
  247. removeEvent(window, 'resize', this._checkForIntersections, true);
  248. removeEvent(document, 'scroll', this._checkForIntersections, true);
  249. if (this._domObserver) {
  250. this._domObserver.disconnect();
  251. this._domObserver = null;
  252. }
  253. }
  254. };
  255. /**
  256. * Scans each observation target for intersection changes and adds them
  257. * to the internal entries queue. If new entries are found, it
  258. * schedules the callback to be invoked.
  259. * @private
  260. */
  261. IntersectionObserver.prototype._checkForIntersections = function() {
  262. var rootIsInDom = this._rootIsInDom();
  263. var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
  264. this._observationTargets.forEach(function(item) {
  265. var target = item.element;
  266. var targetRect = getBoundingClientRect(target);
  267. var rootContainsTarget = this._rootContainsTarget(target);
  268. var oldEntry = item.entry;
  269. var intersectionRect = rootIsInDom && rootContainsTarget &&
  270. this._computeTargetAndRootIntersection(target, rootRect);
  271. var newEntry = item.entry = new IntersectionObserverEntry({
  272. time: now(),
  273. target: target,
  274. boundingClientRect: targetRect,
  275. rootBounds: rootRect,
  276. intersectionRect: intersectionRect
  277. });
  278. if (!oldEntry) {
  279. this._queuedEntries.push(newEntry);
  280. } else if (rootIsInDom && rootContainsTarget) {
  281. // If the new entry intersection ratio has crossed any of the
  282. // thresholds, add a new entry.
  283. if (this._hasCrossedThreshold(oldEntry, newEntry)) {
  284. this._queuedEntries.push(newEntry);
  285. }
  286. } else {
  287. // If the root is not in the DOM or target is not contained within
  288. // root but the previous entry for this target had an intersection,
  289. // add a new record indicating removal.
  290. if (oldEntry && oldEntry.isIntersecting) {
  291. this._queuedEntries.push(newEntry);
  292. }
  293. }
  294. }, this);
  295. if (this._queuedEntries.length) {
  296. this._callback(this.takeRecords(), this);
  297. }
  298. };
  299. /**
  300. * Accepts a target and root rect computes the intersection between then
  301. * following the algorithm in the spec.
  302. * TODO(philipwalton): at this time clip-path is not considered.
  303. * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
  304. * @param {Element} target The target DOM element
  305. * @param {Object} rootRect The bounding rect of the root after being
  306. * expanded by the rootMargin value.
  307. * @return {?Object} The final intersection rect object or undefined if no
  308. * intersection is found.
  309. * @private
  310. */
  311. IntersectionObserver.prototype._computeTargetAndRootIntersection =
  312. function(target, rootRect) {
  313. // If the element isn't displayed, an intersection can't happen.
  314. if (window.getComputedStyle(target).display == 'none') return;
  315. var targetRect = getBoundingClientRect(target);
  316. var intersectionRect = targetRect;
  317. var parent = getParentNode(target);
  318. var atRoot = false;
  319. while (!atRoot) {
  320. var parentRect = null;
  321. var parentComputedStyle = parent.nodeType == 1 ?
  322. window.getComputedStyle(parent) : {};
  323. // If the parent isn't displayed, an intersection can't happen.
  324. if (parentComputedStyle.display == 'none') return;
  325. if (parent == this.root || parent == document) {
  326. atRoot = true;
  327. parentRect = rootRect;
  328. } else {
  329. // If the element has a non-visible overflow, and it's not the <body>
  330. // or <html> element, update the intersection rect.
  331. // Note: <body> and <html> cannot be clipped to a rect that's not also
  332. // the document rect, so no need to compute a new intersection.
  333. if (parent != document.body &&
  334. parent != document.documentElement &&
  335. parentComputedStyle.overflow != 'visible') {
  336. parentRect = getBoundingClientRect(parent);
  337. }
  338. }
  339. // If either of the above conditionals set a new parentRect,
  340. // calculate new intersection data.
  341. if (parentRect) {
  342. intersectionRect = computeRectIntersection(parentRect, intersectionRect);
  343. if (!intersectionRect) break;
  344. }
  345. parent = getParentNode(parent);
  346. }
  347. return intersectionRect;
  348. };
  349. /**
  350. * Returns the root rect after being expanded by the rootMargin value.
  351. * @return {Object} The expanded root rect.
  352. * @private
  353. */
  354. IntersectionObserver.prototype._getRootRect = function() {
  355. var rootRect;
  356. if (this.root) {
  357. rootRect = getBoundingClientRect(this.root);
  358. } else {
  359. // Use <html>/<body> instead of window since scroll bars affect size.
  360. var html = document.documentElement;
  361. var body = document.body;
  362. rootRect = {
  363. top: 0,
  364. left: 0,
  365. right: html.clientWidth || body.clientWidth,
  366. width: html.clientWidth || body.clientWidth,
  367. bottom: html.clientHeight || body.clientHeight,
  368. height: html.clientHeight || body.clientHeight
  369. };
  370. }
  371. return this._expandRectByRootMargin(rootRect);
  372. };
  373. /**
  374. * Accepts a rect and expands it by the rootMargin value.
  375. * @param {Object} rect The rect object to expand.
  376. * @return {Object} The expanded rect.
  377. * @private
  378. */
  379. IntersectionObserver.prototype._expandRectByRootMargin = function(rect) {
  380. var margins = this._rootMarginValues.map(function(margin, i) {
  381. return margin.unit == 'px' ? margin.value :
  382. margin.value * (i % 2 ? rect.width : rect.height) / 100;
  383. });
  384. var newRect = {
  385. top: rect.top - margins[0],
  386. right: rect.right + margins[1],
  387. bottom: rect.bottom + margins[2],
  388. left: rect.left - margins[3]
  389. };
  390. newRect.width = newRect.right - newRect.left;
  391. newRect.height = newRect.bottom - newRect.top;
  392. return newRect;
  393. };
  394. /**
  395. * Accepts an old and new entry and returns true if at least one of the
  396. * threshold values has been crossed.
  397. * @param {?IntersectionObserverEntry} oldEntry The previous entry for a
  398. * particular target element or null if no previous entry exists.
  399. * @param {IntersectionObserverEntry} newEntry The current entry for a
  400. * particular target element.
  401. * @return {boolean} Returns true if a any threshold has been crossed.
  402. * @private
  403. */
  404. IntersectionObserver.prototype._hasCrossedThreshold =
  405. function(oldEntry, newEntry) {
  406. // To make comparing easier, an entry that has a ratio of 0
  407. // but does not actually intersect is given a value of -1
  408. var oldRatio = oldEntry && oldEntry.isIntersecting ?
  409. oldEntry.intersectionRatio || 0 : -1;
  410. var newRatio = newEntry.isIntersecting ?
  411. newEntry.intersectionRatio || 0 : -1;
  412. // Ignore unchanged ratios
  413. if (oldRatio === newRatio) return;
  414. for (var i = 0; i < this.thresholds.length; i++) {
  415. var threshold = this.thresholds[i];
  416. // Return true if an entry matches a threshold or if the new ratio
  417. // and the old ratio are on the opposite sides of a threshold.
  418. if (threshold == oldRatio || threshold == newRatio ||
  419. threshold < oldRatio !== threshold < newRatio) {
  420. return true;
  421. }
  422. }
  423. };
  424. /**
  425. * Returns whether or not the root element is an element and is in the DOM.
  426. * @return {boolean} True if the root element is an element and is in the DOM.
  427. * @private
  428. */
  429. IntersectionObserver.prototype._rootIsInDom = function() {
  430. return !this.root || containsDeep(document, this.root);
  431. };
  432. /**
  433. * Returns whether or not the target element is a child of root.
  434. * @param {Element} target The target element to check.
  435. * @return {boolean} True if the target element is a child of root.
  436. * @private
  437. */
  438. IntersectionObserver.prototype._rootContainsTarget = function(target) {
  439. return containsDeep(this.root || document, target);
  440. };
  441. /**
  442. * Adds the instance to the global IntersectionObserver registry if it isn't
  443. * already present.
  444. * @private
  445. */
  446. IntersectionObserver.prototype._registerInstance = function() {
  447. if (registry.indexOf(this) < 0) {
  448. registry.push(this);
  449. }
  450. };
  451. /**
  452. * Removes the instance from the global IntersectionObserver registry.
  453. * @private
  454. */
  455. IntersectionObserver.prototype._unregisterInstance = function() {
  456. var index = registry.indexOf(this);
  457. if (index != -1) registry.splice(index, 1);
  458. };
  459. /**
  460. * Returns the result of the performance.now() method or null in browsers
  461. * that don't support the API.
  462. * @return {number} The elapsed time since the page was requested.
  463. */
  464. function now() {
  465. return window.performance && performance.now && performance.now();
  466. }
  467. /**
  468. * Throttles a function and delays its executiong, so it's only called at most
  469. * once within a given time period.
  470. * @param {Function} fn The function to throttle.
  471. * @param {number} timeout The amount of time that must pass before the
  472. * function can be called again.
  473. * @return {Function} The throttled function.
  474. */
  475. function throttle(fn, timeout) {
  476. var timer = null;
  477. return function () {
  478. if (!timer) {
  479. timer = setTimeout(function() {
  480. fn();
  481. timer = null;
  482. }, timeout);
  483. }
  484. };
  485. }
  486. /**
  487. * Adds an event handler to a DOM node ensuring cross-browser compatibility.
  488. * @param {Node} node The DOM node to add the event handler to.
  489. * @param {string} event The event name.
  490. * @param {Function} fn The event handler to add.
  491. * @param {boolean} opt_useCapture Optionally adds the even to the capture
  492. * phase. Note: this only works in modern browsers.
  493. */
  494. function addEvent(node, event, fn, opt_useCapture) {
  495. if (typeof node.addEventListener == 'function') {
  496. node.addEventListener(event, fn, opt_useCapture || false);
  497. }
  498. else if (typeof node.attachEvent == 'function') {
  499. node.attachEvent('on' + event, fn);
  500. }
  501. }
  502. /**
  503. * Removes a previously added event handler from a DOM node.
  504. * @param {Node} node The DOM node to remove the event handler from.
  505. * @param {string} event The event name.
  506. * @param {Function} fn The event handler to remove.
  507. * @param {boolean} opt_useCapture If the event handler was added with this
  508. * flag set to true, it should be set to true here in order to remove it.
  509. */
  510. function removeEvent(node, event, fn, opt_useCapture) {
  511. if (typeof node.removeEventListener == 'function') {
  512. node.removeEventListener(event, fn, opt_useCapture || false);
  513. }
  514. else if (typeof node.detatchEvent == 'function') {
  515. node.detatchEvent('on' + event, fn);
  516. }
  517. }
  518. /**
  519. * Returns the intersection between two rect objects.
  520. * @param {Object} rect1 The first rect.
  521. * @param {Object} rect2 The second rect.
  522. * @return {?Object} The intersection rect or undefined if no intersection
  523. * is found.
  524. */
  525. function computeRectIntersection(rect1, rect2) {
  526. var top = Math.max(rect1.top, rect2.top);
  527. var bottom = Math.min(rect1.bottom, rect2.bottom);
  528. var left = Math.max(rect1.left, rect2.left);
  529. var right = Math.min(rect1.right, rect2.right);
  530. var width = right - left;
  531. var height = bottom - top;
  532. return (width >= 0 && height >= 0) && {
  533. top: top,
  534. bottom: bottom,
  535. left: left,
  536. right: right,
  537. width: width,
  538. height: height
  539. };
  540. }
  541. /**
  542. * Shims the native getBoundingClientRect for compatibility with older IE.
  543. * @param {Element} el The element whose bounding rect to get.
  544. * @return {Object} The (possibly shimmed) rect of the element.
  545. */
  546. function getBoundingClientRect(el) {
  547. var rect;
  548. try {
  549. rect = el.getBoundingClientRect();
  550. } catch (err) {
  551. // Ignore Windows 7 IE11 "Unspecified error"
  552. // https://github.com/w3c/IntersectionObserver/pull/205
  553. }
  554. if (!rect) return getEmptyRect();
  555. // Older IE
  556. if (!(rect.width && rect.height)) {
  557. rect = {
  558. top: rect.top,
  559. right: rect.right,
  560. bottom: rect.bottom,
  561. left: rect.left,
  562. width: rect.right - rect.left,
  563. height: rect.bottom - rect.top
  564. };
  565. }
  566. return rect;
  567. }
  568. /**
  569. * Returns an empty rect object. An empty rect is returned when an element
  570. * is not in the DOM.
  571. * @return {Object} The empty rect.
  572. */
  573. function getEmptyRect() {
  574. return {
  575. top: 0,
  576. bottom: 0,
  577. left: 0,
  578. right: 0,
  579. width: 0,
  580. height: 0
  581. };
  582. }
  583. /**
  584. * Checks to see if a parent element contains a child elemnt (including inside
  585. * shadow DOM).
  586. * @param {Node} parent The parent element.
  587. * @param {Node} child The child element.
  588. * @return {boolean} True if the parent node contains the child node.
  589. */
  590. function containsDeep(parent, child) {
  591. var node = child;
  592. while (node) {
  593. if (node == parent) return true;
  594. node = getParentNode(node);
  595. }
  596. return false;
  597. }
  598. /**
  599. * Gets the parent node of an element or its host element if the parent node
  600. * is a shadow root.
  601. * @param {Node} node The node whose parent to get.
  602. * @return {Node|null} The parent node or null if no parent exists.
  603. */
  604. function getParentNode(node) {
  605. var parent = node.parentNode;
  606. if (parent && parent.nodeType == 11 && parent.host) {
  607. // If the parent is a shadow root, return the host element.
  608. return parent.host;
  609. }
  610. return parent;
  611. }
  612. // Exposes the constructors globally.
  613. window.IntersectionObserver = IntersectionObserver;
  614. window.IntersectionObserverEntry = IntersectionObserverEntry;
  615. }(window, document));