doevisualizations
Version:
Data Visualization Library based on RequireJS and D3.js (v4+)
345 lines (303 loc) • 9.91 kB
JavaScript
steal('jquery', function ($) {
// Overwrites `jQuery.fn.animate` to use CSS 3 animations if possible
var
// The global animation counter
animationNum = 0,
// The stylesheet for our animations
styleSheet = null,
// The animation cache
cache = [],
// Stores the browser properties like transition end event name and prefix
browser = null,
// Store the original $.fn.animate
oldanimate = $.fn.animate,
// Return the stylesheet, create it if it doesn't exists
getStyleSheet = function () {
if(!styleSheet) {
var style = document.createElement('style');
style.setAttribute("type", "text/css");
style.setAttribute("media", "screen");
document.getElementsByTagName('head')[0].appendChild(style);
if (!window.createPopup) { /* For Safari */
style.appendChild(document.createTextNode(''));
}
styleSheet = style.sheet;
}
return styleSheet;
},
//removes an animation rule from a sheet
removeAnimation = function (sheet, name) {
for (var j = sheet.cssRules.length - 1; j >= 0; j--) {
var rule = sheet.cssRules[j];
// 7 means the keyframe rule
if (rule.type === 7 && rule.name == name) {
sheet.deleteRule(j)
return;
}
}
},
// Returns whether the animation should be passed to the original $.fn.animate.
passThrough = function (props, ops, easing, callback) {
var nonElement = !(this[0] && this[0].nodeType),
isInline = !nonElement && $(this).css("display") === "inline" && $(this).css("float") === "none",
browser = getBrowser();
for (var name in props) {
// jQuery does something with these values
if (props[name] == 'show' || props[name] == 'hide' || props[name] == 'toggle'
// Arrays for individual easing
|| $.isArray(props[name])
// Negative values not handled the same
|| props[name] < 0
// unit-less value
|| name == 'zIndex' || name == 'z-index' || name == 'scrollTop' || name == 'scrollLeft'
) {
return true;
}
}
return props.jquery === true || browser === null || browser.prefix === '-o-' ||
// Animating empty properties
$.isEmptyObject(props) ||
// We can't do custom easing
(easing || easing && typeof easing == 'string') ||
// Second parameter is an object - we can only handle primitives
$.isPlainObject(ops) ||
// Inline and non elements
isInline || nonElement;
},
// Gets a CSS number (with px added as the default unit if the value is a number)
cssValue = function(origName, value) {
if (typeof value === "number" && !$.cssNumber[ origName ]) {
return value += "px";
}
return value;
},
// Feature detection borrowed by http://modernizr.com/
getBrowser = function(){
if(!browser) {
var t,
el = document.createElement('fakeelement'),
transitions = {
'transition': {
transitionEnd : 'transitionend',
prefix : ''
},
'MozTransition': {
transitionEnd : 'animationend',
prefix : '-moz-'
},
'WebkitTransition': {
transitionEnd : 'webkitTransitionEnd',
prefix : '-webkit-'
},
'OTransition': {
transitionEnd : 'oTransitionEnd',
prefix : '-o-'
}
}
for(t in transitions){
if( t in el.style ){
browser = transitions[t];
}
}
}
return browser;
},
// Properties that Firefox can't animate if set to 'auto':
// https://bugzilla.mozilla.org/show_bug.cgi?id=571344
// Provides a converter that returns the actual value
ffProps = {
top : function(el) {
return el.position().top;
},
left : function(el) {
return el.position().left;
},
width : function(el) {
return el.width();
},
height : function(el) {
return el.height();
},
fontSize : function(el) {
return '1em';
}
},
// Add browser specific prefix
addPrefix = function(properties) {
var result = {};
$.each(properties, function(name, value) {
result[getBrowser().prefix + name] = value;
});
return result;
},
// Returns the animation name for a given style. It either uses a cached
// version or adds it to the stylesheet, removing the oldest style if the
// cache has reached a certain size.
getAnimation = function(style) {
var sheet, name, last;
// Look up the cached style, set it to that name and reset age if found
// increment the age for any other animation
$.each(cache, function(i, animation) {
if(style === animation.style) {
name = animation.name;
animation.age = 0;
} else {
animation.age += 1;
}
});
if(!name) { // Add a new style
sheet = getStyleSheet();
name = "jquerypp_animation_" + (animationNum++);
// get the last sheet and insert this rule into it
sheet.insertRule("@" + getBrowser().prefix + "keyframes " + name + ' ' + style,
(sheet.cssRules && sheet.cssRules.length) || 0);
cache.push({
name : name,
style : style,
age : 0
});
// Sort the cache by age
cache.sort(function(first, second) {
return first.age - second.age;
});
// Remove the last (oldest) item from the cache if it has more than 20 items
if(cache.length > 20) {
last = cache.pop();
removeAnimation(sheet, last.name);
}
}
return name;
};
/**
* @parent jQuery.animate
* @function jQuery.fn.animate
* @signature $(element).animate(options)
* @hide
*
* Animate CSS properties using native CSS animations, if possible.
* Uses the original [$.fn.animate()](http://api.$.com/animate/) otherwise.
*
* @param {Object} props The CSS properties to animate
* @param {Integer|String|Object} [speed=400] The animation duration in ms.
* Will use $.fn.animate if a string or object is passed
* @param {Function} [callback] A callback to execute once the animation is complete
* @return {jQuery} The jQuery element
*/
$.fn.animate = function (props, speed, easing, callback) {
//default to normal animations if browser doesn't support them
if (passThrough.apply(this, arguments)) {
return oldanimate.apply(this, arguments);
}
var optall = $.speed(speed, easing, callback),
overflow = [];
// if we are animating height and width properties, set overflow to hidden, and save
// the previous overflow information to replace with when done.
if("height" in props || "width" in props) {
overflow = [this[0].style.overflow, this[0].style.overflowX, this[0].style.overflowY];
this.css('overflow', 'hidden');
}
// Add everything to the animation queue
this.queue(optall.queue, function(done) {
var
//current CSS values
current,
// The list of properties passed
properties = [],
to = "",
prop,
self = $(this),
duration = optall.duration,
//the animation keyframe name
animationName,
// The key used to store the animation hook
dataKey,
//the text for the keyframe
style = "{ from {",
// The animation end event handler.
// Will be called both on animation end and after calling .stop()
animationEnd = function (currentCSS, exec) {
// As long as we don't stop mid animation, then we will replace
// the overflow values of the element being animated.
if(!exec) {
self[0].style.overflow = overflow[0];
self[0].style.overflowX = overflow[1];
self[0].style.overflowY = overflow[2];
}
else {
self.css('overflow', '');
}
self.css(currentCSS);
self.css(addPrefix({
"animation-duration" : "",
"animation-name" : "",
"animation-fill-mode" : "",
"animation-play-state" : ""
}));
// Call the original callback
if ($.isFunction(optall.old) && exec) {
// Call success, pass the DOM element as the this reference
optall.old.call(self[0], true)
}
$.removeData(self, dataKey, true);
},
finishAnimation = function() {
// Call animationEnd using the passed properties
animationEnd(props, true);
done();
};
for(prop in props) {
properties.push(prop);
}
if(getBrowser().prefix === '-moz-' || /Edge\/\d+/.test(navigator.userAgent)) {
// Normalize 'auto' properties in FF
// This is also needed in Edge (tested in 13)
$.each(properties, function(i, prop) {
var converter = ffProps[$.camelCase(prop)];
if(converter && self.css(prop) == 'auto') {
self.css(prop, converter(self));
}
});
}
// Use $.styles
current = self.css.apply(self, properties);
$.each(properties, function(i, cur) {
// Convert a camelcased property name
var name = cur.replace(/([A-Z]|^ms)/g, "-$1" ).toLowerCase();
style += name + " : " + cssValue(cur, current[cur]) + "; ";
to += name + " : " + cssValue(cur, props[cur]) + "; ";
});
style += "} to {" + to + " }}";
animationName = getAnimation(style);
dataKey = animationName + '.run';
// Add a hook which will be called when the animation stops
$._data(this, dataKey, {
stop : function(gotoEnd) {
// Pause the animation
self.css(addPrefix({
'animation-play-state' : 'paused'
}));
// Unbind the animation end handler
self.off(getBrowser().transitionEnd, finishAnimation);
if(!gotoEnd) {
// We were told not to finish the animation
// Call animationEnd but set the CSS to the current computed style
animationEnd(self.styles.apply(self, properties), false);
} else {
// Finish animaion
animationEnd(props, true);
}
}
});
// set this element to point to that animation
self.css(addPrefix({
"animation-duration" : duration + "ms",
"animation-name" : animationName,
"animation-fill-mode": "forwards"
}));
// Attach the transition end event handler to run only once
self.one(getBrowser().transitionEnd, finishAnimation);
});
return this;
};
return $;
});