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