intersection-observer
Advanced tools
Comparing version
@@ -944,2 +944,653 @@ /** | ||
describe('same-origin iframe', function() { | ||
var iframe, win, doc; | ||
var iframeTargetEl1, iframeTargetEl2; | ||
var bodyWidth; | ||
beforeEach(function(done) { | ||
iframe = document.createElement('iframe'); | ||
iframe.setAttribute('frameborder', '0'); | ||
iframe.setAttribute('scrolling', 'yes'); | ||
iframe.style.position = 'fixed'; | ||
iframe.style.top = '0px'; | ||
iframe.style.width = '100px'; | ||
iframe.style.height = '200px'; | ||
iframe.onerror = function() { | ||
done(new Error('iframe initialization failed')); | ||
}; | ||
iframe.onload = function() { | ||
iframe.onload = null; | ||
iframeWin = iframe.contentWindow; | ||
iframeDoc = iframeWin.document; | ||
iframeDoc.open(); | ||
iframeDoc.write('<!DOCTYPE html><html><body>'); | ||
iframeDoc.write('<style>'); | ||
iframeDoc.write('body {margin: 0}'); | ||
iframeDoc.write('.target {height: 200px; margin-bottom: 2px; background: blue;}'); | ||
iframeDoc.write('</style>'); | ||
iframeDoc.close(); | ||
function createTarget(id, bg) { | ||
var target = iframeDoc.createElement('div'); | ||
target.id = id; | ||
target.className = 'target'; | ||
target.style.background = bg; | ||
iframeDoc.body.appendChild(target); | ||
return target; | ||
} | ||
iframeTargetEl1 = createTarget('target1', 'blue'); | ||
iframeTargetEl2 = createTarget('target2', 'green'); | ||
bodyWidth = iframeDoc.body.clientWidth; | ||
done(); | ||
}; | ||
iframe.src = 'about:blank'; | ||
rootEl.appendChild(iframe); | ||
}); | ||
afterEach(function() { | ||
rootEl.removeChild(iframe); | ||
}); | ||
function rect(r) { | ||
return { | ||
top: r.top, | ||
left: r.left, | ||
width: r.width != null ? r.width : r.right - r.left, | ||
height: r.height != null ? r.height : r.bottom - r.top, | ||
right: r.right != null ? r.right : r.left + r.width, | ||
bottom: r.bottom != null ? r.bottom : r.top + r.height | ||
}; | ||
} | ||
function getRootRect(doc) { | ||
var html = doc.documentElement; | ||
var body = doc.body; | ||
return rect({ | ||
top: 0, | ||
left: 0, | ||
right: html.clientWidth || body.clientWidth, | ||
width: html.clientWidth || body.clientWidth, | ||
bottom: html.clientHeight || body.clientHeight, | ||
height: html.clientHeight || body.clientHeight | ||
}); | ||
} | ||
it('iframe targets do not intersect with a top root element', function(done) { | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(records[0].isIntersecting).to.be(false); | ||
expect(records[1].isIntersecting).to.be(false); | ||
done(); | ||
io.disconnect(); | ||
}, {root: rootEl}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('triggers for all targets in top-level root', function(done) { | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[1].isIntersecting).to.be(false); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
// The rootBounds is for the document's root. | ||
expect(records[0].rootBounds.height).to.be(innerHeight); | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('triggers for all targets in iframe-level root', function(done) { | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[1].intersectionRatio).to.be(1); | ||
// The rootBounds is for the document's root. | ||
expect(rect(records[0].rootBounds)). | ||
to.eql(rect(iframeDoc.body.getBoundingClientRect())); | ||
done(); | ||
io.disconnect(); | ||
}, {root: iframeDoc.body}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a fully visible frame', function(done) { | ||
iframe.style.top = '0px'; | ||
iframe.style.height = '300px'; | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is fully visible. | ||
var clientRect1 = rect({ | ||
top: 0, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(clientRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a fully visible and offset frame', function(done) { | ||
iframe.style.top = '10px'; | ||
iframe.style.height = '300px'; | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is fully visible. | ||
var clientRect1 = rect({ | ||
top: 0, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(clientRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.5 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a clipped frame on top', function(done) { | ||
iframe.style.top = '-10px'; | ||
iframe.style.height = '300px'; | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is clipped at the top by the iframe's clipping. | ||
var clientRect1 = rect({ | ||
top: 0, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect1 = rect({ | ||
left: 0, | ||
width: bodyWidth, | ||
// Top is clipped. | ||
top: 10, | ||
height: 200 - 10 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95 | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.48, 0.5); // ~0.49 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a clipped frame on bottom', function(done) { | ||
iframe.style.top = 'auto'; | ||
iframe.style.bottom = '-10px'; | ||
iframe.style.height = '300px'; | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is clipped at the top by the iframe's clipping. | ||
var clientRect1 = rect({ | ||
top: 0, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(clientRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 - 10 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.43, 0.45); // ~0.44 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a fully visible frame and scrolled', function(done) { | ||
iframe.style.top = '0px'; | ||
iframe.style.height = '300px'; | ||
iframeWin.scrollTo(0, 10); | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is fully visible. | ||
var clientRect1 = rect({ | ||
top: -10, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect1 = rect({ | ||
top: 0, | ||
left: 0, | ||
width: bodyWidth, | ||
// Height is only for the visible area. | ||
height: 200 - 10 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.within(0.94, 0.96); // ~0.95 | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202 - 10, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202 - 10, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('calculates rects for a clipped frame on top and scrolled', function(done) { | ||
iframe.style.top = '-10px'; | ||
iframe.style.height = '300px'; | ||
iframeWin.scrollTo(0, 10); | ||
var io = new IntersectionObserver(function(unsortedRecords) { | ||
var records = sortRecords(unsortedRecords); | ||
expect(records.length).to.be(2); | ||
expect(rect(records[0].rootBounds)).to.eql(getRootRect(document)); | ||
expect(rect(records[1].rootBounds)).to.eql(getRootRect(document)); | ||
// The target1 is clipped at the top by the iframe's clipping. | ||
var clientRect1 = rect({ | ||
top: -10, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect1 = rect({ | ||
left: 0, | ||
width: bodyWidth, | ||
// Top is clipped. | ||
top: 10, | ||
// The height is less by both: offset and scroll. | ||
height: 200 - 10 - 10 | ||
}); | ||
expect(rect(records[0].boundingClientRect)).to.eql(clientRect1); | ||
expect(rect(records[0].intersectionRect)).to.eql(intersectRect1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[0].intersectionRatio).to.within(0.89, 0.91); // ~0.9 | ||
// The target2 is partially visible. | ||
var clientRect2 = rect({ | ||
top: 202 - 10, | ||
left: 0, | ||
width: bodyWidth, | ||
height: 200 | ||
}); | ||
var intersectRect2 = rect({ | ||
top: 202 - 10, | ||
left: 0, | ||
width: bodyWidth, | ||
// The bottom is clipped off. | ||
bottom: 300 | ||
}); | ||
expect(rect(records[1].boundingClientRect)).to.eql(clientRect2); | ||
expect(rect(records[1].intersectionRect)).to.eql(intersectRect2); | ||
expect(records[1].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54 | ||
done(); | ||
io.disconnect(); | ||
}); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
}); | ||
it('handles style changes', function(done) { | ||
var spy = sinon.spy(); | ||
// When first element becomes invisible, the second element will show. | ||
// And in reverse: when the first element becomes visible again, the | ||
// second element will disappear. | ||
var io = new IntersectionObserver(spy); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
runSequence([ | ||
function(done) { | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(1); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
expect(records[1].isIntersecting).to.be(false); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
iframeTargetEl1.style.display = 'none'; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(2); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(0); | ||
expect(records[0].isIntersecting).to.be(false); | ||
expect(records[1].intersectionRatio).to.be(1); | ||
expect(records[1].isIntersecting).to.be(true); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
iframeTargetEl1.style.display = ''; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(3); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
expect(records[1].isIntersecting).to.be(false); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
io.disconnect(); | ||
done(); | ||
} | ||
], done); | ||
}); | ||
it('handles scroll changes', function(done) { | ||
var spy = sinon.spy(); | ||
// Scrolling to the middle of the iframe shows the second box and | ||
// hides the first. | ||
var io = new IntersectionObserver(spy); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
runSequence([ | ||
function(done) { | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(1); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
expect(records[1].isIntersecting).to.be(false); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
iframeWin.scrollTo(0, 202); | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(2); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(0); | ||
expect(records[0].isIntersecting).to.be(false); | ||
expect(records[1].intersectionRatio).to.be(1); | ||
expect(records[1].isIntersecting).to.be(true); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
iframeWin.scrollTo(0, 0); | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(3); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
expect(records[1].isIntersecting).to.be(false); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
io.disconnect(); | ||
done(); | ||
} | ||
], done); | ||
}); | ||
it('handles iframe changes', function(done) { | ||
var spy = sinon.spy(); | ||
// Iframe goes off screen and returns. | ||
var io = new IntersectionObserver(spy); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
runSequence([ | ||
function(done) { | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(1); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(2); | ||
expect(records[0].intersectionRatio).to.be(1); | ||
expect(records[0].isIntersecting).to.be(true); | ||
expect(records[1].intersectionRatio).to.be(0); | ||
expect(records[1].isIntersecting).to.be(false); | ||
// Top-level bounds. | ||
expect(records[0].rootBounds.height).to.be(innerHeight); | ||
expect(records[0].intersectionRect.height).to.be(200); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
// Completely off screen. | ||
iframe.style.top = '-202px'; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(2); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(1); | ||
expect(records[0].intersectionRatio).to.be(0); | ||
expect(records[0].isIntersecting).to.be(false); | ||
// Top-level bounds. | ||
expect(records[0].rootBounds.height).to.be(innerHeight); | ||
expect(records[0].intersectionRect.height).to.be(0); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
// Partially returns. | ||
iframe.style.top = '-100px'; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(3); | ||
var records = sortRecords(spy.lastCall.args[0]); | ||
expect(records.length).to.be(1); | ||
expect(records[0].intersectionRatio).to.be.within(0.45, 0.55); | ||
expect(records[0].isIntersecting).to.be(true); | ||
// Top-level bounds. | ||
expect(records[0].rootBounds.height).to.be(innerHeight); | ||
expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
io.disconnect(); | ||
done(); | ||
} | ||
], done); | ||
}); | ||
it('continues to monitor until the last target unobserved', function(done) { | ||
var spy = sinon.spy(); | ||
// Iframe goes off screen and returns. | ||
var io = new IntersectionObserver(spy); | ||
io.observe(target1); | ||
io.observe(iframeTargetEl1); | ||
io.observe(iframeTargetEl2); | ||
runSequence([ | ||
function(done) { | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(1); | ||
expect(spy.lastCall.args[0].length).to.be(3); | ||
// Unobserve one from the main context and one from iframe. | ||
io.unobserve(target1); | ||
io.unobserve(iframeTargetEl2); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
// Completely off screen. | ||
iframe.style.top = '-202px'; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(2); | ||
expect(spy.lastCall.args[0].length).to.be(1); | ||
io.unobserve(iframeTargetEl1); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
// Partially returns. | ||
iframe.style.top = '-100px'; | ||
setTimeout(function() { | ||
expect(spy.callCount).to.be(2); | ||
done(); | ||
}, ASYNC_TIMEOUT); | ||
}, | ||
function(done) { | ||
io.disconnect(); | ||
done(); | ||
} | ||
], done); | ||
}); | ||
}); | ||
}); | ||
@@ -946,0 +1597,0 @@ |
@@ -121,2 +121,7 @@ /** | ||
}).join(' '); | ||
/** @private @const {!Array<!Document>} */ | ||
this._monitoringDocuments = []; | ||
/** @private @const {!Array<function()>} */ | ||
this._monitoringUnsubscribes = []; | ||
} | ||
@@ -166,3 +171,3 @@ | ||
this._observationTargets.push({element: target, entry: null}); | ||
this._monitorIntersections(); | ||
this._monitorIntersections(target.ownerDocument); | ||
this._checkForIntersections(); | ||
@@ -179,7 +184,6 @@ }; | ||
this._observationTargets.filter(function(item) { | ||
return item.element != target; | ||
}); | ||
if (!this._observationTargets.length) { | ||
this._unmonitorIntersections(); | ||
return item.element != target; | ||
}); | ||
this._unmonitorIntersections(target.ownerDocument); | ||
if (this._observationTargets.length == 0) { | ||
this._unregisterInstance(); | ||
@@ -195,3 +199,3 @@ } | ||
this._observationTargets = []; | ||
this._unmonitorIntersections(); | ||
this._unmonitorAllIntersections(); | ||
this._unregisterInstance(); | ||
@@ -269,28 +273,64 @@ }; | ||
* happening, and if the page's visibility state is visible. | ||
* @param {!Document} doc | ||
* @private | ||
*/ | ||
IntersectionObserver.prototype._monitorIntersections = function() { | ||
if (!this._monitoringIntersections) { | ||
this._monitoringIntersections = true; | ||
IntersectionObserver.prototype._monitorIntersections = function(doc) { | ||
var win = doc.defaultView; | ||
if (!win) { | ||
// Already destroyed. | ||
return; | ||
} | ||
if (this._monitoringDocuments.indexOf(doc) != -1) { | ||
// Already monitoring. | ||
return; | ||
} | ||
// If a poll interval is set, use polling instead of listening to | ||
// resize and scroll events or DOM mutations. | ||
if (this.POLL_INTERVAL) { | ||
this._monitoringInterval = setInterval( | ||
this._checkForIntersections, this.POLL_INTERVAL); | ||
// Private state for monitoring. | ||
var callback = this._checkForIntersections; | ||
var monitoringInterval = null; | ||
var domObserver = null; | ||
// If a poll interval is set, use polling instead of listening to | ||
// resize and scroll events or DOM mutations. | ||
if (this.POLL_INTERVAL) { | ||
monitoringInterval = win.setInterval(callback, this.POLL_INTERVAL); | ||
} else { | ||
addEvent(win, 'resize', callback, true); | ||
addEvent(doc, 'scroll', callback, true); | ||
if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in win) { | ||
domObserver = new win.MutationObserver(callback); | ||
domObserver.observe(doc, { | ||
attributes: true, | ||
childList: true, | ||
characterData: true, | ||
subtree: true | ||
}); | ||
} | ||
else { | ||
addEvent(window, 'resize', this._checkForIntersections, true); | ||
addEvent(document, 'scroll', this._checkForIntersections, true); | ||
} | ||
if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) { | ||
this._domObserver = new MutationObserver(this._checkForIntersections); | ||
this._domObserver.observe(document, { | ||
attributes: true, | ||
childList: true, | ||
characterData: true, | ||
subtree: true | ||
}); | ||
this._monitoringDocuments.push(doc); | ||
this._monitoringUnsubscribes.push(function() { | ||
// Get the window object again. When a friendly iframe is destroyed, it | ||
// will be null. | ||
var win = doc.defaultView; | ||
if (win) { | ||
if (monitoringInterval) { | ||
win.clearInterval(monitoringInterval); | ||
} | ||
removeEvent(win, 'resize', callback, true); | ||
} | ||
removeEvent(doc, 'scroll', callback, true); | ||
if (domObserver) { | ||
domObserver.disconnect(); | ||
} | ||
}); | ||
// Also monitor the parent. | ||
if (doc != (this.root && this.root.ownerDocument || document)) { | ||
var frame = getFrameElement(doc); | ||
if (frame) { | ||
this._monitorIntersections(frame.ownerDocument); | ||
} | ||
} | ||
@@ -302,17 +342,46 @@ }; | ||
* Stops polling for intersection changes. | ||
* @param {!Document} doc | ||
* @private | ||
*/ | ||
IntersectionObserver.prototype._unmonitorIntersections = function() { | ||
if (this._monitoringIntersections) { | ||
this._monitoringIntersections = false; | ||
IntersectionObserver.prototype._unmonitorIntersections = function(doc) { | ||
var index = this._monitoringDocuments.indexOf(doc); | ||
if (index == -1) { | ||
return; | ||
} | ||
clearInterval(this._monitoringInterval); | ||
this._monitoringInterval = null; | ||
var rootDoc = (this.root && this.root.ownerDocument || document); | ||
removeEvent(window, 'resize', this._checkForIntersections, true); | ||
removeEvent(document, 'scroll', this._checkForIntersections, true); | ||
// Check if any dependent targets are still remaining. | ||
var hasDependentTargets = | ||
this._observationTargets.some(function(item) { | ||
var itemDoc = item.element.ownerDocument; | ||
// Target is in this context. | ||
if (itemDoc == doc) { | ||
return true; | ||
} | ||
// Target is nested in this context. | ||
while (itemDoc && itemDoc != rootDoc) { | ||
var frame = getFrameElement(itemDoc); | ||
itemDoc = frame && frame.ownerDocument; | ||
if (itemDoc == doc) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}); | ||
if (hasDependentTargets) { | ||
return; | ||
} | ||
if (this._domObserver) { | ||
this._domObserver.disconnect(); | ||
this._domObserver = null; | ||
// Unsubscribe. | ||
var unsubscribe = this._monitoringUnsubscribes[index]; | ||
this._monitoringDocuments.splice(index, 1); | ||
this._monitoringUnsubscribes.splice(index, 1); | ||
unsubscribe(); | ||
// Also unmonitor the parent. | ||
if (doc != rootDoc) { | ||
var frame = getFrameElement(doc); | ||
if (frame) { | ||
this._unmonitorIntersections(frame.ownerDocument); | ||
} | ||
@@ -324,2 +393,17 @@ } | ||
/** | ||
* Stops polling for intersection changes. | ||
* @param {!Document} doc | ||
* @private | ||
*/ | ||
IntersectionObserver.prototype._unmonitorAllIntersections = function() { | ||
var unsubscribes = this._monitoringUnsubscribes.slice(0); | ||
this._monitoringDocuments.length = 0; | ||
this._monitoringUnsubscribes.length = 0; | ||
for (var i = 0; i < unsubscribes.length; i++) { | ||
unsubscribes[i](); | ||
} | ||
}; | ||
/** | ||
* Scans each observation target for intersection changes and adds them | ||
@@ -340,3 +424,3 @@ * to the internal entries queue. If new entries are found, it | ||
var intersectionRect = rootIsInDom && rootContainsTarget && | ||
this._computeTargetAndRootIntersection(target, rootRect); | ||
this._computeTargetAndRootIntersection(target, targetRect, rootRect); | ||
@@ -381,2 +465,3 @@ var newEntry = item.entry = new IntersectionObserverEntry({ | ||
* @param {Element} target The target DOM element | ||
* @param {Object} targetRect The bounding rect of the target. | ||
* @param {Object} rootRect The bounding rect of the root after being | ||
@@ -389,8 +474,6 @@ * expanded by the rootMargin value. | ||
IntersectionObserver.prototype._computeTargetAndRootIntersection = | ||
function(target, rootRect) { | ||
function(target, targetRect, rootRect) { | ||
// If the element isn't displayed, an intersection can't happen. | ||
if (window.getComputedStyle(target).display == 'none') return; | ||
var targetRect = getBoundingClientRect(target); | ||
var intersectionRect = targetRect; | ||
@@ -400,3 +483,3 @@ var parent = getParentNode(target); | ||
while (!atRoot) { | ||
while (!atRoot && parent) { | ||
var parentRect = null; | ||
@@ -407,7 +490,23 @@ var parentComputedStyle = parent.nodeType == 1 ? | ||
// If the parent isn't displayed, an intersection can't happen. | ||
if (parentComputedStyle.display == 'none') return; | ||
if (parentComputedStyle.display == 'none') return null; | ||
if (parent == this.root || parent == document) { | ||
if (parent == this.root || parent.nodeType == /* DOCUMENT */ 9) { | ||
atRoot = true; | ||
parentRect = rootRect; | ||
if (parent == this.root || parent == document) { | ||
parentRect = rootRect; | ||
} else { | ||
// Check if there's a frame that can be navigated to. | ||
var frame = getParentNode(parent); | ||
var frameRect = frame && getBoundingClientRect(frame); | ||
var frameIntersect = | ||
frame && | ||
this._computeTargetAndRootIntersection(frame, frameRect, rootRect); | ||
if (frameRect && frameIntersect) { | ||
parent = frame; | ||
parentRect = convertFromParentRect(frameRect, frameIntersect); | ||
} else { | ||
parent = null; | ||
intersectionRect = null; | ||
} | ||
} | ||
} else { | ||
@@ -418,4 +517,5 @@ // If the element has a non-visible overflow, and it's not the <body> | ||
// the document rect, so no need to compute a new intersection. | ||
if (parent != document.body && | ||
parent != document.documentElement && | ||
var doc = parent.ownerDocument; | ||
if (parent != doc.body && | ||
parent != doc.documentElement && | ||
parentComputedStyle.overflow != 'visible') { | ||
@@ -430,6 +530,5 @@ parentRect = getBoundingClientRect(parent); | ||
intersectionRect = computeRectIntersection(parentRect, intersectionRect); | ||
if (!intersectionRect) break; | ||
} | ||
parent = getParentNode(parent); | ||
if (!intersectionRect) break; | ||
parent = parent && getParentNode(parent); | ||
} | ||
@@ -543,3 +642,4 @@ return intersectionRect; | ||
IntersectionObserver.prototype._rootContainsTarget = function(target) { | ||
return containsDeep(this.root || document, target); | ||
return containsDeep(this.root || document, target) && | ||
(!this.root || this.root.ownerDocument == target.ownerDocument); | ||
}; | ||
@@ -659,3 +759,3 @@ | ||
height: height | ||
}; | ||
} || null; | ||
} | ||
@@ -712,3 +812,25 @@ | ||
/** | ||
* Inverts the intersection and bounding rect from the parent (frame) BCR to | ||
* the local BCR space. | ||
* @param {Object} parentBoundingRect The parent's bound client rect. | ||
* @param {Object} parentIntersectionRect The parent's own intersection rect. | ||
* @return {Object} The local root bounding rect for the parent's children. | ||
*/ | ||
function convertFromParentRect(parentBoundingRect, parentIntersectionRect) { | ||
var top = parentIntersectionRect.top - parentBoundingRect.top; | ||
var left = parentIntersectionRect.left - parentBoundingRect.left; | ||
return { | ||
top: top, | ||
left: left, | ||
height: parentIntersectionRect.height, | ||
width: parentIntersectionRect.width, | ||
bottom: top + parentIntersectionRect.height, | ||
right: left + parentIntersectionRect.width | ||
}; | ||
} | ||
/** | ||
* Checks to see if a parent element contains a child element (including inside | ||
@@ -740,2 +862,7 @@ * shadow DOM). | ||
if (node.nodeType == /* DOCUMENT */ 9 && node != document) { | ||
// If this node is a document node, look for the embedding frame. | ||
return getFrameElement(node); | ||
} | ||
if (parent && parent.nodeType == 11 && parent.host) { | ||
@@ -755,2 +882,17 @@ // If the parent is a shadow root, return the host element. | ||
/** | ||
* Returns the embedding frame element, if any. | ||
* @param {!Document} doc | ||
* @return {!Element} | ||
*/ | ||
function getFrameElement(doc) { | ||
try { | ||
return doc.defaultView && doc.defaultView.frameElement || null; | ||
} catch (e) { | ||
// Ignore the error. | ||
return null; | ||
} | ||
} | ||
// Exposes the constructors globally. | ||
@@ -757,0 +899,0 @@ window.IntersectionObserver = IntersectionObserver; |
{ | ||
"name": "intersection-observer", | ||
"version": "0.7.0", | ||
"version": "0.8.0", | ||
"description": "A polyfill for IntersectionObserver", | ||
@@ -5,0 +5,0 @@ "main": "intersection-observer", |
@@ -101,6 +101,4 @@ # `IntersectionObserver` polyfill | ||
This polyfill does not attempt to report intersections across same-origin `iframe` boundaries. While supporting same-origin iframes is technically possible, it would drastically reduce performance. Since most `iframe` usage on the web is cross-origin, this polyfill chooses to optimize for performantly handling the most common use cases. (Note: neither this polyfill nor native implementations support reporting intersections across cross-origin `iframe` boundaries.) | ||
This polyfill does not support the [proposed v2 additions](https://github.com/szager-chromium/IntersectionObserver/blob/v2/explainer.md), as these features are not currently possible to do with JavaScript and existing web APIs. | ||
This polyfill also does not support the [proposed v2 additions](https://github.com/szager-chromium/IntersectionObserver/blob/v2/explainer.md), as these features are not currently possible to do with JavaScript and existing web APIs. | ||
## Browser support | ||
@@ -110,6 +108,6 @@ | ||
Legacy support is also possible in very old browsers by including a shim for ES5 as well as the `window.getComputedStyle` method. The easiest way to load the IntersectionObserver polyfill and have it work in the widest range of browsers is via [polyfill.io](https://cdn.polyfill.io/v2/docs/), which will automatically include dependencies where necessary: | ||
Legacy support is also possible in very old browsers by including a shim for ES5 as well as the `window.getComputedStyle` method. The easiest way to load the IntersectionObserver polyfill and have it work in the widest range of browsers is via [polyfill.io](https://cdn.polyfill.io/v3/), which will automatically include dependencies where necessary: | ||
```html | ||
<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script> | ||
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script> | ||
``` | ||
@@ -116,0 +114,0 @@ |
Sorry, the diff of this file is not supported yet
96144
38.7%2302
46.44%153
-1.29%