devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
916 lines (765 loc) • 28 kB
JavaScript
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var $ = require("../core/renderer"),
window = require("../core/utils/window").getWindow(),
eventsEngine = require("../events/core/events_engine"),
errors = require("../core/errors"),
getPublicElement = require("../core/utils/dom").getPublicElement,
extend = require("../core/utils/extend").extend,
typeUtils = require("../core/utils/type"),
iteratorUtils = require("../core/utils/iterator"),
translator = require("./translator"),
easing = require("./easing"),
animationFrame = require("./frame"),
support = require("../core/utils/support"),
positionUtils = require("./position"),
removeEvent = require("../core/remove_event"),
eventUtils = require("../events/utils"),
deferredUtils = require("../core/utils/deferred"),
when = deferredUtils.when,
Deferred = deferredUtils.Deferred,
removeEventName = eventUtils.addNamespace(removeEvent, "dxFX"),
isFunction = typeUtils.isFunction,
isPlainObject = typeUtils.isPlainObject,
noop = require("../core/utils/common").noop;
var RELATIVE_VALUE_REGEX = /^([+-])=(.*)/i,
ANIM_DATA_KEY = "dxAnimData",
ANIM_QUEUE_KEY = "dxAnimQueue",
TRANSFORM_PROP = "transform";
/**
* @name animationConfig
* @publicName animationConfig
* @namespace DevExpress
* @type object
*/
/**
* @name animationConfig.start
* @publicName start
* @type function
* @type_function_param1 $element:dxElement
* @type_function_param2 config:object
*/
/**
* @name animationConfig.complete
* @publicName complete
* @type function
* @type_function_param1 $element:dxElement
* @type_function_param2 config:object
*/
/**
* @name animationConfig.delay
* @publicName delay
* @type number
* @default 0
*/
/**
* @name animationConfig.staggerDelay
* @publicName staggerDelay
* @type number
* @default undefined
*/
/**
* @name animationConfig.duration
* @publicName duration
* @type number
* @default 400
*/
/**
* @name animationConfig.easing
* @publicName easing
* @type string
* @default 'ease'
*/
/**
* @name animationConfig.type
* @publicName type
* @type Enums.AnimationType
* @default 'custom'
*/
/**
* @name animationConfig.direction
* @publicName direction
* @type Enums.Direction
* @default undefined
*/
/**
* @name animationConfig.from
* @publicName from
* @type number|string|object
* @default {}
*/
/**
* @name animationConfig.to
* @publicName to
* @type number|string|object
* @default {}
*/
var TransitionAnimationStrategy = {
initAnimation: function initAnimation($element, config) {
$element.css({
"transitionProperty": "none"
});
if (typeof config.from === "string") {
$element.addClass(config.from);
} else {
setProps($element, config.from);
}
var that = this,
deferred = new Deferred(),
cleanupWhen = config.cleanupWhen;
config.transitionAnimation = {
deferred: deferred,
finish: function finish() {
that._finishTransition($element);
if (cleanupWhen) {
when(deferred, cleanupWhen).always(function () {
that._cleanup($element, config);
});
} else {
that._cleanup($element, config);
}
deferred.resolveWith($element, [config, $element]);
}
};
this._completeAnimationCallback($element, config).done(function () {
config.transitionAnimation.finish();
}).fail(function () {
deferred.rejectWith($element, [config, $element]);
});
if (!config.duration) {
config.transitionAnimation.finish();
}
// NOTE: Hack for setting 'from' css by browser before run animation
// Do not move this hack to initAnimation since some css props can be changed in the 'start' callback (T231434)
// Unfortunately this can't be unit tested
// TODO: find better way if possible
$element.css("transform");
},
animate: function animate($element, config) {
this._startAnimation($element, config);
return config.transitionAnimation.deferred.promise();
},
_completeAnimationCallback: function _completeAnimationCallback($element, config) {
var that = this,
startTime = Date.now() + config.delay,
deferred = new Deferred(),
transitionEndFired = new Deferred(),
simulatedTransitionEndFired = new Deferred(),
simulatedEndEventTimer,
waitForJSCompleteTimer,
transitionEndEventName = support.transitionEndEventName() + ".dxFX";
config.transitionAnimation.cleanup = function () {
clearTimeout(simulatedEndEventTimer);
clearTimeout(waitForJSCompleteTimer);
eventsEngine.off($element, transitionEndEventName);
eventsEngine.off($element, removeEventName);
};
eventsEngine.one($element, transitionEndEventName, function () {
// NOTE: prevent native transitionEnd event from previous animation in queue (Chrome)
if (Date.now() - startTime >= config.duration) {
transitionEndFired.reject();
}
});
eventsEngine.off($element, removeEventName);
eventsEngine.on($element, removeEventName, function () {
that.stop($element, config);
deferred.reject();
});
waitForJSCompleteTimer = setTimeout(function () {
// Fix for a visual bug (T244514): do not setup the timer until all js code has finished working
simulatedEndEventTimer = setTimeout(function () {
simulatedTransitionEndFired.reject();
}, config.duration + config.delay + fx._simulatedTransitionEndDelay /* T255863 */);
when(transitionEndFired, simulatedTransitionEndFired).fail(function () {
deferred.resolve();
}.bind(this));
});
return deferred.promise();
},
_startAnimation: function _startAnimation($element, config) {
$element.css({
"transitionProperty": "all",
"transitionDelay": config.delay + "ms",
"transitionDuration": config.duration + "ms",
"transitionTimingFunction": config.easing
});
if (typeof config.to === "string") {
$element[0].className += " " + config.to;
// Do not uncomment: performance critical
// $element.addClass(config.to);
} else if (config.to) {
setProps($element, config.to);
}
},
_finishTransition: function _finishTransition($element) {
$element.css("transition", "none");
},
_cleanup: function _cleanup($element, config) {
config.transitionAnimation.cleanup();
if (typeof config.from === "string") {
$element.removeClass(config.from);
$element.removeClass(config.to);
}
},
stop: function stop($element, config, jumpToEnd) {
if (!config) {
return;
}
if (jumpToEnd) {
config.transitionAnimation.finish();
} else {
if (isPlainObject(config.to)) {
iteratorUtils.each(config.to, function (key) {
$element.css(key, $element.css(key));
});
}
this._finishTransition($element);
this._cleanup($element, config);
}
}
};
var FrameAnimationStrategy = {
initAnimation: function initAnimation($element, config) {
setProps($element, config.from);
},
animate: function animate($element, config) {
var deferred = new Deferred(),
that = this;
if (!config) {
return deferred.reject().promise();
}
iteratorUtils.each(config.to, function (prop) {
if (config.from[prop] === undefined) {
config.from[prop] = that._normalizeValue($element.css(prop));
}
});
if (config.to[TRANSFORM_PROP]) {
config.from[TRANSFORM_PROP] = that._parseTransform(config.from[TRANSFORM_PROP]);
config.to[TRANSFORM_PROP] = that._parseTransform(config.to[TRANSFORM_PROP]);
}
config.frameAnimation = {
to: config.to,
from: config.from,
currentValue: config.from,
easing: easing.convertTransitionTimingFuncToEasing(config.easing),
duration: config.duration,
startTime: new Date().valueOf(),
finish: function finish() {
this.currentValue = this.to;
this.draw();
animationFrame.cancelAnimationFrame(config.frameAnimation.animationFrameId);
deferred.resolve();
},
draw: function draw() {
if (config.draw) {
config.draw(this.currentValue);
return;
}
var currentValue = extend({}, this.currentValue);
if (currentValue[TRANSFORM_PROP]) {
currentValue[TRANSFORM_PROP] = iteratorUtils.map(currentValue[TRANSFORM_PROP], function (value, prop) {
if (prop === "translate") {
return translator.getTranslateCss(value);
} else if (prop === "scale") {
return "scale(" + value + ")";
} else if (prop.substr(0, prop.length - 1) === "rotate") {
return prop + "(" + value + "deg)";
}
}).join(" ");
}
$element.css(currentValue);
}
};
if (config.delay) {
config.frameAnimation.startTime += config.delay;
config.frameAnimation.delayTimeout = setTimeout(function () {
that._startAnimation($element, config);
}, config.delay);
} else {
that._startAnimation($element, config);
}
return deferred.promise();
},
_startAnimation: function _startAnimation($element, config) {
eventsEngine.off($element, removeEventName);
eventsEngine.on($element, removeEventName, function () {
if (config.frameAnimation) {
animationFrame.cancelAnimationFrame(config.frameAnimation.animationFrameId);
}
});
this._animationStep($element, config);
},
_parseTransform: function _parseTransform(transformString) {
var result = {};
iteratorUtils.each(transformString.match(/(\w|\d)+\([^\)]*\)\s*/g), function (i, part) {
var translateData = translator.parseTranslate(part),
scaleData = part.match(/scale\((.+?)\)/),
rotateData = part.match(/(rotate.)\((.+)deg\)/);
if (translateData) {
result.translate = translateData;
}
if (scaleData && scaleData[1]) {
result.scale = parseFloat(scaleData[1]);
}
if (rotateData && rotateData[1]) {
result[rotateData[1]] = parseFloat(rotateData[2]);
}
});
return result;
},
stop: function stop($element, config, jumpToEnd) {
var frameAnimation = config && config.frameAnimation;
if (!frameAnimation) {
return;
}
animationFrame.cancelAnimationFrame(frameAnimation.animationFrameId);
clearTimeout(frameAnimation.delayTimeout);
if (jumpToEnd) {
frameAnimation.finish();
}
delete config.frameAnimation;
},
_animationStep: function _animationStep($element, config) {
var frameAnimation = config && config.frameAnimation;
if (!frameAnimation) {
return;
}
var now = new Date().valueOf();
if (now >= frameAnimation.startTime + frameAnimation.duration) {
frameAnimation.finish();
return;
}
frameAnimation.currentValue = this._calcStepValue(frameAnimation, now - frameAnimation.startTime);
frameAnimation.draw();
var that = this;
frameAnimation.animationFrameId = animationFrame.requestAnimationFrame(function () {
that._animationStep($element, config);
});
},
_calcStepValue: function _calcStepValue(frameAnimation, currentDuration) {
var calcValueRecursively = function calcValueRecursively(from, to) {
var result = Array.isArray(to) ? [] : {};
var calcEasedValue = function calcEasedValue(propName) {
var x = currentDuration / frameAnimation.duration,
t = currentDuration,
b = 1 * from[propName],
c = to[propName] - from[propName],
d = frameAnimation.duration;
return easing.getEasing(frameAnimation.easing)(x, t, b, c, d);
};
iteratorUtils.each(to, function (propName, endPropValue) {
if (typeof endPropValue === "string" && parseFloat(endPropValue, 10) === false) {
return true;
}
result[propName] = (typeof endPropValue === "undefined" ? "undefined" : _typeof(endPropValue)) === "object" ? calcValueRecursively(from[propName], endPropValue) : calcEasedValue(propName);
});
return result;
};
return calcValueRecursively(frameAnimation.from, frameAnimation.to);
},
_normalizeValue: function _normalizeValue(value) {
var numericValue = parseFloat(value, 10);
if (numericValue === false) {
return value;
}
return numericValue;
}
};
var FallbackToNoAnimationStrategy = {
initAnimation: function initAnimation() {},
animate: function animate() {
return new Deferred().resolve().promise();
},
stop: noop,
isSynchronous: true
};
var getAnimationStrategy = function getAnimationStrategy(config) {
config = config || {};
var animationStrategies = {
"transition": support.transition() ? TransitionAnimationStrategy : FrameAnimationStrategy,
"frame": FrameAnimationStrategy,
"noAnimation": FallbackToNoAnimationStrategy
};
var strategy = config.strategy || "transition";
if (config.type === "css" && !support.transition()) {
strategy = "noAnimation";
}
return animationStrategies[strategy];
};
var baseConfigValidator = function baseConfigValidator(config, animationType, validate, typeMessage) {
iteratorUtils.each(["from", "to"], function () {
if (!validate(config[this])) {
throw errors.Error("E0010", animationType, this, typeMessage);
}
});
};
var isObjectConfigValidator = function isObjectConfigValidator(config, animationType) {
return baseConfigValidator(config, animationType, function (target) {
return isPlainObject(target);
}, "a plain object");
};
var isStringConfigValidator = function isStringConfigValidator(config, animationType) {
return baseConfigValidator(config, animationType, function (target) {
return typeof target === "string";
}, "a string");
};
var CustomAnimationConfigurator = {
setup: function setup() {}
};
var CssAnimationConfigurator = {
validateConfig: function validateConfig(config) {
isStringConfigValidator(config, "css");
},
setup: function setup() {}
};
var positionAliases = {
"top": { my: "bottom center", at: "top center" },
"bottom": { my: "top center", at: "bottom center" },
"right": { my: "left center", at: "right center" },
"left": { my: "right center", at: "left center" }
};
var SlideAnimationConfigurator = {
validateConfig: function validateConfig(config) {
isObjectConfigValidator(config, "slide");
},
setup: function setup($element, config) {
var location = translator.locate($element);
if (config.type !== "slide") {
var positioningConfig = config.type === "slideIn" ? config.from : config.to;
positioningConfig.position = extend({ of: window }, positionAliases[config.direction]);
setupPosition($element, positioningConfig);
}
this._setUpConfig(location, config.from);
this._setUpConfig(location, config.to);
translator.clearCache($element);
},
_setUpConfig: function _setUpConfig(location, config) {
config.left = "left" in config ? config.left : "+=0";
config.top = "top" in config ? config.top : "+=0";
this._initNewPosition(location, config);
},
_initNewPosition: function _initNewPosition(location, config) {
var position = {
left: config.left,
top: config.top
};
delete config.left;
delete config.top;
var relativeValue = this._getRelativeValue(position.left);
if (relativeValue !== undefined) {
position.left = relativeValue + location.left;
} else {
config.left = 0;
}
relativeValue = this._getRelativeValue(position.top);
if (relativeValue !== undefined) {
position.top = relativeValue + location.top;
} else {
config.top = 0;
}
config[TRANSFORM_PROP] = translator.getTranslateCss({ x: position.left, y: position.top });
},
_getRelativeValue: function _getRelativeValue(value) {
var relativeValue;
if (typeof value === "string" && (relativeValue = RELATIVE_VALUE_REGEX.exec(value))) {
return parseInt(relativeValue[1] + "1") * relativeValue[2];
}
}
};
var FadeAnimationConfigurator = {
setup: function setup($element, config) {
var from = config.from,
fromOpacity = isPlainObject(from) ? config.skipElementInitialStyles ? 0 : $element.css("opacity") : String(from),
toOpacity;
switch (config.type) {
case "fadeIn":
toOpacity = 1;
break;
case "fadeOut":
toOpacity = 0;
break;
default:
toOpacity = String(config.to);
}
config.from = {
visibility: "visible",
opacity: fromOpacity
};
config.to = {
opacity: toOpacity
};
}
};
var PopAnimationConfigurator = {
validateConfig: function validateConfig(config) {
isObjectConfigValidator(config, "pop");
},
setup: function setup($element, config) {
var from = config.from,
to = config.to,
fromOpacity = "opacity" in from ? from.opacity : $element.css("opacity"),
toOpacity = "opacity" in to ? to.opacity : 1,
fromScale = "scale" in from ? from.scale : 0,
toScale = "scale" in to ? to.scale : 1;
config.from = {
opacity: fromOpacity
};
var translate = translator.getTranslate($element);
config.from[TRANSFORM_PROP] = this._getCssTransform(translate, fromScale);
config.to = {
opacity: toOpacity
};
config.to[TRANSFORM_PROP] = this._getCssTransform(translate, toScale);
},
_getCssTransform: function _getCssTransform(translate, scale) {
return translator.getTranslateCss(translate) + "scale(" + scale + ")";
}
};
var animationConfigurators = {
"custom": CustomAnimationConfigurator,
"slide": SlideAnimationConfigurator,
"slideIn": SlideAnimationConfigurator,
"slideOut": SlideAnimationConfigurator,
"fade": FadeAnimationConfigurator,
"fadeIn": FadeAnimationConfigurator,
"fadeOut": FadeAnimationConfigurator,
"pop": PopAnimationConfigurator,
"css": CssAnimationConfigurator
};
var getAnimationConfigurator = function getAnimationConfigurator(config) {
var result = animationConfigurators[config.type];
if (!result) {
throw errors.Error("E0011", config.type);
}
return result;
};
var defaultJSConfig = {
type: "custom",
from: {},
to: {},
duration: 400,
start: noop,
complete: noop,
easing: "ease",
delay: 0
},
defaultCssConfig = {
duration: 400,
easing: "ease",
delay: 0
};
var setupAnimationOnElement = function setupAnimationOnElement() {
var animation = this,
$element = animation.element,
config = animation.config;
setupPosition($element, config.from);
setupPosition($element, config.to);
animation.configurator.setup($element, config);
$element.data(ANIM_DATA_KEY, animation);
if (fx.off) {
config.duration = 0;
config.delay = 0;
}
animation.strategy.initAnimation($element, config);
if (config.start) {
var element = getPublicElement($element);
config.start.apply(this, [element, config]);
}
};
var onElementAnimationComplete = function onElementAnimationComplete(animation) {
var $element = animation.element,
config = animation.config;
$element.removeData(ANIM_DATA_KEY);
if (config.complete) {
var element = getPublicElement($element);
config.complete.apply(this, [element, config]);
}
animation.deferred.resolveWith(this, [$element, config]);
};
var startAnimationOnElement = function startAnimationOnElement() {
var animation = this,
$element = animation.element,
config = animation.config;
animation.isStarted = true;
return animation.strategy.animate($element, config).done(function () {
onElementAnimationComplete(animation);
}).fail(function () {
animation.deferred.rejectWith(this, [$element, config]);
});
};
var stopAnimationOnElement = function stopAnimationOnElement(jumpToEnd) {
var animation = this,
$element = animation.element,
config = animation.config;
clearTimeout(animation.startTimeout);
if (!animation.isStarted) {
animation.start();
}
animation.strategy.stop($element, config, jumpToEnd);
};
var scopedRemoveEvent = eventUtils.addNamespace(removeEvent, "dxFXStartAnimation");
var subscribeToRemoveEvent = function subscribeToRemoveEvent(animation) {
eventsEngine.off(animation.element, scopedRemoveEvent);
eventsEngine.on(animation.element, scopedRemoveEvent, function () {
fx.stop(animation.element);
});
animation.deferred.always(function () {
eventsEngine.off(animation.element, scopedRemoveEvent);
});
};
var createAnimation = function createAnimation(element, initialConfig) {
var defaultConfig = initialConfig.type === "css" ? defaultCssConfig : defaultJSConfig,
config = extend(true, {}, defaultConfig, initialConfig),
configurator = getAnimationConfigurator(config),
strategy = getAnimationStrategy(config),
animation = {
element: $(element),
config: config,
configurator: configurator,
strategy: strategy,
isSynchronous: strategy.isSynchronous,
setup: setupAnimationOnElement,
start: startAnimationOnElement,
stop: stopAnimationOnElement,
deferred: new Deferred()
};
if (isFunction(configurator.validateConfig)) {
configurator.validateConfig(config);
}
subscribeToRemoveEvent(animation);
return animation;
};
var animate = function animate(element, config) {
var $element = $(element);
if (!$element.length) {
return new Deferred().resolve().promise();
}
var animation = createAnimation($element, config);
pushInAnimationQueue($element, animation);
return animation.deferred.promise();
};
var pushInAnimationQueue = function pushInAnimationQueue($element, animation) {
var queueData = getAnimQueueData($element);
writeAnimQueueData($element, queueData);
queueData.push(animation);
if (!isAnimating($element)) {
shiftFromAnimationQueue($element, queueData);
}
};
var getAnimQueueData = function getAnimQueueData($element) {
return $element.data(ANIM_QUEUE_KEY) || [];
};
var writeAnimQueueData = function writeAnimQueueData($element, queueData) {
$element.data(ANIM_QUEUE_KEY, queueData);
};
var destroyAnimQueueData = function destroyAnimQueueData($element) {
$element.removeData(ANIM_QUEUE_KEY);
};
var isAnimating = function isAnimating($element) {
return !!$element.data(ANIM_DATA_KEY);
};
var shiftFromAnimationQueue = function shiftFromAnimationQueue($element, queueData) {
queueData = getAnimQueueData($element);
if (!queueData.length) {
return;
}
var animation = queueData.shift();
if (queueData.length === 0) {
destroyAnimQueueData($element);
}
executeAnimation(animation).done(function () {
if (!isAnimating($element)) {
shiftFromAnimationQueue($element);
}
});
};
var executeAnimation = function executeAnimation(animation) {
animation.setup();
if (fx.off || animation.isSynchronous) {
animation.start();
} else {
animation.startTimeout = setTimeout(function () {
animation.start();
});
}
return animation.deferred.promise();
};
var setupPosition = function setupPosition($element, config) {
if (!config || !config.position) {
return;
}
var position = positionUtils.calculate($element, config.position),
offset = $element.offset(),
currentPosition = $element.position();
extend(config, {
left: position.h.location - offset.left + currentPosition.left,
top: position.v.location - offset.top + currentPosition.top
});
delete config.position;
};
var setProps = function setProps($element, props) {
iteratorUtils.each(props, function (key, value) {
try {
$element.css(key, typeUtils.isFunction(value) ? value() : value);
} catch (e) {}
});
};
var stop = function stop(element, jumpToEnd) {
var $element = $(element),
queueData = getAnimQueueData($element);
// TODO: think about complete all animation in queue
iteratorUtils.each(queueData, function (_, animation) {
animation.config.delay = 0;
animation.config.duration = 0;
animation.isSynchronous = true;
});
if (!isAnimating($element)) {
shiftFromAnimationQueue($element, queueData);
}
var animation = $element.data(ANIM_DATA_KEY);
if (animation) {
animation.stop(jumpToEnd);
}
$element.removeData(ANIM_DATA_KEY);
destroyAnimQueueData($element);
};
/**
* @name fx
* @publicName fx
* @section utils
* @module animation/fx
* @namespace DevExpress
* @export default
*/
var fx = {
off: false,
animationTypes: animationConfigurators,
/**
* @name fxmethods.animate
* @publicName animate(element, config)
* @param1 element:Node
* @param2 config:animationConfig
* @return Promise<void>
* @namespace DevExpress.fx
*/
animate: animate,
createAnimation: createAnimation,
/**
* @name fxmethods.isAnimating
* @publicName isAnimating(element)
* @param1 element:Node
* @return boolean
* @namespace DevExpress.fx
*/
isAnimating: isAnimating,
/**
* @name fxmethods.stop
* @publicName stop(element, jumpToEnd)
* @param1 element:Node
* @param2 jumpToEnd:boolean
* @namespace DevExpress.fx
*/
stop: stop,
_simulatedTransitionEndDelay: 100
};
module.exports = fx;
;