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