diff --git a/polyfill/intersection-observer-test.js b/polyfill/intersection-observer-test.js index dfb87dae..79666c18 100644 --- a/polyfill/intersection-observer-test.js +++ b/polyfill/intersection-observer-test.js @@ -69,12 +69,15 @@ describe('IntersectionObserver', function() { io = new IntersectionObserver(noop); expect(io.root).to.be(null); + io = new IntersectionObserver(noop, {root: document}); + expect(io.root).to.be(document); + io = new IntersectionObserver(noop, {root: rootEl}); expect(io.root).to.be(rootEl); }); - it('throws when root is not an Element', function() { + it('throws when root is not a Document or Element', function() { expect(function() { io = new IntersectionObserver(noop, {root: 'foo'}); }).to.throwException(); @@ -1398,6 +1401,89 @@ describe('IntersectionObserver', function() { io.observe(iframeTargetEl2); }); + it('handles tracking iframe viewport', function(done) { + iframe.style.height = '100px'; + iframe.style.top = '100px'; + iframeWin.scrollTo(0, 110); + // {root:iframeDoc} means to track the iframe viewport. + var io = new IntersectionObserver( + function (records) { + io.unobserve(iframeTargetEl1); + + var intersectionRect = rect({ + top: 0, // if root=null, then this would be 100. + left: 0, + height: 90, + width: bodyWidth + }); + expect(records.length).to.be(1); + expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc)); + expect(rect(records[0].intersectionRect)).to.eql(intersectionRect); + done(); + }, + { root: iframeDoc } + ); + + io.observe(iframeTargetEl1); + }); + + it('handles tracking iframe viewport with rootMargin', function(done) { + iframe.style.height = '100px'; + + var io = new IntersectionObserver( + function (records) { + io.unobserve(iframeTargetEl1); + var intersectionRect = rect({ + top: 0, // if root=null, then this would be 100. + left: 0, + height: 200, + width: bodyWidth + }); + + // rootMargin: 100% --> 3x width + 3x height. + var expectedRootBounds = rect({ + top: -100, + left: -bodyWidth, + width: bodyWidth * 3, + height: 100 * 3 + }); + expect(records.length).to.be(1); + expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds); + expect(rect(records[0].intersectionRect)).to.eql(intersectionRect); + done(); + }, + { root: iframeDoc, rootMargin: '100%' } + ); + + io.observe(iframeTargetEl1); + }); + + // Current spec indicates that cross-document tracking yields + // an essentially empty IntersectionObserverEntry. + // See: https://github.com/w3c/IntersectionObserver/issues/87 + it('does not track cross-document elements', function(done) { + var io = new IntersectionObserver( + function (records) { + io.unobserve(iframeTargetEl1) + + expect(records.length).to.be(1); + const zeroesRect = rect({ + top: 0, + left: 0, + width: 0, + height: 0 + }); + expect(rect(records[0].rootBounds)).to.eql(zeroesRect); + expect(rect(records[0].intersectionRect)).to.eql(zeroesRect); + expect(records.isIntersecting).false; + done(); + }, + { root: document } + ); + + io.observe(iframeTargetEl1); + }); + it('handles style changes', function(done) { var spy = sinon.spy(); @@ -3022,6 +3108,62 @@ describe('IntersectionObserver', function() { } ], done); }); + + it('handles tracking iframe viewport', function(done) { + iframe.style.height = '100px'; + iframe.style.top = '100px'; + iframeWin.scrollTo(0, 110); + // {root:iframeDoc} means to track the iframe viewport. + var io = createObserver( + function (records) { + io.unobserve(iframeTargetEl1); + var intersectionRect = rect({ + top: 0, // if root=null, then this would be 100. + left: 0, + height: 90, + width: bodyWidth + }); + expect(records.length).to.be(1); + expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc)); + expect(rect(records[0].intersectionRect)).to.eql(intersectionRect); + done(); + }, + { root: iframeDoc } + ); + + io.observe(iframeTargetEl1); + }); + + it('handles tracking iframe viewport with rootMargin', function(done) { + iframe.style.height = '100px'; + + var io = createObserver( + function (records) { + io.unobserve(iframeTargetEl1); + var intersectionRect = rect({ + top: 0, // if root=null, then this would be 100. + left: 0, + height: 200, + width: bodyWidth + }); + + // rootMargin: 100% --> 3x width + 3x height. + var expectedRootBounds = rect({ + top: -100, + left: -bodyWidth, + width: bodyWidth * 3, + height: 100 * 3 + }); + expect(records.length).to.be(1); + expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds); + expect(rect(records[0].intersectionRect)).to.eql(intersectionRect); + done(); + }, + { root: iframeDoc, rootMargin: '100%' } + ); + + io.observe(iframeTargetEl1); + }); }); }); }); diff --git a/polyfill/intersection-observer.js b/polyfill/intersection-observer.js index 9b619cb1..871140ba 100644 --- a/polyfill/intersection-observer.js +++ b/polyfill/intersection-observer.js @@ -131,8 +131,12 @@ function IntersectionObserver(callback, opt_options) { throw new Error('callback must be a function'); } - if (options.root && options.root.nodeType != 1) { - throw new Error('root must be an Element'); + if ( + options.root && + options.root.nodeType != 1 && + options.root.nodeType != 9 + ) { + throw new Error('root must be a Document or Element'); } // Binds and throttles `this._checkForIntersections`. @@ -395,7 +399,9 @@ IntersectionObserver.prototype._monitorIntersections = function(doc) { }); // Also monitor the parent. - if (doc != (this.root && this.root.ownerDocument || document)) { + var rootDoc = + (this.root && (this.root.ownerDocument || this.root)) || document; + if (doc != rootDoc) { var frame = getFrameElement(doc); if (frame) { this._monitorIntersections(frame.ownerDocument); @@ -415,7 +421,8 @@ IntersectionObserver.prototype._unmonitorIntersections = function(doc) { return; } - var rootDoc = (this.root && this.root.ownerDocument || document); + var rootDoc = + (this.root && (this.root.ownerDocument || this.root)) || document; // Check if any dependent targets are still remaining. var hasDependentTargets = @@ -493,11 +500,18 @@ IntersectionObserver.prototype._checkForIntersections = function() { var intersectionRect = rootIsInDom && rootContainsTarget && this._computeTargetAndRootIntersection(target, targetRect, rootRect); + var rootBounds = null; + if (!this._rootContainsTarget(target)) { + rootBounds = getEmptyRect(); + } else if (!crossOriginUpdater || this.root) { + rootBounds = rootRect; + } + var newEntry = item.entry = new IntersectionObserverEntry({ time: now(), target: target, boundingClientRect: targetRect, - rootBounds: crossOriginUpdater && !this.root ? null : rootRect, + rootBounds: rootBounds, intersectionRect: intersectionRect }); @@ -618,12 +632,13 @@ IntersectionObserver.prototype._computeTargetAndRootIntersection = */ IntersectionObserver.prototype._getRootRect = function() { var rootRect; - if (this.root) { + if (this.root && !isDoc(this.root)) { rootRect = getBoundingClientRect(this.root); } else { // Use / instead of window since scroll bars affect size. - var html = document.documentElement; - var body = document.body; + var doc = isDoc(this.root) ? this.root : document; + var html = doc.documentElement; + var body = doc.body; rootRect = { top: 0, left: 0, @@ -714,8 +729,12 @@ IntersectionObserver.prototype._rootIsInDom = function() { * @private */ IntersectionObserver.prototype._rootContainsTarget = function(target) { - return containsDeep(this.root || document, target) && - (!this.root || this.root.ownerDocument == target.ownerDocument); + var rootDoc = + (this.root && (this.root.ownerDocument || this.root)) || document; + return ( + containsDeep(rootDoc, target) && + (!this.root || rootDoc == target.ownerDocument) + ); }; @@ -978,6 +997,15 @@ function getParentNode(node) { return parent; } +/** + * Returns true if `node` is a Document. + * @param {!Node} node + * @returns {boolean} + */ +function isDoc(node) { + return node && node.nodeType === 9; +} + // Exposes the constructors globally. window.IntersectionObserver = IntersectionObserver; diff --git a/polyfill/package.json b/polyfill/package.json index e8ce0699..0b841fbd 100644 --- a/polyfill/package.json +++ b/polyfill/package.json @@ -1,6 +1,6 @@ { "name": "intersection-observer", - "version": "0.11.0", + "version": "0.12.0", "description": "A polyfill for IntersectionObserver", "main": "intersection-observer", "repository": {