patashala/assets/js/scrollMonitor.js

351 lines
9.8 KiB
JavaScript
Raw Normal View History

(function( factory ) {
if (typeof define !== 'undefined' && define.amd) {
define(['jquery'], factory);
} else if (typeof module !== 'undefined' && module.exports) {
var $ = require('jquery');
module.exports = factory( $ );
} else {
window.scrollMonitor = factory( jQuery );
}
})(function( $ ) {
var exports = {};
var $window = $(window);
var $document = $(document);
var watchers = [];
var VISIBILITYCHANGE = 'visibilityChange';
var ENTERVIEWPORT = 'enterViewport';
var FULLYENTERVIEWPORT = 'fullyEnterViewport';
var EXITVIEWPORT = 'exitViewport';
var PARTIALLYEXITVIEWPORT = 'partiallyExitViewport';
var LOCATIONCHANGE = 'locationChange';
var STATECHANGE = 'stateChange';
var eventTypes = [
VISIBILITYCHANGE,
ENTERVIEWPORT,
FULLYENTERVIEWPORT,
EXITVIEWPORT,
PARTIALLYEXITVIEWPORT,
LOCATIONCHANGE,
STATECHANGE
];
var defaultOffsets = {top: 0, bottom: 0};
exports.viewportTop;
exports.viewportBottom;
exports.documentHeight;
exports.viewportHeight = windowHeight();
var previousDocumentHeight;
var latestEvent;
function windowHeight() {
return window.innerHeight || document.documentElement.clientHeight;
}
var calculateViewportI;
function calculateViewport() {
exports.viewportTop = $window.scrollTop();
exports.viewportBottom = exports.viewportTop + exports.viewportHeight;
exports.documentHeight = $document.height();
if (exports.documentHeight !== previousDocumentHeight) {
calculateViewportI = watchers.length;
while( calculateViewportI-- ) {
watchers[calculateViewportI].recalculateLocation();
}
previousDocumentHeight = exports.documentHeight;
}
}
function recalculateWatchLocationsAndTrigger() {
exports.viewportHeight = windowHeight();
calculateViewport();
updateAndTriggerWatchers();
}
var recalculateAndTriggerTimer;
function debouncedRecalcuateAndTrigger() {
clearTimeout(recalculateAndTriggerTimer);
recalculateAndTriggerTimer = setTimeout( recalculateWatchLocationsAndTrigger, 100 );
}
var updateAndTriggerWatchersI;
function updateAndTriggerWatchers() {
// update all watchers then trigger the events so one can rely on another being up to date.
updateAndTriggerWatchersI = watchers.length;
while( updateAndTriggerWatchersI-- ) {
watchers[updateAndTriggerWatchersI].update();
}
updateAndTriggerWatchersI = watchers.length;
while( updateAndTriggerWatchersI-- ) {
watchers[updateAndTriggerWatchersI].triggerCallbacks();
}
}
function ElementWatcher( watchItem, offsets ) {
var self = this;
this.watchItem = watchItem;
if (!offsets) {
this.offsets = defaultOffsets;
} else if (offsets === +offsets) {
this.offsets = {top: offsets, bottom: offsets};
} else {
this.offsets = $.extend({}, defaultOffsets, offsets);
}
this.callbacks = {}; // {callback: function, isOne: true }
for (var i = 0, j = eventTypes.length; i < j; i++) {
self.callbacks[eventTypes[i]] = [];
}
this.locked = false;
var wasInViewport;
var wasFullyInViewport;
var wasAboveViewport;
var wasBelowViewport;
var listenerToTriggerListI;
var listener;
function triggerCallbackArray( listeners ) {
if (listeners.length === 0) {
return;
}
listenerToTriggerListI = listeners.length;
while( listenerToTriggerListI-- ) {
listener = listeners[listenerToTriggerListI];
listener.callback.call( self, latestEvent );
if (listener.isOne) {
listeners.splice(listenerToTriggerListI, 1);
}
}
}
this.triggerCallbacks = function triggerCallbacks() {
if (this.isInViewport && !wasInViewport) {
triggerCallbackArray( this.callbacks[ENTERVIEWPORT] );
}
if (this.isFullyInViewport && !wasFullyInViewport) {
triggerCallbackArray( this.callbacks[FULLYENTERVIEWPORT] );
}
if (this.isAboveViewport !== wasAboveViewport &&
this.isBelowViewport !== wasBelowViewport) {
triggerCallbackArray( this.callbacks[VISIBILITYCHANGE] );
// if you skip completely past this element
if (!wasFullyInViewport && !this.isFullyInViewport) {
triggerCallbackArray( this.callbacks[FULLYENTERVIEWPORT] );
triggerCallbackArray( this.callbacks[PARTIALLYEXITVIEWPORT] );
}
if (!wasInViewport && !this.isInViewport) {
triggerCallbackArray( this.callbacks[ENTERVIEWPORT] );
triggerCallbackArray( this.callbacks[EXITVIEWPORT] );
}
}
if (!this.isFullyInViewport && wasFullyInViewport) {
triggerCallbackArray( this.callbacks[PARTIALLYEXITVIEWPORT] );
}
if (!this.isInViewport && wasInViewport) {
triggerCallbackArray( this.callbacks[EXITVIEWPORT] );
}
if (this.isInViewport !== wasInViewport) {
triggerCallbackArray( this.callbacks[VISIBILITYCHANGE] );
}
switch( true ) {
case wasInViewport !== this.isInViewport:
case wasFullyInViewport !== this.isFullyInViewport:
case wasAboveViewport !== this.isAboveViewport:
case wasBelowViewport !== this.isBelowViewport:
triggerCallbackArray( this.callbacks[STATECHANGE] );
}
wasInViewport = this.isInViewport;
wasFullyInViewport = this.isFullyInViewport;
wasAboveViewport = this.isAboveViewport;
wasBelowViewport = this.isBelowViewport;
};
this.recalculateLocation = function() {
if (this.locked) {
return;
}
var previousTop = this.top;
var previousBottom = this.bottom;
if (this.watchItem.nodeName) { // a dom element
var cachedDisplay = this.watchItem.style.display;
if (cachedDisplay === 'none') {
this.watchItem.style.display = '';
}
var elementLocation = $(this.watchItem).offset();
this.top = elementLocation.top;
this.bottom = elementLocation.top + this.watchItem.offsetHeight;
if (cachedDisplay === 'none') {
this.watchItem.style.display = cachedDisplay;
}
} else if (this.watchItem === +this.watchItem) { // number
if (this.watchItem > 0) {
this.top = this.bottom = this.watchItem;
} else {
this.top = this.bottom = exports.documentHeight - this.watchItem;
}
} else { // an object with a top and bottom property
this.top = this.watchItem.top;
this.bottom = this.watchItem.bottom;
}
this.top -= this.offsets.top;
this.bottom += this.offsets.bottom;
this.height = this.bottom - this.top;
if ( (previousTop !== undefined || previousBottom !== undefined) && (this.top !== previousTop || this.bottom !== previousBottom) ) {
triggerCallbackArray( this.callbacks[LOCATIONCHANGE] );
}
};
this.recalculateLocation();
this.update();
wasInViewport = this.isInViewport;
wasFullyInViewport = this.isFullyInViewport;
wasAboveViewport = this.isAboveViewport;
wasBelowViewport = this.isBelowViewport;
}
ElementWatcher.prototype = {
on: function( event, callback, isOne ) {
// trigger the event if it applies to the element right now.
switch( true ) {
case event === VISIBILITYCHANGE && !this.isInViewport && this.isAboveViewport:
case event === ENTERVIEWPORT && this.isInViewport:
case event === FULLYENTERVIEWPORT && this.isFullyInViewport:
case event === EXITVIEWPORT && this.isAboveViewport && !this.isInViewport:
case event === PARTIALLYEXITVIEWPORT && this.isAboveViewport:
callback();
if (isOne) {
return;
}
}
if (this.callbacks[event]) {
this.callbacks[event].push({callback: callback, isOne: isOne});
} else {
throw new Error('Tried to add a scroll monitor listener of type '+event+'. Your options are: '+eventTypes.join(', '));
}
},
off: function( event, callback ) {
if (this.callbacks[event]) {
for (var i = 0, item; item = this.callbacks[event][i]; i++) {
if (item.callback === callback) {
this.callbacks[event].splice(i, 1);
break;
}
}
} else {
throw new Error('Tried to remove a scroll monitor listener of type '+event+'. Your options are: '+eventTypes.join(', '));
}
},
one: function( event, callback ) {
this.on( event, callback, true);
},
recalculateSize: function() {
this.height = this.watchItem.offsetHeight + this.offsets.top + this.offsets.bottom;
this.bottom = this.top + this.height;
},
update: function() {
this.isAboveViewport = this.top < exports.viewportTop;
this.isBelowViewport = this.bottom > exports.viewportBottom;
this.isInViewport = (this.top <= exports.viewportBottom && this.bottom >= exports.viewportTop);
this.isFullyInViewport = (this.top >= exports.viewportTop && this.bottom <= exports.viewportBottom) ||
(this.isAboveViewport && this.isBelowViewport);
},
destroy: function() {
var index = watchers.indexOf(this),
self = this;
watchers.splice(index, 1);
for (var i = 0, j = eventTypes.length; i < j; i++) {
self.callbacks[eventTypes[i]].length = 0;
}
},
// prevent recalculating the element location
lock: function() {
this.locked = true;
},
unlock: function() {
this.locked = false;
}
};
var eventHandlerFactory = function (type) {
return function( callback, isOne ) {
this.on.call(this, type, callback, isOne);
};
};
for (var i = 0, j = eventTypes.length; i < j; i++) {
var type = eventTypes[i];
ElementWatcher.prototype[type] = eventHandlerFactory(type);
}
try {
calculateViewport();
} catch (e) {
$(calculateViewport);
}
function scrollMonitorListener(event) {
latestEvent = event;
calculateViewport();
updateAndTriggerWatchers();
}
$window.on('scroll', scrollMonitorListener);
$window.on('resize', debouncedRecalcuateAndTrigger);
exports.beget = exports.create = function( element, offsets ) {
if (typeof element === 'string') {
element = $(element)[0];
}
if (element instanceof $) {
element = element[0];
}
var watcher = new ElementWatcher( element, offsets );
watchers.push(watcher);
watcher.update();
return watcher;
};
exports.update = function() {
latestEvent = null;
calculateViewport();
updateAndTriggerWatchers();
};
exports.recalculateLocations = function() {
exports.documentHeight = 0;
exports.update();
};
return exports;
});