You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1093 lines
35 KiB

/*!
* # Semantic UI 1.11.4 - Visibility
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2014 Contributors
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
;(function ( $, window, document, undefined ) {
"use strict";
$.fn.visibility = function(parameters) {
var
$allModules = $(this),
moduleSelector = $allModules.selector || '',
time = new Date().getTime(),
performance = [],
query = arguments[0],
methodInvoked = (typeof query == 'string'),
queryArguments = [].slice.call(arguments, 1),
returnedValue
;
$allModules
.each(function() {
var
settings = ( $.isPlainObject(parameters) )
? $.extend(true, {}, $.fn.visibility.settings, parameters)
: $.extend({}, $.fn.visibility.settings),
className = settings.className,
namespace = settings.namespace,
error = settings.error,
eventNamespace = '.' + namespace,
moduleNamespace = 'module-' + namespace,
$window = $(window),
$module = $(this),
$context = $(settings.context),
selector = $module.selector || '',
instance = $module.data(moduleNamespace),
requestAnimationFrame = window.requestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 0); },
element = this,
observer,
module
;
module = {
initialize: function() {
module.debug('Initializing', settings);
module.setup.cache();
module.save.position();
if( module.should.trackChanges() ) {
module.bind.events();
if(settings.type == 'image') {
module.setup.image();
}
if(settings.type == 'fixed') {
module.setup.fixed();
}
if(settings.observeChanges) {
module.observeChanges();
}
if( !module.is.visible() ) {
module.error(error.visible, $module);
}
}
if(settings.initialCheck) {
module.checkVisibility();
}
module.instantiate();
},
instantiate: function() {
module.debug('Storing instance', module);
$module
.data(moduleNamespace, module)
;
instance = module;
},
destroy: function() {
module.verbose('Destroying previous module');
if(observer) {
observer.disconnect();
}
$window
.off('load' + eventNamespace, module.event.load)
.off('resize' + eventNamespace, module.event.resize)
;
$context.off('scrollchange' + eventNamespace, module.event.scrollchange);
$module
.off(eventNamespace)
.removeData(moduleNamespace)
;
},
observeChanges: function() {
var
context = $context[0]
;
if('MutationObserver' in window) {
observer = new MutationObserver(function(mutations) {
module.verbose('DOM tree modified, updating visibility calculations');
module.refresh();
});
observer.observe(element, {
childList : true,
subtree : true
});
module.debug('Setting up mutation observer', observer);
}
},
bind: {
events: function() {
module.verbose('Binding visibility events to scroll and resize');
$window
.on('load' + eventNamespace, module.event.load)
.on('resize' + eventNamespace, module.event.resize)
;
// pub/sub pattern
$context
.off('scroll' + eventNamespace)
.on('scroll' + eventNamespace, module.event.scroll)
.on('scrollchange' + eventNamespace, module.event.scrollchange)
;
},
imageLoad: function() {
var
$images = $module.find('img'),
imageCount = $images.length,
index = imageCount,
loadedCount = 0,
images = [],
cache = [],
cacheImage = document.createElement('img'),
handleLoad = function() {
loadedCount++;
if(loadedCount >= imageCount) {
module.debug('Images finished loading inside element, refreshing position');
module.refresh();
}
}
;
if(imageCount > 0) {
$images
.each(function() {
images.push( $(this).attr('src') );
})
;
while(index--) {
cacheImage = document.createElement('img');
cacheImage.onload = handleLoad;
cacheImage.onerror = handleLoad;
cacheImage.src = images[index];
cache.push(cacheImage);
}
}
}
},
event: {
resize: function() {
module.verbose('Window resized');
requestAnimationFrame(module.refresh);
},
load: function() {
module.verbose('Page finished loading');
requestAnimationFrame(module.refresh);
},
// publishes scrollchange event on one scroll
scroll: function() {
if(settings.throttle) {
clearTimeout(module.timer);
module.timer = setTimeout(function() {
$context.trigger('scrollchange' + eventNamespace, [ $context.scrollTop() ]);
}, settings.throttle);
}
else {
requestAnimationFrame(function() {
$context.trigger('scrollchange' + eventNamespace, [ $context.scrollTop() ]);
});
}
},
// subscribes to scrollchange
scrollchange: function(event, scrollPosition) {
module.checkVisibility(scrollPosition);
},
},
precache: function(images, callback) {
if (!(images instanceof Array)) {
images = [images];
}
var
imagesLength = images.length,
loadedCounter = 0,
cache = [],
cacheImage = document.createElement('img'),
handleLoad = function() {
loadedCounter++;
if (loadedCounter >= images.length) {
if ($.isFunction(callback)) {
callback();
}
}
}
;
while (imagesLength--) {
cacheImage = document.createElement('img');
cacheImage.onload = handleLoad;
cacheImage.onerror = handleLoad;
cacheImage.src = images[imagesLength];
cache.push(cacheImage);
}
},
should: {
trackChanges: function() {
if(methodInvoked) {
module.debug('One time query, no need to bind events');
return false;
}
module.debug('Callbacks being attached');
return true;
}
},
setup: {
cache: function() {
module.cache = {
occurred : {},
screen : {},
element : {},
};
},
image: function() {
var
src = $module.data('src')
;
if(src) {
module.verbose('Lazy loading image', src);
settings.observeChanges = false;
// show when top visible
module.topVisible(function() {
module.debug('Image top visible', element);
module.precache(src, function() {
module.set.image(src);
settings.onTopVisible = false;
});
});
}
},
fixed: function() {
module.verbose('Setting up fixed on element pass');
settings.once = false;
settings.onTopPassed = function() {
$module
.addClass(className.fixed)
.css({
top: settings.offset + 'px'
})
;
if(settings.transition) {
if($.fn.transition !== undefined) {
$module.transition(settings.transition, settings.duration);
}
}
};
settings.onTopPassedReverse = function() {
$module
.removeClass(className.fixed)
.css({
position: '',
top: ''
})
;
};
}
},
set: {
image: function(src) {
var
offScreen = (module.cache.screen.bottom < module.cache.element.top)
;
$module
.attr('src', src)
;
if(offScreen) {
module.verbose('Image outside browser, no show animation');
$module.show();
}
else {
if(settings.transition) {
if( $.fn.transition !== undefined ) {
$module.transition(settings.transition, settings.duration);
}
else {
$module.fadeIn(settings.duration);
}
}
else {
$module.show();
}
}
}
},
is: {
visible: function() {
if(module.cache && module.cache.element) {
return (module.cache.element.width > 0);
}
return false;
}
},
refresh: function() {
module.debug('Refreshing constants (element width/height)');
module.reset();
module.save.position();
module.checkVisibility();
settings.onRefresh.call(element);
},
reset: function() {
module.verbose('Reseting all cached values');
if( $.isPlainObject(module.cache) ) {
module.cache.screen = {};
module.cache.element = {};
}
},
checkVisibility: function(scroll) {
module.verbose('Checking visibility of element', module.cache.element);
if( module.is.visible() ) {
// save scroll position
module.save.scroll(scroll);
// update calculations derived from scroll
module.save.calculations();
// percentage
module.passed();
// reverse (must be first)
module.passingReverse();
module.topVisibleReverse();
module.bottomVisibleReverse();
module.topPassedReverse();
module.bottomPassedReverse();
// one time
module.passing();
module.topVisible();
module.bottomVisible();
module.topPassed();
module.bottomPassed();
// on update callback
if(settings.onUpdate) {
settings.onUpdate.call(element, module.get.elementCalculations());
}
}
},
passed: function(amount, newCallback) {
var
calculations = module.get.elementCalculations(),
amountInPixels
;
// assign callback
if(amount !== undefined && newCallback !== undefined) {
settings.onPassed[amount] = newCallback;
}
else if(amount !== undefined) {
return (module.get.pixelsPassed(amount) > calculations.pixelsPassed);
}
else if(calculations.passing) {
$.each(settings.onPassed, function(amount, callback) {
if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
module.execute(callback, amount);
}
else if(!settings.once) {
module.remove.occurred(callback);
}
});
}
},
passing: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onPassing,
callbackName = 'passing'
;
if(newCallback) {
module.debug('Adding callback for passing', newCallback);
settings.onPassing = newCallback;
}
if(calculations.passing) {
module.execute(callback, callbackName);
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback !== undefined) {
return calculations.passing;
}
},
topVisible: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onTopVisible,
callbackName = 'topVisible'
;
if(newCallback) {
module.debug('Adding callback for top visible', newCallback);
settings.onTopVisible = newCallback;
}
if(calculations.topVisible) {
module.execute(callback, callbackName);
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return calculations.topVisible;
}
},
bottomVisible: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onBottomVisible,
callbackName = 'bottomVisible'
;
if(newCallback) {
module.debug('Adding callback for bottom visible', newCallback);
settings.onBottomVisible = newCallback;
}
if(calculations.bottomVisible) {
module.execute(callback, callbackName);
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return calculations.bottomVisible;
}
},
topPassed: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onTopPassed,
callbackName = 'topPassed'
;
if(newCallback) {
module.debug('Adding callback for top passed', newCallback);
settings.onTopPassed = newCallback;
}
if(calculations.topPassed) {
module.execute(callback, callbackName);
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return calculations.topPassed;
}
},
bottomPassed: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onBottomPassed,
callbackName = 'bottomPassed'
;
if(newCallback) {
module.debug('Adding callback for bottom passed', newCallback);
settings.onBottomPassed = newCallback;
}
if(calculations.bottomPassed) {
module.execute(callback, callbackName);
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return calculations.bottomPassed;
}
},
passingReverse: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onPassingReverse,
callbackName = 'passingReverse'
;
if(newCallback) {
module.debug('Adding callback for passing reverse', newCallback);
settings.onPassingReverse = newCallback;
}
if(!calculations.passing) {
if(module.get.occurred('passing')) {
module.execute(callback, callbackName);
}
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback !== undefined) {
return !calculations.passing;
}
},
topVisibleReverse: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onTopVisibleReverse,
callbackName = 'topVisibleReverse'
;
if(newCallback) {
module.debug('Adding callback for top visible reverse', newCallback);
settings.onTopVisibleReverse = newCallback;
}
if(!calculations.topVisible) {
if(module.get.occurred('topVisible')) {
module.execute(callback, callbackName);
}
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return !calculations.topVisible;
}
},
bottomVisibleReverse: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onBottomVisibleReverse,
callbackName = 'bottomVisibleReverse'
;
if(newCallback) {
module.debug('Adding callback for bottom visible reverse', newCallback);
settings.onBottomVisibleReverse = newCallback;
}
if(!calculations.bottomVisible) {
if(module.get.occurred('bottomVisible')) {
module.execute(callback, callbackName);
}
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return !calculations.bottomVisible;
}
},
topPassedReverse: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onTopPassedReverse,
callbackName = 'topPassedReverse'
;
if(newCallback) {
module.debug('Adding callback for top passed reverse', newCallback);
settings.onTopPassedReverse = newCallback;
}
if(!calculations.topPassed) {
if(module.get.occurred('topPassed')) {
module.execute(callback, callbackName);
}
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return !calculations.onTopPassed;
}
},
bottomPassedReverse: function(newCallback) {
var
calculations = module.get.elementCalculations(),
callback = newCallback || settings.onBottomPassedReverse,
callbackName = 'bottomPassedReverse'
;
if(newCallback) {
module.debug('Adding callback for bottom passed reverse', newCallback);
settings.onBottomPassedReverse = newCallback;
}
if(!calculations.bottomPassed) {
if(module.get.occurred('bottomPassed')) {
module.execute(callback, callbackName);
}
}
else if(!settings.once) {
module.remove.occurred(callbackName);
}
if(newCallback === undefined) {
return !calculations.bottomPassed;
}
},
execute: function(callback, callbackName) {
var
calculations = module.get.elementCalculations(),
screen = module.get.screenCalculations()
;
callback = callback || false;
if(callback) {
if(settings.continuous) {
module.debug('Callback being called continuously', callbackName, calculations);
callback.call(element, calculations, screen);
}
else if(!module.get.occurred(callbackName)) {
module.debug('Conditions met', callbackName, calculations);
callback.call(element, calculations, screen);
}
}
module.save.occurred(callbackName);
},
remove: {
occurred: function(callback) {
if(callback) {
if(module.cache.occurred[callback] !== undefined && module.cache.occurred[callback] === true) {
module.debug('Callback can now be called again', callback);
module.cache.occurred[callback] = false;
}
}
else {
module.cache.occurred = {};
}
}
},
save: {
calculations: function() {
module.verbose('Saving all calculations necessary to determine positioning');
module.save.direction();
module.save.screenCalculations();
module.save.elementCalculations();
},
occurred: function(callback) {
if(callback) {
if(module.cache.occurred[callback] === undefined || (module.cache.occurred[callback] !== true)) {
module.verbose('Saving callback occurred', callback);
module.cache.occurred[callback] = true;
}
}
},
scroll: function(scrollPosition) {
scrollPosition = scrollPosition + settings.offset || $context.scrollTop() + settings.offset;
module.cache.scroll = scrollPosition;
},
direction: function() {
var
scroll = module.get.scroll(),
lastScroll = module.get.lastScroll(),
direction
;
if(scroll > lastScroll && lastScroll) {
direction = 'down';
}
else if(scroll < lastScroll && lastScroll) {
direction = 'up';
}
else {
direction = 'static';
}
module.cache.direction = direction;
return module.cache.direction;
},
elementPosition: function() {
var
element = module.cache.element,
screen = module.get.screenSize()
;
module.verbose('Saving element position');
// (quicker than $.extend)
element.fits = (element.height < screen.height);
element.offset = $module.offset();
element.width = $module.outerWidth();
element.height = $module.outerHeight();
// store
module.cache.element = element;
return element;
},
elementCalculations: function() {
var
screen = module.get.screenCalculations(),
element = module.get.elementPosition()
;
// offset
if(settings.includeMargin) {
element.margin = {};
element.margin.top = parseInt($module.css('margin-top'), 10);
element.margin.bottom = parseInt($module.css('margin-bottom'), 10);
element.top = element.offset.top - element.margin.top;
element.bottom = element.offset.top + element.height + element.margin.bottom;
}
else {
element.top = element.offset.top;
element.bottom = element.offset.top + element.height;
}
// visibility
element.topVisible = (screen.bottom >= element.top);
element.topPassed = (screen.top >= element.top);
element.bottomVisible = (screen.bottom >= element.bottom);
element.bottomPassed = (screen.top >= element.bottom);
element.pixelsPassed = 0;
element.percentagePassed = 0;
// meta calculations
element.visible = (element.topVisible || element.bottomVisible);
element.passing = (element.topPassed && !element.bottomPassed);
element.hidden = (!element.topVisible && !element.bottomVisible);
// passing calculations
if(element.passing) {
element.pixelsPassed = (screen.top - element.top);
element.percentagePassed = (screen.top - element.top) / element.height;
}
module.cache.element = element;
module.verbose('Updated element calculations', element);
return element;
},
screenCalculations: function() {
var
scroll = module.get.scroll()
;
module.save.direction();
module.cache.screen.top = scroll;
module.cache.screen.bottom = scroll + module.cache.screen.height;
return module.cache.screen;
},
screenSize: function() {
module.verbose('Saving window position');
module.cache.screen = {
height: $context.height()
};
},
position: function() {
module.save.screenSize();
module.save.elementPosition();
}
},
get: {
pixelsPassed: function(amount) {
var
element = module.get.elementCalculations()
;
if(amount.search('%') > -1) {
return ( element.height * (parseInt(amount, 10) / 100) );
}
return parseInt(amount, 10);
},
occurred: function(callback) {
return (module.cache.occurred !== undefined)
? module.cache.occurred[callback] || false
: false
;
},
direction: function() {
if(module.cache.direction === undefined) {
module.save.direction();
}
return module.cache.direction;
},
elementPosition: function() {
if(module.cache.element === undefined) {
module.save.elementPosition();
}
return module.cache.element;
},
elementCalculations: function() {
if(module.cache.element === undefined) {
module.save.elementCalculations();
}
return module.cache.element;
},
screenCalculations: function() {
if(module.cache.screen === undefined) {
module.save.screenCalculations();
}
return module.cache.screen;
},
screenSize: function() {
if(module.cache.screen === undefined) {
module.save.screenSize();
}
return module.cache.screen;
},
scroll: function() {
if(module.cache.scroll === undefined) {
module.save.scroll();
}
return module.cache.scroll;
},
lastScroll: function() {
if(module.cache.screen === undefined) {
module.debug('First scroll event, no last scroll could be found');
return false;
}
return module.cache.screen.top;
}
},
setting: function(name, value) {
if( $.isPlainObject(name) ) {
$.extend(true, settings, name);
}
else if(value !== undefined) {
settings[name] = value;
}
else {
return settings[name];
}
},
internal: function(name, value) {
if( $.isPlainObject(name) ) {
$.extend(true, module, name);
}
else if(value !== undefined) {
module[name] = value;
}
else {
return module[name];
}
},
debug: function() {
if(settings.debug) {
if(settings.performance) {
module.performance.log(arguments);
}
else {
module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
module.debug.apply(console, arguments);
}
}
},
verbose: function() {
if(settings.verbose && settings.debug) {
if(settings.performance) {
module.performance.log(arguments);
}
else {
module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
module.verbose.apply(console, arguments);
}
}
},
error: function() {
module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
module.error.apply(console, arguments);
},
performance: {
log: function(message) {
var
currentTime,
executionTime,
previousTime
;
if(settings.performance) {
currentTime = new Date().getTime();
previousTime = time || currentTime;
executionTime = currentTime - previousTime;
time = currentTime;
performance.push({
'Name' : message[0],
'Arguments' : [].slice.call(message, 1) || '',
'Element' : element,
'Execution Time' : executionTime
});
}
clearTimeout(module.performance.timer);
module.performance.timer = setTimeout(module.performance.display, 500);
},
display: function() {
var
title = settings.name + ':',
totalTime = 0
;
time = false;
clearTimeout(module.performance.timer);
$.each(performance, function(index, data) {
totalTime += data['Execution Time'];
});
title += ' ' + totalTime + 'ms';
if(moduleSelector) {
title += ' \'' + moduleSelector + '\'';
}
if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
console.groupCollapsed(title);
if(console.table) {
console.table(performance);
}
else {
$.each(performance, function(index, data) {
console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
});
}
console.groupEnd();
}
performance = [];
}
},
invoke: function(query, passedArguments, context) {
var
object = instance,
maxDepth,
found,
response
;
passedArguments = passedArguments || queryArguments;
context = element || context;
if(typeof query == 'string' && object !== undefined) {
query = query.split(/[\. ]/);
maxDepth = query.length - 1;
$.each(query, function(depth, value) {
var camelCaseValue = (depth != maxDepth)
? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
: query
;
if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
object = object[camelCaseValue];
}
else if( object[camelCaseValue] !== undefined ) {
found = object[camelCaseValue];
return false;
}
else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
object = object[value];
}
else if( object[value] !== undefined ) {
found = object[value];
return false;
}
else {
module.error(error.method, query);
return false;
}
});
}
if ( $.isFunction( found ) ) {
response = found.apply(context, passedArguments);
}
else if(found !== undefined) {
response = found;
}
if($.isArray(returnedValue)) {
returnedValue.push(response);
}
else if(returnedValue !== undefined) {
returnedValue = [returnedValue, response];
}
else if(response !== undefined) {
returnedValue = response;
}
return found;
}
};
if(methodInvoked) {
if(instance === undefined) {
module.initialize();
}
module.invoke(query);
}
else {
if(instance !== undefined) {
instance.invoke('destroy');
}
module.initialize();
}
})
;
return (returnedValue !== undefined)
? returnedValue
: this
;
};
$.fn.visibility.settings = {
name : 'Visibility',
namespace : 'visibility',
debug : false,
verbose : false,
performance : true,
// whether to use mutation observers to follow changes
observeChanges : true,
// whether to refresh calculations after all page images load
refreshOnLoad : true,
// callback should only occur one time
once : true,
// callback should fire continuously whe evaluates to true
continuous : false,
// offset to use with scroll top
offset : 0,
// whether to include margin in elements position
includeMargin : false,
// scroll context for visibility checks
context : window,
// check position immediately on init
initialCheck : true,
// visibility check delay in ms (defaults to animationFrame)
throttle : false,
// special visibility type (image, fixed)
type : false,
// image only animation settings
transition : false,
duration : 1000,
// array of callbacks for percentage
onPassed : {},
// standard callbacks
onPassing : false,
onTopVisible : false,
onBottomVisible : false,
onTopPassed : false,
onBottomPassed : false,
// reverse callbacks
onPassingReverse : false,
onTopVisibleReverse : false,
onBottomVisibleReverse : false,
onTopPassedReverse : false,
onBottomPassedReverse : false,
// utility callbacks
onUpdate : false, // disabled by default for performance
onRefresh : function(){},
className: {
fixed: 'fixed'
},
error : {
method : 'The method you called is not defined.',
visible : 'Element is hidden, you must call refresh after element becomes visible'
}
};
})( jQuery, window , document );