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