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