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