You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
9.4 KiB
JavaScript
336 lines
9.4 KiB
JavaScript
// Legacy Viewport helper.
|
|
|
|
(function( $ ) {
|
|
|
|
'use strict';
|
|
|
|
var instance = null;
|
|
var $window = $(window);
|
|
|
|
var ViewportItem = function( options ) {
|
|
var self = this;
|
|
|
|
// Get defaults
|
|
$.extend( self, ViewportItem.options, options, ViewportItem.settings );
|
|
|
|
// The whole point is to have a callback function.
|
|
// Don't do anything if it's not given
|
|
if ( !$.isFunction( self.enter ) ) {
|
|
throw new TypeError('Viewport.add :: No `enter` function provided in Viewport options.');
|
|
}
|
|
|
|
// Threshold can be a percentage. Parse it.
|
|
if ( (typeof self.threshold === 'string' && self.threshold.indexOf('%') > -1 ) ) {
|
|
self.isThresholdPercentage = true;
|
|
self.threshold = parseFloat( self.threshold ) / 100;
|
|
|
|
} else if ( self.threshold < 1 && self.threshold > 0 ) {
|
|
self.isThresholdPercentage = true;
|
|
}
|
|
|
|
self.hasLeaveCallback = $.isFunction( self.leave );
|
|
self.$element = $( self.element );
|
|
|
|
// Cache element's offsets and dimensions
|
|
self.update();
|
|
};
|
|
|
|
ViewportItem.prototype.update = function() {
|
|
var self = this;
|
|
|
|
self.offset = self.$element.offset();
|
|
self.height = self.$element.height();
|
|
self.width = self.$element.width();
|
|
};
|
|
|
|
ViewportItem.options = {
|
|
threshold: 200,
|
|
delay: 0
|
|
};
|
|
|
|
ViewportItem.settings = {
|
|
triggered: false,
|
|
isThresholdPercentage: false
|
|
};
|
|
|
|
var Viewport = function() {
|
|
this.init();
|
|
};
|
|
|
|
Viewport.prototype = {
|
|
|
|
init : function() {
|
|
var self = this;
|
|
|
|
self.list = [];
|
|
self.lastScrollY = 0;
|
|
self.windowHeight = $window.height();
|
|
self.windowWidth = $window.width();
|
|
self.throttleTime = 100;
|
|
|
|
self.onResize();
|
|
self.bindEvents();
|
|
|
|
// What's nice here is that rAF won't execute until the user is on this tab,
|
|
// so if they open the page in a new tab which they aren't looking at,
|
|
// this will execute when they come back to that tab
|
|
self.willProcessNextFrame = true;
|
|
requestAnimationFrame(function() {
|
|
self.setScrollTop();
|
|
self.process();
|
|
self.willProcessNextFrame = false;
|
|
});
|
|
},
|
|
|
|
bindEvents : function() {
|
|
var self = this,
|
|
refresh;
|
|
|
|
// Updates offsets after a zero timeout
|
|
refresh = function() {
|
|
setTimeout(function refreshWithDelay() {
|
|
self.refresh();
|
|
}, 0);
|
|
};
|
|
|
|
// Listen for global resize
|
|
$window.on('resize.viewport', $.proxy( self.onResize, self ));
|
|
|
|
// Throttle scrolling because it doesn't need to be super accurate
|
|
$window.on('scroll.viewport', $.proxy( self.onScroll, self ));
|
|
|
|
self.hasActiveHandlers = true;
|
|
},
|
|
|
|
unbindEvents : function() {
|
|
$window.off('.viewport');
|
|
|
|
this.hasActiveHandlers = false;
|
|
},
|
|
|
|
maybeUnbindEvents : function() {
|
|
var self = this;
|
|
|
|
// Not currently watching anything, unbind events
|
|
if ( !self.list.length ) {
|
|
self.unbindEvents();
|
|
}
|
|
},
|
|
|
|
add : function( viewportItem ) {
|
|
var self = this;
|
|
|
|
self.list.push( viewportItem );
|
|
|
|
// Event handlers are removed if a callback is triggered and the
|
|
// watch list is empty. Because modules are instantiated asynchronously,
|
|
// another module could potentially add itself to the watch list when the events
|
|
// have been unbound.
|
|
// Check here if events have been unbound and bind them again if they have
|
|
if ( !self.hasActiveHandlers ) {
|
|
self.bindEvents();
|
|
}
|
|
|
|
if ( !self.willProcessNextFrame ) {
|
|
self.willProcessNextFrame = true;
|
|
requestAnimationFrame(function() {
|
|
self.willProcessNextFrame = false;
|
|
self.process();
|
|
});
|
|
}
|
|
},
|
|
|
|
saveDimensions : function() {
|
|
var self = this;
|
|
|
|
$.each(self.list, function( i, viewportItem ) {
|
|
viewportItem.update();
|
|
});
|
|
|
|
// self.documentHeight
|
|
self.windowHeight = $window.height();
|
|
self.windowWidth = $window.width();
|
|
},
|
|
|
|
// Throttled scroll event
|
|
onScroll : function() {
|
|
var self = this;
|
|
|
|
// No point in doing anything if there aren't any viewports to watch
|
|
if ( !self.list.length ) {
|
|
return;
|
|
}
|
|
|
|
// Save the new scroll top
|
|
self.setScrollTop();
|
|
|
|
self.process();
|
|
},
|
|
|
|
// Debounced resize event
|
|
onResize : function() {
|
|
this.refresh();
|
|
},
|
|
|
|
refresh : function() {
|
|
|
|
// No point in doing anything if there aren't any viewports to watch
|
|
if ( !this.list.length ) {
|
|
return;
|
|
}
|
|
|
|
// Update offsets and width/height for each viewport item
|
|
this.saveDimensions();
|
|
|
|
},
|
|
|
|
isInViewport : function( viewportItem ) {
|
|
var self = this,
|
|
offset = viewportItem.offset,
|
|
threshold = viewportItem.threshold,
|
|
percentage = threshold,
|
|
st = self.lastScrollY,
|
|
isTopInView;
|
|
|
|
|
|
if ( viewportItem.isThresholdPercentage ) {
|
|
threshold = 0;
|
|
}
|
|
|
|
// Other checks could be added here in the future
|
|
isTopInView = self.isTopInView( st, self.windowHeight, offset.top, viewportItem.height, threshold );
|
|
|
|
// If the top isn't in view with zero threshold,
|
|
// don't bother checking if it's at a percent of the window
|
|
if ( isTopInView && viewportItem.isThresholdPercentage ) {
|
|
isTopInView = self.isTopPastPercent( st, self.windowHeight, offset.top, viewportItem.height, percentage );
|
|
}
|
|
|
|
return isTopInView;
|
|
},
|
|
|
|
// If the top of the element (plus the threshold) is past the viewport's top
|
|
// and the top of the element (plus the threshold) is not past the viewport's bottom.
|
|
// Then the top is in view.
|
|
isTopInView : function( viewportTop, viewportHeight, elementTop, elementHeight, threshold ) {
|
|
var viewportBottom = viewportTop + viewportHeight;
|
|
return (elementTop + threshold) >= viewportTop && (elementTop + threshold) < viewportBottom;
|
|
},
|
|
|
|
isTopPastPercent : function( viewportTop, viewportHeight, elementTop, elementHeight, percentage ) {
|
|
var viewportBottom = viewportTop + viewportHeight,
|
|
distFromViewportBottomToElementTop = viewportBottom - elementTop,
|
|
percentFromBottom = distFromViewportBottomToElementTop / viewportHeight;
|
|
return percentFromBottom >= percentage;
|
|
},
|
|
|
|
isOutOfViewport : function( viewport, side ) {
|
|
var self = this,
|
|
offset = viewport.offset,
|
|
st = self.lastScrollY,
|
|
bool;
|
|
|
|
if ( side === 'bottom' ) {
|
|
bool = !self.isBottomInView( st, self.windowHeight, offset.top, viewport.height );
|
|
}
|
|
|
|
return bool;
|
|
},
|
|
|
|
isBottomInView : function( viewportTop, viewportHeight, elementTop, elementHeight ) {
|
|
var viewportBottom = viewportTop + viewportHeight,
|
|
elementBottom = elementTop + elementHeight;
|
|
return elementBottom > viewportTop && elementBottom <= viewportBottom;
|
|
},
|
|
|
|
triggerEnter : function( viewportItem ) {
|
|
var self = this;
|
|
|
|
// Queue up the callback with the delay. Default is 0
|
|
setTimeout(function() {
|
|
viewportItem.enter.call( viewportItem.element, viewportItem );
|
|
}, viewportItem.delay);
|
|
|
|
|
|
if ( $.isFunction( viewportItem.leave ) ) {
|
|
viewportItem.triggered = true;
|
|
|
|
// If the leave property is not a function,
|
|
// The module no longer needs to watch it, so remove from list
|
|
// However, the list may have been modified already in this loop, so find the
|
|
// index of the viewport item instead of using the loop index.
|
|
} else {
|
|
self.list.splice( $.inArray( viewportItem, self.list ), 1 );
|
|
}
|
|
|
|
// If there are no more, unbind from scroll and resize events
|
|
self.maybeUnbindEvents();
|
|
},
|
|
|
|
triggerLeave : function( viewportItem ) {
|
|
// var self = this;
|
|
|
|
// Queue up the callback with the delay. Default is 0
|
|
setTimeout(function() {
|
|
viewportItem.leave.call( viewportItem.element, viewportItem );
|
|
}, viewportItem.delay);
|
|
|
|
viewportItem.triggered = false;
|
|
},
|
|
|
|
setScrollTop : function() {
|
|
// Save the new scroll top
|
|
this.lastScrollY = $window.scrollTop();
|
|
},
|
|
|
|
process : function() {
|
|
var self = this,
|
|
|
|
// The list can possibly be modified mid loop,
|
|
// so the loop needs a copy of the variable which won't be modified
|
|
list = $.extend( [], self.list );
|
|
|
|
$.each(list, function( i, viewportItem ) {
|
|
var isInViewport = self.isInViewport( viewportItem ),
|
|
isBottomOutOfView = viewportItem.hasLeaveCallback && self.isOutOfViewport( viewportItem, 'bottom' );
|
|
|
|
// If the enter callback hasn't been triggerd and it's in the viewport,
|
|
// trigger the enter callback
|
|
if ( !viewportItem.triggered && isInViewport ) {
|
|
return self.triggerEnter( viewportItem );
|
|
}
|
|
|
|
// This viewport has already come into view once and now it is out of view
|
|
// It's not in view, the bottom is out of view, the list item's enter has been triggered
|
|
if ( !isInViewport && isBottomOutOfView && viewportItem.triggered ) {
|
|
return self.triggerLeave( viewportItem );
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
Viewport.add = function( options ) {
|
|
var instance = Viewport.getInstance();
|
|
|
|
return instance.add( new ViewportItem( options ) );
|
|
};
|
|
|
|
|
|
Viewport.refresh = function() {
|
|
Viewport.getInstance().refresh();
|
|
};
|
|
|
|
|
|
Viewport.getInstance = function() {
|
|
if ( !instance ) {
|
|
instance = new Viewport();
|
|
}
|
|
|
|
return instance;
|
|
};
|
|
|
|
|
|
window.Viewport = Viewport;
|
|
}( jQuery ));
|