Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
44a763e
Add IntersectionObserver polyfill
surma Mar 16, 2016
7ddc159
Check root element in constructor
surma Mar 19, 2016
f07ac8d
Typo
surma Mar 19, 2016
c9e5bf8
Debounce scroll event
surma Mar 19, 2016
5b319fb
Fix tests
surma Mar 19, 2016
3ecdcb4
Use MutationObserver
surma Mar 28, 2016
903b6d7
Add license header
surma Mar 28, 2016
5b66682
Calculate a rects width and height if not given
surma Apr 1, 2016
d28204b
Code review
surma Apr 4, 2016
d7d1ccd
Minor typo
surma Apr 4, 2016
2bc3346
Move tests to mocha
surma Apr 9, 2016
176e10f
Fix margin tests
surma Apr 9, 2016
fa2ec4b
Use consistent indents
surma Apr 9, 2016
3f19f1f
Improve intersection detection
surma Apr 14, 2016
32823e4
Comment in ratio tests
surma Apr 15, 2016
981e7d3
Implement @jeremenichelli review
surma Apr 21, 2016
2a63fa0
Remove `resize` event listener on disconnect()
surma Apr 21, 2016
b54809d
These styles are redundant - now applied directly to the test element…
triblondon May 3, 2016
fa0ec6d
No longer needed
triblondon May 3, 2016
a6dc1cd
CDNjs annoyingly doesn't have the latest version of expect, so async …
triblondon May 3, 2016
0c2a172
Make it look nicer
triblondon May 3, 2016
016165b
Conventional to load the script under test after the test framework
triblondon May 3, 2016
8dd15a4
Move test definitions into <head>. Start testing at onload to avoid …
triblondon May 3, 2016
141e7e2
Conventional to put test cases in a test suite
triblondon May 3, 2016
1cc2190
Change async test implementation to support more legacy browsers, mor…
triblondon May 3, 2016
93577cc
Add a test
triblondon May 3, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions polyfill/intersectionobserver-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: rootMargin are completely ignored for now
(function(scope) {
if('IntersectionObserver' in window) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polyfill service prefers not to include feature tests inside the polyfill, because you may want to apply a polyfill over a native implementation in some cases, and detects are often too generous (this is actually a perfect case in point, because it will pass on the existing partial implementation in Chrome - though I realise it's behind a flag so not - yet - an issue in the wild)

Copy link

@jeremenichelli jeremenichelli Apr 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't like to overlap with a native implementation. In that case it would be necessary to see what is the partial part of the spec that's out and detect that and fix it like in classList API which Opera and IE10 doesn't support multiple class names or second argument in toggle method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll can add an option to overwrite a native implementation, but I don’t think that should be the default behavior. I think the expected behavior is to do nothing if the browser has support for IO and polyfill it if it doesn’t.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fair enough. It's not an important point and I accept the wider benefits.


var POLL_INTERVAL = 100;

// Constructor
var IntersectionObserver = function(callback, options) {
options = options || {};

if(!(callback instanceof Function)) {
throw('callback needs to be a function');
}

if(options.root && !(options.root instanceof HTMLElement)) {
throw('Root needs to be an HTMLElement');
}

this._callback = callback;
this._root = options.root || null;
this._rootMargin = options.rootMargin || [0, 0, 0, 0];
this._thresholds = options.threshold || [0];
this._init();
};

IntersectionObserver.prototype = {
//
// Public API
//
get root() {
return this._root || document;
},

get rootMargin() {
return '0';
},

get thresholds() {
if(this._thresholds instanceof Array) {
return this._thresholds;
}
return [this._thresholds];
},

observe: function(target) {
if(this._observationTargets.has(target)) {
return;
}
if(!(target instanceof HTMLElement)) {
throw('Target needs to be an HTMLelement');
}

var root = this.root;
var ancestor = target.parentNode;
if(!root.contains(target)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IE11 does not have the contains method, it needs to be polyfilled if wanting that browser support.

throw('Target must be descendant of root');
}
this._mutationObserver.observe(target.parentNode, {
attributes: true,
childList: true,
characterData: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to observe characterData changes to track the target element's visibility? Can you use attributeFilter to observe only changes in the target's dimension attributes?

subtree: true
});
this._observationTargets.set(target, {});
this._update();
},

unobserve: function(target) {
this._observationTargets.delete(target);
},

disconnect: function() {
this._observationTargets.clear();
this.root.removeEventListener('scroll', this._boundUpdate);
scope.removeEventListener('resize', this._boundUpdate);
this._mutationObserver.disconnect();
this._descheduleCallback();
},

takeRecords: function() {
this._update();
this._descheduleCallback();
var copy = this._queue.slice();
this._queue = [];
return copy;
},

//
// Private API
//
_init: function() {
this._observationTargets = new Map();
this._boundUpdate = throttle(this._update.bind(this), POLL_INTERVAL);
this.root.addEventListener('scroll', this._boundUpdate);
scope.addEventListener('resize', this._boundUpdate);
this._mutationObserver = new MutationObserver(this._boundUpdate);
this._mutationObserver.observe(this.root, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
this._queue = [];
},

_update: function() {
var rootRect = this._rootRect();
this._observationTargets.forEach(function(oldIntersectionEntry, target) {
var targetRect = getBoundingClientRect(target);
var intersectionRect = intersectRects(rootRect, targetRect);
if(!intersectionRect) {
return;
}
var targetArea = targetRect.width * targetRect.height;
var intersectionArea = intersectionRect.width * intersectionRect.height;
var intersectionRatio = intersectionArea / targetArea;
if(!this._hasCrossedThreshold(oldIntersectionEntry.intersectionRatio || 0, intersectionRatio)) {
return;
}
var intersectionEntry = {
time: scope.performance.now(),
rootBounds: rootRect,
boundingClientRect: targetRect,
intersectionRect: intersectionRect,
intersectionRatio: intersectionRatio,
target: target
};
Object.freeze(intersectionEntry);
this._queue.push(intersectionEntry);
this._scheduleCallback();
this._observationTargets.set(target, intersectionEntry);
}.bind(this));
},

_scheduleCallback: function() {
if(this._timeoutId) {
return;
}
this._timeoutId = scope.setTimeout(function() {
this._descheduleCallback();
this._callback(this._queue, this);
this._queue = [];
}.bind(this), POLL_INTERVAL);
},

_descheduleCallback: function() {
if(!this._timeoutId) {
return;
}
scope.clearTimeout(this._timeoutId);
this._timeoutId = null;
},

_rootRect: function() {
if(this._root) {
return getBoundingClientRect(this.root);
}
return {
top: 0,
left: 0,
right: scope.innerWidth,
width: scope.innerWidth,
bottom: scope.innerHeight,
height: scope.innerHeight
};
},

_hasCrossedThreshold: function(oldRatio, newRatio) {
if(oldRatio === newRatio) {
return;
}
var isOnBoundary = this.thresholds.map(function(threshold) {
return threshold == newRatio;
}).indexOf(true) !== -1;

var b1 = this.thresholds.map(function(threshold) {
return threshold < oldRatio;
});
var b2 = this.thresholds.map(function(threshold) {
return threshold < newRatio;
});
var hasCrossedThreshold = b1.map(function(_, idx) {
return b1[idx] !== b2[idx]
}).indexOf(true) !== -1;
return isOnBoundary || hasCrossedThreshold;
}
};

var intersectRects = function(rect1, rect2) {
var top = Math.max(rect1.top, rect2.top);
var bottom = Math.min(rect1.bottom, rect2.bottom);
var left = Math.max(rect1.left, rect2.left);
var right = Math.min(rect1.right, rect2.right);
var intersectionRect = {
top: top,
bottom: bottom,
left: left,
right: right,
width: right - left,
height: bottom - top
};
if(top > bottom) {
intersectionRect.height = 0;
}
if(left > right) {
intersectionRect.width = 0;
}
return intersectionRect;
};

scope.IntersectionObserver = IntersectionObserver;

var throttle = function(fn, int) {
var timer = null;
return function () {
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(this, arguments);
timer = null;
}.bind(this), int);
};
};

var getBoundingClientRect = function(el) {
var r = el.getBoundingClientRect();
if(!r) {
return null;
}
// Older IE
r.width = r.width || r.right - r.left;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safari throws an read-only error on setting this property. I fixed it by checking if the property is writable using the property descriptor getter.

var desc = Object.getOwnPropertyDescriptor(r, 'width');
if (desc && desc.writable) {
  r.width = r.width || r.right - r.left;
  r.height = r.height || r.bottom - r.top;
}

Copy link

@jeremenichelli jeremenichelli May 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have this problem when using getBoundingClientRect, I think the best way to get around this is to return a different object with the rects.

var r = el.getBoundingClientRect();
  if(!r) {
    return null;
  }
  // old IE
  return {
    top: r.top,
    bottom: r.bottom,
    left: r.left,
    right: r.right,
    width: r.width || r.right - r.left,
    height: r.height || r.bottom - r.top
  }

Extra code, but safe.

r.height = r.height || r.bottom - r.top;
return r;
};
})(this);
Loading