Skip to content

Commit 76ba976

Browse files
authored
Merge pull request #465 from samouri/support-iframe-viewport-tracking
Support iframe viewport tracking
2 parents 293adcd + 77d813a commit 76ba976

File tree

3 files changed

+182
-12
lines changed

3 files changed

+182
-12
lines changed

polyfill/intersection-observer-test.js

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,15 @@ describe('IntersectionObserver', function() {
6969
io = new IntersectionObserver(noop);
7070
expect(io.root).to.be(null);
7171

72+
io = new IntersectionObserver(noop, {root: document});
73+
expect(io.root).to.be(document);
74+
7275
io = new IntersectionObserver(noop, {root: rootEl});
7376
expect(io.root).to.be(rootEl);
7477
});
7578

7679

77-
it('throws when root is not an Element', function() {
80+
it('throws when root is not a Document or Element', function() {
7881
expect(function() {
7982
io = new IntersectionObserver(noop, {root: 'foo'});
8083
}).to.throwException();
@@ -1398,6 +1401,89 @@ describe('IntersectionObserver', function() {
13981401
io.observe(iframeTargetEl2);
13991402
});
14001403

1404+
it('handles tracking iframe viewport', function(done) {
1405+
iframe.style.height = '100px';
1406+
iframe.style.top = '100px';
1407+
iframeWin.scrollTo(0, 110);
1408+
// {root:iframeDoc} means to track the iframe viewport.
1409+
var io = new IntersectionObserver(
1410+
function (records) {
1411+
io.unobserve(iframeTargetEl1);
1412+
1413+
var intersectionRect = rect({
1414+
top: 0, // if root=null, then this would be 100.
1415+
left: 0,
1416+
height: 90,
1417+
width: bodyWidth
1418+
});
1419+
expect(records.length).to.be(1);
1420+
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
1421+
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1422+
done();
1423+
},
1424+
{ root: iframeDoc }
1425+
);
1426+
1427+
io.observe(iframeTargetEl1);
1428+
});
1429+
1430+
it('handles tracking iframe viewport with rootMargin', function(done) {
1431+
iframe.style.height = '100px';
1432+
1433+
var io = new IntersectionObserver(
1434+
function (records) {
1435+
io.unobserve(iframeTargetEl1);
1436+
var intersectionRect = rect({
1437+
top: 0, // if root=null, then this would be 100.
1438+
left: 0,
1439+
height: 200,
1440+
width: bodyWidth
1441+
});
1442+
1443+
// rootMargin: 100% --> 3x width + 3x height.
1444+
var expectedRootBounds = rect({
1445+
top: -100,
1446+
left: -bodyWidth,
1447+
width: bodyWidth * 3,
1448+
height: 100 * 3
1449+
});
1450+
expect(records.length).to.be(1);
1451+
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
1452+
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1453+
done();
1454+
},
1455+
{ root: iframeDoc, rootMargin: '100%' }
1456+
);
1457+
1458+
io.observe(iframeTargetEl1);
1459+
});
1460+
1461+
// Current spec indicates that cross-document tracking yields
1462+
// an essentially empty IntersectionObserverEntry.
1463+
// See: https://github.com/w3c/IntersectionObserver/issues/87
1464+
it('does not track cross-document elements', function(done) {
1465+
var io = new IntersectionObserver(
1466+
function (records) {
1467+
io.unobserve(iframeTargetEl1)
1468+
1469+
expect(records.length).to.be(1);
1470+
const zeroesRect = rect({
1471+
top: 0,
1472+
left: 0,
1473+
width: 0,
1474+
height: 0
1475+
});
1476+
expect(rect(records[0].rootBounds)).to.eql(zeroesRect);
1477+
expect(rect(records[0].intersectionRect)).to.eql(zeroesRect);
1478+
expect(records.isIntersecting).false;
1479+
done();
1480+
},
1481+
{ root: document }
1482+
);
1483+
1484+
io.observe(iframeTargetEl1);
1485+
});
1486+
14011487
it('handles style changes', function(done) {
14021488
var spy = sinon.spy();
14031489

@@ -3022,6 +3108,62 @@ describe('IntersectionObserver', function() {
30223108
}
30233109
], done);
30243110
});
3111+
3112+
it('handles tracking iframe viewport', function(done) {
3113+
iframe.style.height = '100px';
3114+
iframe.style.top = '100px';
3115+
iframeWin.scrollTo(0, 110);
3116+
// {root:iframeDoc} means to track the iframe viewport.
3117+
var io = createObserver(
3118+
function (records) {
3119+
io.unobserve(iframeTargetEl1);
3120+
var intersectionRect = rect({
3121+
top: 0, // if root=null, then this would be 100.
3122+
left: 0,
3123+
height: 90,
3124+
width: bodyWidth
3125+
});
3126+
expect(records.length).to.be(1);
3127+
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
3128+
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3129+
done();
3130+
},
3131+
{ root: iframeDoc }
3132+
);
3133+
3134+
io.observe(iframeTargetEl1);
3135+
});
3136+
3137+
it('handles tracking iframe viewport with rootMargin', function(done) {
3138+
iframe.style.height = '100px';
3139+
3140+
var io = createObserver(
3141+
function (records) {
3142+
io.unobserve(iframeTargetEl1);
3143+
var intersectionRect = rect({
3144+
top: 0, // if root=null, then this would be 100.
3145+
left: 0,
3146+
height: 200,
3147+
width: bodyWidth
3148+
});
3149+
3150+
// rootMargin: 100% --> 3x width + 3x height.
3151+
var expectedRootBounds = rect({
3152+
top: -100,
3153+
left: -bodyWidth,
3154+
width: bodyWidth * 3,
3155+
height: 100 * 3
3156+
});
3157+
expect(records.length).to.be(1);
3158+
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
3159+
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3160+
done();
3161+
},
3162+
{ root: iframeDoc, rootMargin: '100%' }
3163+
);
3164+
3165+
io.observe(iframeTargetEl1);
3166+
});
30253167
});
30263168
});
30273169
});

polyfill/intersection-observer.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,12 @@ function IntersectionObserver(callback, opt_options) {
131131
throw new Error('callback must be a function');
132132
}
133133

134-
if (options.root && options.root.nodeType != 1) {
135-
throw new Error('root must be an Element');
134+
if (
135+
options.root &&
136+
options.root.nodeType != 1 &&
137+
options.root.nodeType != 9
138+
) {
139+
throw new Error('root must be a Document or Element');
136140
}
137141

138142
// Binds and throttles `this._checkForIntersections`.
@@ -395,7 +399,9 @@ IntersectionObserver.prototype._monitorIntersections = function(doc) {
395399
});
396400

397401
// Also monitor the parent.
398-
if (doc != (this.root && this.root.ownerDocument || document)) {
402+
var rootDoc =
403+
(this.root && (this.root.ownerDocument || this.root)) || document;
404+
if (doc != rootDoc) {
399405
var frame = getFrameElement(doc);
400406
if (frame) {
401407
this._monitorIntersections(frame.ownerDocument);
@@ -415,7 +421,8 @@ IntersectionObserver.prototype._unmonitorIntersections = function(doc) {
415421
return;
416422
}
417423

418-
var rootDoc = (this.root && this.root.ownerDocument || document);
424+
var rootDoc =
425+
(this.root && (this.root.ownerDocument || this.root)) || document;
419426

420427
// Check if any dependent targets are still remaining.
421428
var hasDependentTargets =
@@ -493,11 +500,18 @@ IntersectionObserver.prototype._checkForIntersections = function() {
493500
var intersectionRect = rootIsInDom && rootContainsTarget &&
494501
this._computeTargetAndRootIntersection(target, targetRect, rootRect);
495502

503+
var rootBounds = null;
504+
if (!this._rootContainsTarget(target)) {
505+
rootBounds = getEmptyRect();
506+
} else if (!crossOriginUpdater || this.root) {
507+
rootBounds = rootRect;
508+
}
509+
496510
var newEntry = item.entry = new IntersectionObserverEntry({
497511
time: now(),
498512
target: target,
499513
boundingClientRect: targetRect,
500-
rootBounds: crossOriginUpdater && !this.root ? null : rootRect,
514+
rootBounds: rootBounds,
501515
intersectionRect: intersectionRect
502516
});
503517

@@ -618,12 +632,13 @@ IntersectionObserver.prototype._computeTargetAndRootIntersection =
618632
*/
619633
IntersectionObserver.prototype._getRootRect = function() {
620634
var rootRect;
621-
if (this.root) {
635+
if (this.root && !isDoc(this.root)) {
622636
rootRect = getBoundingClientRect(this.root);
623637
} else {
624638
// Use <html>/<body> instead of window since scroll bars affect size.
625-
var html = document.documentElement;
626-
var body = document.body;
639+
var doc = isDoc(this.root) ? this.root : document;
640+
var html = doc.documentElement;
641+
var body = doc.body;
627642
rootRect = {
628643
top: 0,
629644
left: 0,
@@ -714,8 +729,12 @@ IntersectionObserver.prototype._rootIsInDom = function() {
714729
* @private
715730
*/
716731
IntersectionObserver.prototype._rootContainsTarget = function(target) {
717-
return containsDeep(this.root || document, target) &&
718-
(!this.root || this.root.ownerDocument == target.ownerDocument);
732+
var rootDoc =
733+
(this.root && (this.root.ownerDocument || this.root)) || document;
734+
return (
735+
containsDeep(rootDoc, target) &&
736+
(!this.root || rootDoc == target.ownerDocument)
737+
);
719738
};
720739

721740

@@ -978,6 +997,15 @@ function getParentNode(node) {
978997
return parent;
979998
}
980999

1000+
/**
1001+
* Returns true if `node` is a Document.
1002+
* @param {!Node} node
1003+
* @returns {boolean}
1004+
*/
1005+
function isDoc(node) {
1006+
return node && node.nodeType === 9;
1007+
}
1008+
9811009

9821010
// Exposes the constructors globally.
9831011
window.IntersectionObserver = IntersectionObserver;

polyfill/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "intersection-observer",
3-
"version": "0.11.0",
3+
"version": "0.12.0",
44
"description": "A polyfill for IntersectionObserver",
55
"main": "intersection-observer",
66
"repository": {

0 commit comments

Comments
 (0)