lazy-images.js 25 KB

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