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.
Vestride_Shuffle/js/viewport.js

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 ));